#![forbid(unsafe_code)]
use crate::error::{Result, TallyError};
use anchor_client::solana_sdk::{
hash::Hash,
instruction::Instruction,
message::{Message, VersionedMessage},
pubkey::Pubkey,
signature::Signature,
transaction::VersionedTransaction,
};
use base64::{engine::general_purpose::STANDARD, Engine};
#[derive(Debug, Clone)]
pub struct SubscribeTransactionParams<'a> {
pub user: &'a Pubkey,
pub plan_pda: &'a Pubkey,
pub plan_price: u64,
pub allowance_periods: u8,
pub recent_blockhash: Hash,
pub merchant: &'a crate::program_types::Merchant,
pub plan: &'a crate::program_types::Plan,
pub platform_treasury_ata: &'a Pubkey,
}
#[must_use]
pub const fn convert_anchor_pubkey(pk: &Pubkey) -> Pubkey {
*pk }
#[must_use]
pub fn create_memo_instruction(memo: &str) -> Instruction {
Instruction {
program_id: spl_memo::ID,
accounts: vec![],
data: memo.as_bytes().to_vec(),
}
}
pub fn build_transaction(
instructions: &[Instruction],
payer: &Pubkey,
recent_blockhash: Hash,
) -> Result<String> {
let message = Message::new_with_blockhash(instructions, Some(payer), &recent_blockhash);
let num_signatures = message.header.num_required_signatures;
let versioned_message = VersionedMessage::Legacy(message);
let transaction = VersionedTransaction {
signatures: vec![Signature::default(); num_signatures as usize],
message: versioned_message,
};
let serialized = bincode::serialize(&transaction)
.map_err(|e| TallyError::Generic(format!("Transaction serialization failed: {e}")))?;
Ok(STANDARD.encode(serialized))
}
pub fn get_user_usdc_ata(user: &Pubkey, usdc_mint: &Pubkey) -> Result<Pubkey> {
crate::ata::get_associated_token_address_for_mint(user, usdc_mint)
}
#[must_use]
pub fn map_tally_error_to_string(err: &TallyError) -> String {
match err {
TallyError::Generic(msg) => msg.clone(),
TallyError::InvalidPda(msg) | TallyError::InvalidSubscriptionState(msg) => {
format!("Invalid account: {msg}")
}
TallyError::SplToken(err) => format!("SPL Token error: {err}"),
TallyError::Program(err) => format!("Program error: {err}"),
TallyError::Solana(err) => format!("Solana error: {err}"),
TallyError::InvalidTokenProgram { expected, found } => {
format!("Invalid token program: expected {expected}, found {found}")
}
TallyError::AccountNotFound(account) => format!("Account not found: {account}"),
TallyError::InsufficientFunds {
required,
available,
} => {
format!("Insufficient funds: required {required}, available {available}")
}
TallyError::TokenProgramDetectionFailed { mint } => {
format!("Failed to detect token program for mint: {mint}")
}
_ => err.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_anchor_pubkey() {
let pk = Pubkey::new_unique();
let converted = convert_anchor_pubkey(&pk);
assert_eq!(pk, converted);
}
#[test]
fn test_create_memo_instruction() {
let memo = "Test memo";
let instruction = create_memo_instruction(memo);
assert_eq!(instruction.program_id, spl_memo::ID);
assert_eq!(instruction.data, memo.as_bytes());
assert!(instruction.accounts.is_empty());
}
#[test]
fn test_build_transaction() {
let payer = Pubkey::new_unique();
let recent_blockhash = Hash::default();
let instruction = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![],
data: vec![],
};
let instructions = vec![instruction];
let result = build_transaction(&instructions, &payer, recent_blockhash);
assert!(result.is_ok());
let transaction_b64 = result.unwrap();
assert!(!transaction_b64.is_empty());
let decoded = STANDARD.decode(&transaction_b64);
assert!(decoded.is_ok());
let transaction_bytes = decoded.unwrap();
let transaction: std::result::Result<VersionedTransaction, _> =
bincode::deserialize(&transaction_bytes);
assert!(transaction.is_ok());
}
#[test]
fn test_get_user_usdc_ata() {
let user = Pubkey::new_unique();
let usdc_mint = Pubkey::new_unique();
let result1 = get_user_usdc_ata(&user, &usdc_mint);
let result2 = get_user_usdc_ata(&user, &usdc_mint);
assert!(result1.is_ok());
assert!(result2.is_ok());
assert_eq!(result1.unwrap(), result2.unwrap());
}
#[test]
fn test_map_tally_error_to_string() {
let generic_error = TallyError::Generic("Test error".to_string());
let mapped = map_tally_error_to_string(&generic_error);
assert_eq!(mapped, "Test error");
let account_error = TallyError::AccountNotFound("test_account".to_string());
let mapped = map_tally_error_to_string(&account_error);
assert_eq!(mapped, "Account not found: test_account");
}
}