universalsettle-api 0.1.6

X402-inspired settlement program for any token on Solana
Documentation
use steel::*;

use crate::{
    instruction::*,
    state::{authority_transfer_pda, config_pda, split_vault_pda, vault_sol_storage_pda},
};

/// Builds an instruction to create a SplitVault for a seller.
///
/// # Arguments
///
/// * `payer` - Fee payer for the account creation (signer)
/// * `seller` - Resource owner wallet address
pub fn create_vault(payer: Pubkey, seller: Pubkey) -> Instruction {
    let (vault_pda, _) = split_vault_pda(&seller);
    let (vault_sol_storage, _) = vault_sol_storage_pda(vault_pda);
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(payer, true),
            AccountMeta::new(vault_pda, false),
            AccountMeta::new(vault_sol_storage, false),
            AccountMeta::new_readonly(solana_program::system_program::ID, false),
        ],
        data: CreateVault { seller }.to_bytes(),
    }
}

/// Builds an instruction to sweep funds from a vault to the seller and facilitator.
///
/// # Arguments
///
/// * `payer` - The fee payer and authorized signer (Facilitator)
/// * `vault` - The SplitVault PDA
/// * `seller` - The seller's wallet
/// * `fee_destination` - The facilitator's fee wallet
/// * `token_mint` - For SOL: Pubkey::default(), For SPL: the mint address
/// * `amount` - Amount to sweep (0 = all)
/// * `is_sol` - Whether to sweep native SOL or SPL tokens
/// * `vault_tokens` - The vault's token account (for SPL)
/// * `seller_tokens` - The seller's token account (for SPL)
/// * `fee_dest_tokens` - The facilitator's token account (for SPL)
#[allow(clippy::too_many_arguments)]
pub fn sweep(
    payer: Pubkey,
    vault: Pubkey,
    seller: Pubkey,
    fee_destination: Pubkey,
    token_mint: Pubkey,
    amount: u64,
    is_sol: bool,
    vault_tokens: Option<Pubkey>,
    seller_tokens: Option<Pubkey>,
    fee_dest_tokens: Option<Pubkey>,
    token_program: Option<Pubkey>,
) -> Instruction {
    let (config_pda, _) = config_pda();
    let mut accounts = vec![
        AccountMeta::new(payer, true),
        // vault (SplitVault PDA) must be writable to update recovery status
        AccountMeta::new(vault, false),
        AccountMeta::new_readonly(config_pda, false),
    ];

    if is_sol {
        let (vault_sol_storage, _) = vault_sol_storage_pda(vault);
        accounts.push(AccountMeta::new(vault_sol_storage, false));
        accounts.push(AccountMeta::new(seller, false));
        accounts.push(AccountMeta::new(fee_destination, false));
        accounts.push(AccountMeta::new_readonly(
            solana_program::system_program::ID,
            false,
        ));
    } else {
        accounts.push(AccountMeta::new(
            vault_tokens.expect("vault_tokens is required for SPL sweep"),
            false,
        ));
        accounts.push(AccountMeta::new(
            seller_tokens.expect("seller_tokens is required for SPL sweep"),
            false,
        ));
        accounts.push(AccountMeta::new(
            fee_dest_tokens.expect("fee_dest_tokens is required for SPL sweep"),
            false,
        ));
        accounts.push(AccountMeta::new_readonly(token_mint, false));
        accounts.push(AccountMeta::new_readonly(
            token_program.unwrap_or(spl_token::ID),
            false,
        ));
    }

    Instruction {
        program_id: crate::ID,
        accounts,
        data: Sweep {
            token_mint,
            amount: amount.to_le_bytes(),
            is_sol: if is_sol { [1] } else { [0] },
            _padding: [0; 7],
        }
        .to_bytes(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn spl_sweep_uses_token_mint_as_mint_account() {
        let payer = Pubkey::new_unique();
        let vault = Pubkey::new_unique();
        let seller = Pubkey::new_unique();
        let fee_destination = Pubkey::new_unique();
        let token_mint = Pubkey::new_unique();
        let vault_tokens = Pubkey::new_unique();
        let seller_tokens = Pubkey::new_unique();
        let fee_dest_tokens = Pubkey::new_unique();

        let ix = sweep(
            payer,
            vault,
            seller,
            fee_destination,
            token_mint,
            0,
            false,
            Some(vault_tokens),
            Some(seller_tokens),
            Some(fee_dest_tokens),
            Some(spl_token::ID),
        );

        assert_eq!(ix.accounts[6].pubkey, token_mint);
        assert_eq!(ix.accounts[7].pubkey, spl_token::ID);
    }
}

/// Create an initialize instruction
#[allow(clippy::too_many_arguments)]
pub fn initialize(
    authority: Pubkey,
    fee_destination: Pubkey,
    fee_bps: Option<u16>,
    min_fee_amount: Option<u64>,
    min_fee_amount_sol: Option<u64>,
    provisioning_fee_sol: Option<u64>,
    provisioning_fee_spl: Option<u64>,
    discounted_fee_bps: Option<u16>,
) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
            AccountMeta::new_readonly(solana_program::system_program::ID, false),
            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
        ],
        data: Initialize {
            fee_destination,
            min_fee_amount: (min_fee_amount.unwrap_or(10_000)).to_le_bytes(),
            min_fee_amount_sol: (min_fee_amount_sol.unwrap_or(200_000)).to_le_bytes(),
            provisioning_fee_sol: (provisioning_fee_sol.unwrap_or(10_000_000)).to_le_bytes(),
            provisioning_fee_spl: (provisioning_fee_spl.unwrap_or(1_000_000)).to_le_bytes(),
            fee_bps: (fee_bps.unwrap_or(100)).to_le_bytes(),
            discounted_fee_bps: (discounted_fee_bps.unwrap_or(95)).to_le_bytes(),
            _padding: [0; 4],
        }
        .to_bytes(),
    }
}

/// Create an update authority instruction (Step 1: Propose)
pub fn update_authority(current_authority: Pubkey, new_authority: Pubkey) -> Instruction {
    let (config_pda, _) = config_pda();
    let (transfer_pda, _) = authority_transfer_pda(config_pda);

    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(current_authority, true),
            AccountMeta::new_readonly(config_pda, false),
            AccountMeta::new(transfer_pda, false),
            AccountMeta::new_readonly(solana_program::system_program::ID, false),
        ],
        data: UpdateAuthority { new_authority }.to_bytes(),
    }
}

/// Create an accept authority instruction (Step 2: Accept)
pub fn accept_authority(new_authority: Pubkey) -> Instruction {
    let (config_pda, _) = config_pda();
    let (transfer_pda, _) = authority_transfer_pda(config_pda);

    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(new_authority, true), // signer, writable
            AccountMeta::new(config_pda, false),   // writable
            AccountMeta::new(transfer_pda, false), // writable
        ],
        data: AcceptAuthority {}.to_bytes(),
    }
}

/// Create a cancel authority proposal instruction
pub fn cancel_authority_proposal(current_authority: Pubkey) -> Instruction {
    let (config_pda, _) = config_pda();
    let (transfer_pda, _) = authority_transfer_pda(config_pda);

    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(current_authority, true), // signer, writable
            AccountMeta::new_readonly(config_pda, false),
            AccountMeta::new(transfer_pda, false), // writable
        ],
        data: CancelAuthorityProposal {}.to_bytes(),
    }
}

/// Create an update fee rate instruction
pub fn update_fee_rate(authority: Pubkey, new_fee_bps: u16) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
        ],
        data: UpdateFeeRate {
            new_fee_bps: new_fee_bps.to_le_bytes(),
            _padding: [0; 6],
        }
        .to_bytes(),
    }
}

/// Create an update fee destination instruction
pub fn update_fee_destination(authority: Pubkey, new_fee_destination: Pubkey) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
        ],
        data: UpdateFeeDestination {
            new_fee_destination,
        }
        .to_bytes(),
    }
}

/// Create an update min fee amount instruction
pub fn update_min_fee_amount(authority: Pubkey, new_min_fee_amount: u64) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
        ],
        data: UpdateMinFeeAmount {
            new_min_fee_amount: new_min_fee_amount.to_le_bytes(),
        }
        .to_bytes(),
    }
}

/// Create an update provisioning fee instruction
pub fn update_provisioning_fee(
    authority: Pubkey,
    new_provisioning_fee_sol: u64,
    new_provisioning_fee_spl: u64,
) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
        ],
        data: UpdateProvisioningFee {
            new_provisioning_fee_sol: new_provisioning_fee_sol.to_le_bytes(),
            new_provisioning_fee_spl: new_provisioning_fee_spl.to_le_bytes(),
        }
        .to_bytes(),
    }
}

/// Create an update discounted fee rate instruction
pub fn update_discounted_fee_rate(authority: Pubkey, new_discounted_fee_bps: u16) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
        ],
        data: UpdateDiscountedFeeRate {
            new_discounted_fee_bps: new_discounted_fee_bps.to_le_bytes(),
            _padding: [0; 6],
        }
        .to_bytes(),
    }
}

/// Create an update min fee amount sol instruction
pub fn update_min_fee_amount_sol(authority: Pubkey, new_min_fee_amount_sol: u64) -> Instruction {
    let (config_pda, _) = config_pda();
    Instruction {
        program_id: crate::ID,
        accounts: vec![
            AccountMeta::new(authority, true),
            AccountMeta::new(config_pda, false),
        ],
        data: UpdateMinFeeAmountSol {
            new_min_fee_amount_sol: new_min_fee_amount_sol.to_le_bytes(),
        }
        .to_bytes(),
    }
}