├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── Xargo.toml ├── scripts ├── patch.crates-io.sh └── update-solana-dependencies.sh └── src ├── engine.rs ├── engine ├── cancel_subscription.rs ├── common.rs ├── constants.rs ├── json.rs ├── pay.rs ├── register.rs ├── renew.rs ├── subscribe.rs └── withdraw.rs ├── entrypoint.rs ├── error.rs ├── instruction.rs ├── lib.rs ├── processor.rs ├── state.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /*-dump.txt 2 | /*.so 3 | /target/ 4 | /test-ledger/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sol-payment-processor" 3 | version = "0.1.0" 4 | edition = "2018" 5 | license = "WTFPL" 6 | publish = false 7 | 8 | [dependencies] 9 | arrayref = "0.3.6" 10 | solana-program = "=1.7.1" 11 | thiserror = "1.0.23" 12 | borsh = "=0.9.0" 13 | serde = "1.0.126" 14 | serde_json = "1.0.64" 15 | num-traits = "0.2.14" 16 | num-derive = "0.3.3" 17 | spl-token = {version = "3.0.1", features = ["no-entrypoint"]} 18 | 19 | [features] 20 | no-entrypoint = [] 21 | test-bpf = [] 22 | 23 | [dev-dependencies] 24 | assert_matches = "1.4.0" 25 | solana-sdk = "=1.7.1" 26 | solana-program-test = "=1.7.1" 27 | 28 | [lib] 29 | crate-type = ["cdylib", "lib"] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sol Payments 2 | 3 | SolPayments is a smart contract program build for the [Solana blockchain](https://solana.com/) that allows merchants to receive crypto-currency payments at their online shops. 4 | 5 | More information: [SolPayments Official Website](https://solpayments.com/) 6 | 7 | ## Benefits of using SolPayments 8 | 9 | - **Low fees** - SolPayments charges merchants just 0.3% of the transaction value. Additionally, the fees charged to buyers for sending the crypto-currency payments are almost free (just a few cents) 10 | - **Fast** - payments made through SolPayments are completed in a few seconds 11 | - **Non-custodial** - SolPayments never takes custody of payments made to any merchants that use it. You are always in full control of your money. 12 | 13 | ## Program API 14 | 15 | All the instructions supported by the Sol Payments program are documented [here](src/instruction.rs). 16 | 17 | ## Contributing 18 | 19 | ### Environment Setup 20 | 21 | 1. Install Rust from [https://rustup.rs/](https://rustup.rs/) 22 | 2. Install Solana v1.5.0 or later from [https://docs.solana.com/cli/install-solana-cli-tools#use-solanas-install-tool](https://docs.solana.com/cli/install-solana-cli-tools#use-solanas-install-tool) 23 | 24 | ### Build and test for program compiled natively 25 | 26 | ```sh 27 | $ cargo build 28 | $ cargo test 29 | ``` 30 | 31 | ### Build and test the program compiled for BPF 32 | 33 | ```sh 34 | $ cargo build-bpf 35 | $ cargo test-bpf 36 | ``` 37 | -------------------------------------------------------------------------------- /Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /scripts/patch.crates-io.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Patches the SPL crates for developing against a local solana monorepo 4 | # 5 | 6 | here="$(dirname "$0")" 7 | 8 | solana_dir=$1 9 | if [[ -z $solana_dir ]]; then 10 | echo "Usage: $0 " 11 | exit 1 12 | fi 13 | 14 | workspace_crates=( 15 | "$here"/../Cargo.toml 16 | ) 17 | 18 | if [[ ! -r "$solana_dir"/scripts/read-cargo-variable.sh ]]; then 19 | echo "$solana_dir is not a path to the solana monorepo" 20 | exit 1 21 | fi 22 | 23 | set -e 24 | 25 | solana_dir=$(cd "$solana_dir" && pwd) 26 | 27 | source "$solana_dir"/scripts/read-cargo-variable.sh 28 | solana_ver=$(readCargoVariable version "$solana_dir"/sdk/Cargo.toml) 29 | 30 | echo "Patching in $solana_ver from $solana_dir" 31 | 32 | if ! git diff --quiet && [[ -z $DIRTY_OK ]]; then 33 | echo "Error: dirty tree" 34 | exit 1 35 | fi 36 | export DIRTY_OK=1 37 | 38 | for crate in "${workspace_crates[@]}"; do 39 | if grep -q '\[patch.crates-io\]' "$crate"; then 40 | echo "* $crate is already patched" 41 | else 42 | echo "* patched $crate" 43 | cat >> "$crate" <" 11 | exit 1 12 | fi 13 | 14 | if [[ $solana_ver =~ ^v ]]; then 15 | # Drop `v` from v1.2.3... 16 | solana_ver=${solana_ver:1} 17 | fi 18 | 19 | cd "$here"/.. 20 | 21 | echo "Updating Solana version to $solana_ver in $PWD" 22 | 23 | if ! git diff --quiet && [[ -z $DIRTY_OK ]]; then 24 | echo "Error: dirty tree" 25 | exit 1 26 | fi 27 | 28 | declare tomls=() 29 | while IFS='' read -r line; do tomls+=("$line"); done < <(find . -name Cargo.toml) 30 | 31 | crates=( 32 | solana-clap-utils 33 | solana-cli-config 34 | solana-client 35 | solana-logger 36 | solana-program 37 | solana-program-test 38 | solana-remote-wallet 39 | solana-sdk 40 | solana-validator 41 | ) 42 | 43 | set -x 44 | for crate in "${crates[@]}"; do 45 | sed -i -e "s#\(${crate} = \"\).*\(\"\)#\1$solana_ver\2#g" "${tomls[@]}" 46 | done 47 | 48 | -------------------------------------------------------------------------------- /src/engine.rs: -------------------------------------------------------------------------------- 1 | pub mod cancel_subscription; 2 | pub mod common; 3 | pub mod constants; 4 | pub mod json; 5 | pub mod register; 6 | pub mod renew; 7 | pub mod subscribe; 8 | pub mod withdraw; 9 | pub mod pay; -------------------------------------------------------------------------------- /src/engine/cancel_subscription.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | engine::common::{subscribe_checks, transfer_sol}, 3 | engine::constants::PDA_SEED, 4 | error::PaymentProcessorError, 5 | state::{ 6 | Discriminator, IsClosed, OrderAccount, OrderStatus, Serdes, SubscriptionAccount, 7 | SubscriptionStatus, 8 | }, 9 | }; 10 | use solana_program::{ 11 | account_info::{next_account_info, AccountInfo}, 12 | entrypoint::ProgramResult, 13 | msg, 14 | program::invoke_signed, 15 | program_error::ProgramError, 16 | program_pack::IsInitialized, 17 | pubkey::Pubkey, 18 | sysvar::{clock::Clock, Sysvar}, 19 | }; 20 | use spl_token::{self}; 21 | 22 | /// Cancel Subscription 23 | /// currently only works well for subscriptions still in the trial period 24 | pub fn process_cancel_subscription(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { 25 | let account_info_iter = &mut accounts.iter(); 26 | 27 | let signer_info = next_account_info(account_info_iter)?; 28 | let subscription_info = next_account_info(account_info_iter)?; 29 | let merchant_info = next_account_info(account_info_iter)?; 30 | let order_info = next_account_info(account_info_iter)?; 31 | let order_token_info = next_account_info(account_info_iter)?; 32 | let refund_token_info = next_account_info(account_info_iter)?; 33 | let account_to_receive_sol_refund_info = next_account_info(account_info_iter)?; 34 | let pda_info = next_account_info(account_info_iter)?; 35 | let token_program_info = next_account_info(account_info_iter)?; 36 | 37 | let timestamp = Clock::get()?.unix_timestamp; 38 | 39 | // ensure signer can sign 40 | if !signer_info.is_signer { 41 | return Err(ProgramError::MissingRequiredSignature); 42 | } 43 | // ensure subscription account is owned by this program 44 | if *subscription_info.owner != *program_id { 45 | msg!("Error: Wrong owner for subscription account"); 46 | return Err(ProgramError::IncorrectProgramId); 47 | } 48 | // ensure token accounts are owned by token program 49 | if *order_token_info.owner != spl_token::id() { 50 | msg!("Error: Order token account must be owned by token program"); 51 | return Err(ProgramError::IncorrectProgramId); 52 | } 53 | if *refund_token_info.owner != spl_token::id() { 54 | msg!("Error: Refund token account must be owned by token program"); 55 | return Err(ProgramError::IncorrectProgramId); 56 | } 57 | // check that provided pda is correct 58 | let (pda, pda_nonce) = Pubkey::find_program_address(&[PDA_SEED], &program_id); 59 | if pda_info.key != &pda { 60 | return Err(ProgramError::InvalidSeeds); 61 | } 62 | 63 | // get the subscription account 64 | let mut subscription_account = SubscriptionAccount::unpack(&subscription_info.data.borrow())?; 65 | if !subscription_account.is_initialized() { 66 | return Err(ProgramError::UninitializedAccount); 67 | } 68 | if subscription_account.is_closed() { 69 | return Err(PaymentProcessorError::ClosedAccount.into()); 70 | } 71 | if subscription_account.discriminator != Discriminator::Subscription as u8 { 72 | msg!("Error: Invalid subscription account"); 73 | return Err(ProgramError::InvalidAccountData); 74 | } 75 | let (mut order_account, package) = subscribe_checks( 76 | program_id, 77 | signer_info, 78 | merchant_info, 79 | order_info, 80 | subscription_info, 81 | &subscription_account.name, 82 | )?; 83 | 84 | // ensure the order payment token account is the right one 85 | if order_token_info.key.to_bytes() != order_account.token { 86 | msg!("Error: Incorrect order token account"); 87 | return Err(ProgramError::InvalidAccountData); 88 | } 89 | // ensure the signer is the order payer 90 | if signer_info.key.to_bytes() != order_account.payer { 91 | msg!("Error: One can only cancel their own subscription payment"); 92 | return Err(ProgramError::InvalidAccountData); 93 | } 94 | 95 | // get the trial period duration 96 | let trial_duration: i64 = match package.trial { 97 | None => 0, 98 | Some(value) => value, 99 | }; 100 | // don't allow cancellation if trial period ended 101 | if timestamp >= (subscription_account.joined + trial_duration) { 102 | msg!("Info: Subscription amount not refunded because trial period has ended."); 103 | } else { 104 | // Transferring payment back to the payer... 105 | invoke_signed( 106 | &spl_token::instruction::transfer( 107 | token_program_info.key, 108 | order_token_info.key, 109 | refund_token_info.key, 110 | &pda, 111 | &[&pda], 112 | order_account.paid_amount, 113 | ) 114 | .unwrap(), 115 | &[ 116 | token_program_info.clone(), 117 | pda_info.clone(), 118 | order_token_info.clone(), 119 | refund_token_info.clone(), 120 | ], 121 | &[&[&PDA_SEED, &[pda_nonce]]], 122 | )?; 123 | // Close the order token account since it will never be needed again 124 | invoke_signed( 125 | &spl_token::instruction::close_account( 126 | token_program_info.key, 127 | order_token_info.key, 128 | account_to_receive_sol_refund_info.key, 129 | &pda, 130 | &[&pda], 131 | ) 132 | .unwrap(), 133 | &[ 134 | token_program_info.clone(), 135 | order_token_info.clone(), 136 | account_to_receive_sol_refund_info.clone(), 137 | pda_info.clone(), 138 | ], 139 | &[&[&PDA_SEED, &[pda_nonce]]], 140 | )?; 141 | // mark order account as closed 142 | order_account.discriminator = Discriminator::Closed as u8; 143 | // Transfer all the sol from the order account to the sol_destination. 144 | transfer_sol( 145 | order_info.clone(), 146 | account_to_receive_sol_refund_info.clone(), 147 | order_info.lamports(), 148 | )?; 149 | // Updating order account information... 150 | order_account.status = OrderStatus::Cancelled as u8; 151 | order_account.modified = timestamp; 152 | OrderAccount::pack(&order_account, &mut order_info.data.borrow_mut()); 153 | // set period end to right now 154 | subscription_account.period_end = timestamp; 155 | } 156 | 157 | // Updating subscription account information... 158 | subscription_account.status = SubscriptionStatus::Cancelled as u8; 159 | SubscriptionAccount::pack( 160 | &subscription_account, 161 | &mut subscription_info.data.borrow_mut(), 162 | ); 163 | 164 | Ok(()) 165 | } 166 | -------------------------------------------------------------------------------- /src/engine/common.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | engine::json::{OrderSubscription, Package, Packages}, 3 | error::PaymentProcessorError, 4 | state::{Discriminator, IsClosed, MerchantAccount, OrderAccount, OrderStatus, Serdes}, 5 | }; 6 | use serde_json::Error as JSONError; 7 | use solana_program::program_pack::Pack; 8 | use solana_program::{ 9 | account_info::AccountInfo, 10 | entrypoint::ProgramResult, 11 | msg, 12 | program::{invoke, invoke_signed}, 13 | program_error::ProgramError, 14 | program_pack::IsInitialized, 15 | pubkey::Pubkey, 16 | system_instruction, 17 | sysvar::rent::Rent, 18 | }; 19 | 20 | /// ensure the order is for the subscription 21 | pub fn verify_subscription_order( 22 | subscription_info: &AccountInfo<'_>, 23 | order_account: &OrderAccount, 24 | ) -> ProgramResult { 25 | let order_json_data: Result = 26 | serde_json::from_str(&order_account.data); 27 | let expected_subscription = match order_json_data { 28 | Err(_error) => return Err(PaymentProcessorError::InvalidSubscriptionData.into()), 29 | Ok(data) => data.subscription, 30 | }; 31 | if expected_subscription != subscription_info.key.to_string() { 32 | return Err(PaymentProcessorError::WrongOrderAccount.into()); 33 | } 34 | Ok(()) 35 | } 36 | 37 | /// Get subscription package 38 | pub fn get_subscription_package( 39 | subscription_package_name: &str, 40 | merchant_account: &MerchantAccount, 41 | ) -> Result { 42 | // ensure the merchant has a subscription by this name 43 | let merchant_json_data: Result = 44 | serde_json::from_str(&merchant_account.data); 45 | let packages = match merchant_json_data { 46 | Err(_error) => return Err(PaymentProcessorError::InvalidSubscriptionData.into()), 47 | Ok(data) => data.packages, 48 | }; 49 | // NB: if the are duplicates, take the first one --> verified in a test 50 | let package = packages 51 | .into_iter() 52 | .find(|package| package.name == subscription_package_name); 53 | match package { 54 | None => return Err(PaymentProcessorError::InvalidSubscriptionPackage.into()), 55 | Some(value) => Ok(value), 56 | } 57 | } 58 | 59 | /// run checks for subscription processing 60 | pub fn subscribe_checks( 61 | program_id: &Pubkey, 62 | signer_info: &AccountInfo<'_>, 63 | merchant_info: &AccountInfo<'_>, 64 | order_info: &AccountInfo<'_>, 65 | subscription_info: &AccountInfo<'_>, 66 | subscription_name: &str, 67 | ) -> Result<(OrderAccount, Package), ProgramError> { 68 | // ensure signer can sign 69 | if !signer_info.is_signer { 70 | return Err(ProgramError::MissingRequiredSignature); 71 | } 72 | // ensure merchant & order accounts are owned by this program 73 | if *merchant_info.owner != *program_id { 74 | msg!("Error: Wrong owner for merchant account"); 75 | return Err(ProgramError::IncorrectProgramId); 76 | } 77 | if *order_info.owner != *program_id { 78 | msg!("Error: Wrong owner for order account"); 79 | return Err(ProgramError::IncorrectProgramId); 80 | } 81 | // get the merchant account 82 | let merchant_account = MerchantAccount::unpack(&merchant_info.data.borrow())?; 83 | if merchant_account.is_closed() { 84 | return Err(PaymentProcessorError::ClosedAccount.into()); 85 | } 86 | if !merchant_account.is_initialized() { 87 | return Err(ProgramError::UninitializedAccount); 88 | } 89 | let allowed_merchant_account_types = vec![ 90 | Discriminator::MerchantSubscription as u8, 91 | Discriminator::MerchantSubscriptionWithTrial as u8, 92 | ]; 93 | if !allowed_merchant_account_types.contains(&merchant_account.discriminator) { 94 | msg!("Error: Invalid merchant account"); 95 | return Err(ProgramError::InvalidAccountData); 96 | } 97 | // get the order account 98 | let order_account = OrderAccount::unpack(&order_info.data.borrow())?; 99 | if order_account.is_closed() { 100 | return Err(PaymentProcessorError::ClosedAccount.into()); 101 | } 102 | if !order_account.is_initialized() { 103 | return Err(ProgramError::UninitializedAccount); 104 | } 105 | if order_account.discriminator != Discriminator::OrderExpressCheckout as u8 { 106 | msg!("Error: Invalid order account"); 107 | return Err(ProgramError::InvalidAccountData); 108 | } 109 | // ensure this order is for this subscription 110 | verify_subscription_order(subscription_info, &order_account)?; 111 | // ensure we have the right payer 112 | if signer_info.key.to_bytes() != order_account.payer { 113 | return Err(PaymentProcessorError::WrongPayer.into()); 114 | } 115 | // ensure order account is paid 116 | if order_account.status != (OrderStatus::Paid as u8) { 117 | return Err(PaymentProcessorError::NotPaid.into()); 118 | } 119 | // ensure the order account belongs to this merchant 120 | if merchant_info.key.to_bytes() != order_account.merchant { 121 | return Err(ProgramError::InvalidAccountData); 122 | } 123 | // get the package 124 | let package = get_subscription_package(subscription_name, &merchant_account)?; 125 | if package.mint != Pubkey::new_from_array(order_account.mint).to_string() { 126 | return Err(PaymentProcessorError::WrongMint.into()); 127 | } 128 | Ok((order_account, package)) 129 | } 130 | 131 | /// Create associated token account 132 | /// 133 | /// Creates an associated token account that is owned by a custom program. 134 | /// This is similar to spl_associated_token_account::create_associated_token_account 135 | /// which would fail for creating token accounts not owned by the token program 136 | pub fn create_program_owned_associated_token_account( 137 | program_id: &Pubkey, 138 | accounts: &[AccountInfo; 8], 139 | rent: &Rent, 140 | ) -> ProgramResult { 141 | let signer_info = &accounts[0]; 142 | let base_account_info = &accounts[1]; 143 | let new_account_info = &accounts[2]; 144 | let mint_info = &accounts[3]; 145 | let pda_info = &accounts[4]; 146 | let token_program_info = &accounts[5]; 147 | let system_program_info = &accounts[6]; 148 | let rent_sysvar_info = &accounts[7]; 149 | 150 | let (associated_token_address, bump_seed) = Pubkey::find_program_address( 151 | &[ 152 | &base_account_info.key.to_bytes(), 153 | &spl_token::id().to_bytes(), 154 | &mint_info.key.to_bytes(), 155 | ], 156 | program_id, 157 | ); 158 | // assert that the derived address matches the one supplied 159 | if associated_token_address != *new_account_info.key { 160 | msg!("Error: Associated address does not match seed derivation"); 161 | return Err(ProgramError::InvalidSeeds); 162 | } 163 | // get signer seeds 164 | let associated_token_account_signer_seeds: &[&[_]] = &[ 165 | &base_account_info.key.to_bytes(), 166 | &spl_token::id().to_bytes(), 167 | &mint_info.key.to_bytes(), 168 | &[bump_seed], 169 | ]; 170 | // Fund the associated seller token account with the minimum balance to be rent exempt 171 | let required_lamports = rent 172 | .minimum_balance(spl_token::state::Account::LEN) 173 | .max(1) 174 | .saturating_sub(new_account_info.lamports()); 175 | if required_lamports > 0 { 176 | // Transfer lamports to the associated seller token account 177 | invoke( 178 | &system_instruction::transfer( 179 | &signer_info.key, 180 | new_account_info.key, 181 | required_lamports, 182 | ), 183 | &[ 184 | signer_info.clone(), 185 | new_account_info.clone(), 186 | system_program_info.clone(), 187 | ], 188 | )?; 189 | } 190 | // Allocate space for the associated seller token account 191 | invoke_signed( 192 | &system_instruction::allocate(new_account_info.key, spl_token::state::Account::LEN as u64), 193 | &[new_account_info.clone(), system_program_info.clone()], 194 | &[&associated_token_account_signer_seeds], 195 | )?; 196 | // Assign the associated seller token account to the SPL Token program 197 | invoke_signed( 198 | &system_instruction::assign(new_account_info.key, &spl_token::id()), 199 | &[new_account_info.clone(), system_program_info.clone()], 200 | &[&associated_token_account_signer_seeds], 201 | )?; 202 | // Initialize the associated seller token account 203 | invoke( 204 | &spl_token::instruction::initialize_account( 205 | &spl_token::id(), 206 | new_account_info.key, 207 | mint_info.key, 208 | pda_info.key, 209 | )?, 210 | &[ 211 | new_account_info.clone(), 212 | mint_info.clone(), 213 | pda_info.clone(), 214 | rent_sysvar_info.clone(), 215 | token_program_info.clone(), 216 | ], 217 | )?; 218 | 219 | Ok(()) 220 | } 221 | 222 | /// Transfer SOL from one account to another 223 | /// Used for accounts not owned by the system program 224 | pub fn transfer_sol( 225 | sol_origin_info: AccountInfo, 226 | sol_destination_info: AccountInfo, 227 | amount: u64, 228 | ) -> ProgramResult { 229 | // Transfer tokens from the account to the sol_destination. 230 | let dest_starting_lamports = sol_destination_info.lamports(); 231 | let origin_starting_lamports = sol_origin_info.lamports(); 232 | 233 | **sol_destination_info.lamports.borrow_mut() = 234 | dest_starting_lamports.checked_add(amount).unwrap(); 235 | **sol_origin_info.lamports.borrow_mut() = origin_starting_lamports.checked_sub(amount).unwrap(); 236 | Ok(()) 237 | } 238 | -------------------------------------------------------------------------------- /src/engine/constants.rs: -------------------------------------------------------------------------------- 1 | /// the word merchant as a string 2 | pub const MERCHANT: &str = "merchant"; 3 | /// the word trial as a string 4 | pub const TRIAL: &str = "trial"; 5 | /// the word packages as a string 6 | pub const PACKAGES: &str = "packages"; 7 | /// the word packages as a string 8 | pub const PAID: &str = "_paid"; 9 | /// the word packages as a string 10 | pub const INITIAL: &str = "_initial"; 11 | /// seed for pgram derived addresses 12 | pub const PDA_SEED: &[u8] = b"sol_payment_processor"; 13 | /// the program owner 14 | pub const PROGRAM_OWNER: &str = "mosh782eoKyPca9eotWfepHVSKavjDMBjNkNE3Gge6Z"; 15 | /// minimum transaction fee percentage 16 | pub const MIN_FEE_IN_LAMPORTS: u64 = 50000; 17 | /// default transaction fee percentage 18 | pub const DEFAULT_FEE_IN_LAMPORTS: u64 = 500000; 19 | /// sponsor fee percentage 20 | pub const SPONSOR_FEE: u128 = 3; 21 | /// default data value 22 | pub const DEFAULT_DATA: &str = "{}"; 23 | // these are purely by trial and error ... TODO: understand these some more 24 | /// the mem size of string ... apparently 25 | pub const STRING_SIZE: usize = 4; 26 | -------------------------------------------------------------------------------- /src/engine/json.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::BTreeMap; 3 | 4 | #[derive(Serialize, Debug, Deserialize, PartialEq)] 5 | /// Subscription package 6 | pub struct Package { 7 | pub name: String, 8 | /// duration of the trial period in seconds 9 | pub trial: Option, 10 | /// duration of the subscription in seconds 11 | pub duration: i64, 12 | /// the price in full for this subscription option 13 | /// e.g. if the duration is 1 hour (3600) then the price is per hour 14 | /// e.g. if the duration is 1 month (3600 * 24 * 30) then the price is per month 15 | pub price: u64, 16 | /// the mint (currency) used for this package 17 | pub mint: String, 18 | } 19 | 20 | #[derive(Serialize, Debug, Deserialize, PartialEq)] 21 | /// Subscription packages 22 | pub struct Packages { 23 | pub packages: Vec, 24 | } 25 | 26 | #[derive(Serialize, Debug, Deserialize, PartialEq)] 27 | /// Used in order account data field to tie the order to a subscription 28 | pub struct OrderSubscription { 29 | pub subscription: String, 30 | } 31 | 32 | #[derive(Serialize, Debug, Deserialize, PartialEq)] 33 | /// Item 34 | /// 35 | /// Represents an ...item for which a payment can be made 36 | pub struct Item { 37 | pub price: u64, 38 | /// the mint (currency) used for this package 39 | pub mint: String, 40 | } 41 | 42 | pub type OrderItems = BTreeMap; -------------------------------------------------------------------------------- /src/engine/pay.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | engine::{ 3 | common::create_program_owned_associated_token_account, 4 | constants::{DEFAULT_DATA, INITIAL, PAID, PROGRAM_OWNER, SPONSOR_FEE}, 5 | json::{Item, OrderItems}, 6 | }, 7 | error::PaymentProcessorError, 8 | state::{Discriminator, IsClosed, MerchantAccount, OrderAccount, OrderStatus, Serdes}, 9 | utils::{get_amounts, get_order_account_size}, 10 | }; 11 | use serde_json::{json, Error as JSONError, Value}; 12 | use solana_program::program_pack::Pack; 13 | use solana_program::{ 14 | account_info::{next_account_info, AccountInfo}, 15 | entrypoint::ProgramResult, 16 | msg, 17 | program::invoke, 18 | program_error::ProgramError, 19 | program_pack::IsInitialized, 20 | pubkey::Pubkey, 21 | system_instruction, 22 | sysvar::{clock::Clock, rent::Rent, Sysvar}, 23 | }; 24 | use spl_token::{self, state::Account as TokenAccount}; 25 | use std::collections::BTreeMap; 26 | use std::str::FromStr; 27 | 28 | /// Run checks for order processing 29 | pub fn order_checks( 30 | program_id: &Pubkey, 31 | signer_info: &AccountInfo<'_>, 32 | merchant_info: &AccountInfo<'_>, 33 | buyer_token_info: &AccountInfo<'_>, 34 | mint_info: &AccountInfo<'_>, 35 | program_owner_info: &AccountInfo<'_>, 36 | sponsor_info: &AccountInfo<'_>, 37 | ) -> Result { 38 | // ensure signer can sign 39 | if !signer_info.is_signer { 40 | return Err(ProgramError::MissingRequiredSignature); 41 | } 42 | // ensure merchant account is owned by this program 43 | if *merchant_info.owner != *program_id { 44 | msg!("Error: Wrong owner for merchant account"); 45 | return Err(ProgramError::IncorrectProgramId); 46 | } 47 | // get the merchant account 48 | let merchant_account = MerchantAccount::unpack(&merchant_info.data.borrow())?; 49 | if merchant_account.is_closed() { 50 | return Err(PaymentProcessorError::ClosedAccount.into()); 51 | } 52 | if !merchant_account.is_initialized() { 53 | return Err(ProgramError::UninitializedAccount); 54 | } 55 | // ensure buyer token account is owned by token program 56 | if *buyer_token_info.owner != spl_token::id() { 57 | msg!("Error: Buyer token account not owned by Token Program"); 58 | return Err(ProgramError::IncorrectProgramId); 59 | } 60 | // Get mint details and verify that they match token account 61 | let buyer_token_data = TokenAccount::unpack(&buyer_token_info.data.borrow())?; 62 | if *mint_info.key != buyer_token_data.mint { 63 | return Err(PaymentProcessorError::MintNotEqual.into()); 64 | } 65 | // check that provided program owner is correct 66 | if *program_owner_info.key != Pubkey::from_str(PROGRAM_OWNER).unwrap() { 67 | return Err(PaymentProcessorError::WrongProgramOwner.into()); 68 | } 69 | // check that the provided sponsor is correct 70 | if *sponsor_info.key != Pubkey::new_from_array(merchant_account.sponsor) { 71 | msg!("Error: Sponsor account is incorrect"); 72 | return Err(PaymentProcessorError::WrongSponsor.into()); 73 | } 74 | 75 | Ok(merchant_account) 76 | } 77 | 78 | /// Verify chain checkout 79 | /// 80 | /// Mainly ensure that the item(s) being paid for match the item(s) in the 81 | /// merchant account and that the amount being paid is sufficient. 82 | /// 83 | /// order_items is an object that looks like so: 84 | /// { 85 | /// id: quantity 86 | /// } 87 | /// e.g. {"item1", 1, "item2": 33} 88 | pub fn chain_checkout_checks( 89 | merchant_account: &MerchantAccount, 90 | mint: &AccountInfo, 91 | order_items: &OrderItems, 92 | amount: u64, 93 | ) -> ProgramResult { 94 | if merchant_account.discriminator != Discriminator::MerchantChainCheckout as u8 { 95 | msg!("Error: Invalid merchant account"); 96 | return Err(PaymentProcessorError::InvalidMerchantData.into()); 97 | } 98 | 99 | let merchant_json_data: Result, JSONError> = 100 | serde_json::from_str(&merchant_account.data); 101 | 102 | let registered_items = match merchant_json_data { 103 | Err(_error) => return Err(PaymentProcessorError::InvalidMerchantData.into()), 104 | Ok(data) => data, 105 | }; 106 | 107 | let mut total_amount: u64 = 0; 108 | 109 | for (key, quantity) in order_items.iter() { 110 | let registered_item = match registered_items.get(key) { 111 | None => { 112 | msg!("Error: Invalid order item {:?}", key); 113 | return Err(PaymentProcessorError::InvalidOrderData.into()); 114 | } 115 | Some(value) => value, 116 | }; 117 | if registered_item.mint != mint.key.to_string() { 118 | msg!( 119 | "Error: Mint {:?} invalid for this order", 120 | mint.key.to_string() 121 | ); 122 | return Err(PaymentProcessorError::WrongMint.into()); 123 | } 124 | 125 | total_amount = total_amount + (registered_item.price * quantity); 126 | } 127 | 128 | if total_amount > amount { 129 | msg!("Error: Insufficient amount, should be {:?}", total_amount); 130 | return Err(ProgramError::InsufficientFunds); 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | /// process an order payment 137 | pub fn process_order( 138 | program_id: &Pubkey, 139 | accounts: &[AccountInfo], 140 | amount: u64, 141 | order_id: String, 142 | secret: String, 143 | maybe_data: Option, 144 | checkout_items: Option, 145 | ) -> ProgramResult { 146 | let account_info_iter = &mut accounts.iter(); 147 | 148 | let signer_info = next_account_info(account_info_iter)?; 149 | let order_info = next_account_info(account_info_iter)?; 150 | let merchant_info = next_account_info(account_info_iter)?; 151 | let seller_token_info = next_account_info(account_info_iter)?; 152 | let buyer_token_info = next_account_info(account_info_iter)?; 153 | let program_owner_info = next_account_info(account_info_iter)?; 154 | let sponsor_info = next_account_info(account_info_iter)?; 155 | let mint_info = next_account_info(account_info_iter)?; 156 | let pda_info = next_account_info(account_info_iter)?; 157 | let token_program_info = next_account_info(account_info_iter)?; 158 | let system_program_info = next_account_info(account_info_iter)?; 159 | let rent_sysvar_info = next_account_info(account_info_iter)?; 160 | 161 | let rent = &Rent::from_account_info(rent_sysvar_info)?; 162 | let timestamp = Clock::get()?.unix_timestamp; 163 | 164 | let merchant_account = order_checks( 165 | program_id, 166 | signer_info, 167 | merchant_info, 168 | buyer_token_info, 169 | mint_info, 170 | program_owner_info, 171 | sponsor_info, 172 | )?; 173 | 174 | // get data 175 | let mut data = match maybe_data { 176 | None => String::from(DEFAULT_DATA), 177 | Some(value) => value, 178 | }; 179 | 180 | let mut order_account_type = Discriminator::OrderExpressCheckout as u8; 181 | 182 | // process chain checkout 183 | if checkout_items.is_some() { 184 | order_account_type = Discriminator::OrderChainCheckout as u8; 185 | let order_items = checkout_items.unwrap(); 186 | chain_checkout_checks(&merchant_account, &mint_info.clone(), &order_items, amount)?; 187 | if data == String::from(DEFAULT_DATA) { 188 | data = json!({ PAID: order_items }).to_string(); 189 | } else { 190 | // let possible_json_data: Result, JSONError> = serde_json::from_str(&data); 191 | // let json_data = match possible_json_data { 192 | let json_data: Value = match serde_json::from_str(&data) { 193 | Err(_error) => return Err(PaymentProcessorError::InvalidOrderData.into()), 194 | Ok(data) => data, 195 | }; 196 | data = json!({ 197 | INITIAL: json_data, 198 | PAID: order_items 199 | }) 200 | .to_string(); 201 | } 202 | } 203 | 204 | // create order account 205 | let order_account_size = get_order_account_size(&order_id, &secret, &data); 206 | // the order account amount includes the fee in SOL 207 | let order_account_amount = Rent::default().minimum_balance(order_account_size); 208 | invoke( 209 | &system_instruction::create_account( 210 | signer_info.key, 211 | order_info.key, 212 | order_account_amount, 213 | order_account_size as u64, 214 | program_id, 215 | ), 216 | &[ 217 | signer_info.clone(), 218 | order_info.clone(), 219 | system_program_info.clone(), 220 | ], 221 | )?; 222 | 223 | // next we are going to try and create a token account owned by the program 224 | // but whose address is derived from the order account 225 | // TODO: for subscriptions, should this use the subscription account as the base? 226 | create_program_owned_associated_token_account( 227 | program_id, 228 | &[ 229 | signer_info.clone(), 230 | order_info.clone(), 231 | seller_token_info.clone(), 232 | mint_info.clone(), 233 | pda_info.clone(), 234 | token_program_info.clone(), 235 | system_program_info.clone(), 236 | rent_sysvar_info.clone(), 237 | ], 238 | rent, 239 | )?; 240 | 241 | // Transfer payment amount to associated seller token account... 242 | invoke( 243 | &spl_token::instruction::transfer( 244 | token_program_info.key, 245 | buyer_token_info.key, 246 | seller_token_info.key, 247 | signer_info.key, 248 | &[&signer_info.key], 249 | amount, 250 | ) 251 | .unwrap(), 252 | &[ 253 | buyer_token_info.clone(), 254 | seller_token_info.clone(), 255 | signer_info.clone(), 256 | token_program_info.clone(), 257 | ], 258 | )?; 259 | 260 | if Pubkey::new_from_array(merchant_account.sponsor) == Pubkey::from_str(PROGRAM_OWNER).unwrap() 261 | { 262 | // Transferring processing fee to the program owner... 263 | invoke( 264 | &system_instruction::transfer( 265 | &signer_info.key, 266 | program_owner_info.key, 267 | merchant_account.fee, 268 | ), 269 | &[ 270 | signer_info.clone(), 271 | program_owner_info.clone(), 272 | system_program_info.clone(), 273 | ], 274 | )?; 275 | } else { 276 | // we need to pay both the program owner and the sponsor 277 | let (program_owner_fee, sponsor_fee) = get_amounts(merchant_account.fee, SPONSOR_FEE); 278 | // Transferring processing fee to the program owner and sponsor... 279 | invoke( 280 | &system_instruction::transfer( 281 | &signer_info.key, 282 | program_owner_info.key, 283 | program_owner_fee, 284 | ), 285 | &[ 286 | signer_info.clone(), 287 | program_owner_info.clone(), 288 | system_program_info.clone(), 289 | ], 290 | )?; 291 | invoke( 292 | &system_instruction::transfer(&signer_info.key, sponsor_info.key, sponsor_fee), 293 | &[ 294 | signer_info.clone(), 295 | sponsor_info.clone(), 296 | system_program_info.clone(), 297 | ], 298 | )?; 299 | } 300 | 301 | // get the order account 302 | // TODO: ensure this account is not already initialized 303 | let mut order_account_data = order_info.try_borrow_mut_data()?; 304 | // Saving order information... 305 | let order = OrderAccount { 306 | discriminator: order_account_type, 307 | status: OrderStatus::Paid as u8, 308 | created: timestamp, 309 | modified: timestamp, 310 | merchant: merchant_info.key.to_bytes(), 311 | mint: mint_info.key.to_bytes(), 312 | token: seller_token_info.key.to_bytes(), 313 | payer: signer_info.key.to_bytes(), 314 | expected_amount: amount, 315 | paid_amount: amount, 316 | order_id, 317 | secret, 318 | data, 319 | }; 320 | 321 | order.pack(&mut order_account_data); 322 | 323 | // ensure order account is rent exempt 324 | if !rent.is_exempt(order_info.lamports(), order_account_size) { 325 | return Err(ProgramError::AccountNotRentExempt); 326 | } 327 | 328 | Ok(()) 329 | } 330 | 331 | pub fn process_express_checkout( 332 | program_id: &Pubkey, 333 | accounts: &[AccountInfo], 334 | amount: u64, 335 | order_id: String, 336 | secret: String, 337 | maybe_data: Option, 338 | ) -> ProgramResult { 339 | process_order( 340 | program_id, 341 | accounts, 342 | amount, 343 | order_id, 344 | secret, 345 | maybe_data, 346 | Option::None, 347 | )?; 348 | Ok(()) 349 | } 350 | 351 | pub fn process_chain_checkout( 352 | program_id: &Pubkey, 353 | accounts: &[AccountInfo], 354 | amount: u64, 355 | order_items: OrderItems, 356 | maybe_data: Option, 357 | ) -> ProgramResult { 358 | process_order( 359 | program_id, 360 | accounts, 361 | amount, 362 | format!("{timestamp}", timestamp = Clock::get()?.unix_timestamp), 363 | "".to_string(), 364 | maybe_data, 365 | Some(order_items), 366 | )?; 367 | Ok(()) 368 | } 369 | -------------------------------------------------------------------------------- /src/engine/register.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | engine::constants::{ 3 | DEFAULT_DATA, DEFAULT_FEE_IN_LAMPORTS, MERCHANT, MIN_FEE_IN_LAMPORTS, PROGRAM_OWNER, TRIAL, 4 | }, 5 | engine::json::{Item, Packages}, 6 | state::{Discriminator, MerchantAccount, Serdes}, 7 | utils::get_merchant_account_size, 8 | }; 9 | use serde_json::Error as JSONError; 10 | use solana_program::{ 11 | account_info::{next_account_info, AccountInfo}, 12 | entrypoint::ProgramResult, 13 | msg, 14 | program::invoke, 15 | program_error::ProgramError, 16 | pubkey::Pubkey, 17 | system_instruction, 18 | sysvar::{rent::Rent, Sysvar}, 19 | }; 20 | use std::collections::BTreeMap; 21 | use std::str::FromStr; 22 | 23 | pub fn process_register_merchant( 24 | program_id: &Pubkey, 25 | accounts: &[AccountInfo], 26 | seed: Option, 27 | maybe_fee: Option, 28 | maybe_data: Option, 29 | ) -> ProgramResult { 30 | let account_info_iter = &mut accounts.iter(); 31 | 32 | let signer_info = next_account_info(account_info_iter)?; 33 | let merchant_info = next_account_info(account_info_iter)?; 34 | let system_sysvar_info = next_account_info(account_info_iter)?; 35 | let rent_sysvar_info = next_account_info(account_info_iter)?; 36 | let possible_sponsor_info = next_account_info(account_info_iter); 37 | let rent = &Rent::from_account_info(rent_sysvar_info)?; 38 | 39 | // ensure signer can sign 40 | if !signer_info.is_signer { 41 | return Err(ProgramError::MissingRequiredSignature); 42 | } 43 | 44 | let data = match maybe_data { 45 | None => String::from(DEFAULT_DATA), 46 | Some(value) => value, 47 | }; 48 | let account_size = get_merchant_account_size(&data); 49 | 50 | // Creating merchant account on chain... 51 | invoke( 52 | &system_instruction::create_account_with_seed( 53 | signer_info.key, 54 | merchant_info.key, 55 | signer_info.key, 56 | match &seed { 57 | None => MERCHANT, 58 | Some(value) => &value, 59 | }, 60 | Rent::default().minimum_balance(account_size), 61 | account_size as u64, 62 | program_id, 63 | ), 64 | &[ 65 | signer_info.clone(), 66 | merchant_info.clone(), 67 | signer_info.clone(), 68 | system_sysvar_info.clone(), 69 | ], 70 | )?; 71 | 72 | // get merchant account type 73 | let maybe_subscription_merchant: Result = serde_json::from_str(&data); 74 | let merchant_account_type: u8 = match maybe_subscription_merchant { 75 | Ok(_value) => { 76 | if data.contains(TRIAL) { 77 | Discriminator::MerchantSubscriptionWithTrial as u8 78 | } else { 79 | Discriminator::MerchantSubscription as u8 80 | } 81 | } 82 | Err(_error) => { 83 | let maybe_chain_checkout: Result, JSONError> = 84 | serde_json::from_str(&data); 85 | match maybe_chain_checkout { 86 | Ok(_value) => Discriminator::MerchantChainCheckout as u8, 87 | Err(_error) => Discriminator::Merchant as u8, 88 | } 89 | } 90 | }; 91 | 92 | // get the merchant account data 93 | // TODO: ensure this account is not already initialized 94 | let mut merchant_account_data = merchant_info.try_borrow_mut_data()?; 95 | // save it 96 | let merchant = MerchantAccount { 97 | discriminator: merchant_account_type, 98 | owner: signer_info.key.to_bytes(), 99 | sponsor: match possible_sponsor_info { 100 | Ok(sponsor_info) => sponsor_info.key.to_bytes(), 101 | Err(_error) => Pubkey::from_str(PROGRAM_OWNER).unwrap().to_bytes(), 102 | }, 103 | fee: match maybe_fee { 104 | None => DEFAULT_FEE_IN_LAMPORTS, 105 | Some(value) => { 106 | let mut result = value; 107 | if result < MIN_FEE_IN_LAMPORTS { 108 | msg!( 109 | "Info: setting minimum transaction fee of {:?}", 110 | MIN_FEE_IN_LAMPORTS 111 | ); 112 | result = MIN_FEE_IN_LAMPORTS; 113 | } 114 | result 115 | } 116 | }, 117 | data, 118 | }; 119 | 120 | merchant.pack(&mut merchant_account_data); 121 | 122 | // ensure merchant account is rent exempt 123 | if !rent.is_exempt(merchant_info.lamports(), account_size) { 124 | return Err(ProgramError::AccountNotRentExempt); 125 | } 126 | 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /src/engine/renew.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::common::subscribe_checks; 2 | use crate::error::PaymentProcessorError; 3 | use crate::state::{Discriminator, IsClosed, Serdes, SubscriptionAccount, SubscriptionStatus}; 4 | use solana_program::{ 5 | account_info::{next_account_info, AccountInfo}, 6 | entrypoint::ProgramResult, 7 | msg, 8 | program_error::ProgramError, 9 | program_pack::IsInitialized, 10 | pubkey::Pubkey, 11 | sysvar::{clock::Clock, Sysvar}, 12 | }; 13 | 14 | pub fn process_renew_subscription( 15 | program_id: &Pubkey, 16 | accounts: &[AccountInfo], 17 | quantity: i64, 18 | ) -> ProgramResult { 19 | let account_info_iter = &mut accounts.iter(); 20 | 21 | let signer_info = next_account_info(account_info_iter)?; 22 | let subscription_info = next_account_info(account_info_iter)?; 23 | let merchant_info = next_account_info(account_info_iter)?; 24 | let order_info = next_account_info(account_info_iter)?; 25 | 26 | // ensure subscription account is owned by this program 27 | if *subscription_info.owner != *program_id { 28 | msg!("Error: Wrong owner for subscription account"); 29 | return Err(ProgramError::IncorrectProgramId); 30 | } 31 | // get the subscription account 32 | let mut subscription_account = SubscriptionAccount::unpack(&subscription_info.data.borrow())?; 33 | if !subscription_account.is_initialized() { 34 | return Err(ProgramError::UninitializedAccount); 35 | } 36 | if subscription_account.is_closed() { 37 | return Err(PaymentProcessorError::ClosedAccount.into()); 38 | } 39 | if subscription_account.discriminator != Discriminator::Subscription as u8 { 40 | msg!("Error: Invalid subscription account"); 41 | return Err(ProgramError::InvalidAccountData); 42 | } 43 | let (order_account, package) = subscribe_checks( 44 | program_id, 45 | signer_info, 46 | merchant_info, 47 | order_info, 48 | subscription_info, 49 | &subscription_account.name, 50 | )?; 51 | // ensure the amount paid is as expected 52 | let expected_amount = (quantity as u64) * package.price; 53 | if expected_amount > order_account.paid_amount { 54 | return Err(PaymentProcessorError::NotFullyPaid.into()); 55 | } 56 | // update subscription account 57 | let timestamp = Clock::get()?.unix_timestamp; 58 | if timestamp > subscription_account.period_end { 59 | // had ended so we start a new period 60 | subscription_account.period_start = timestamp; 61 | subscription_account.period_end = timestamp + (package.duration * quantity); 62 | } else { 63 | // not yet ended so we add the time to the end of the current period 64 | subscription_account.period_end = 65 | subscription_account.period_end + (package.duration * quantity); 66 | } 67 | subscription_account.status = SubscriptionStatus::Initialized as u8; 68 | SubscriptionAccount::pack( 69 | &subscription_account, 70 | &mut subscription_info.data.borrow_mut(), 71 | ); 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /src/engine/subscribe.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::common::subscribe_checks; 2 | use crate::engine::constants::DEFAULT_DATA; 3 | use crate::error::PaymentProcessorError; 4 | use crate::state::{Discriminator, Serdes, SubscriptionAccount, SubscriptionStatus}; 5 | use crate::utils::get_subscription_account_size; 6 | use solana_program::{ 7 | account_info::{next_account_info, AccountInfo}, 8 | entrypoint::ProgramResult, 9 | program::{invoke, invoke_signed}, 10 | program_error::ProgramError, 11 | pubkey::Pubkey, 12 | system_instruction, 13 | sysvar::{clock::Clock, rent::Rent, Sysvar}, 14 | }; 15 | 16 | pub fn process_subscribe( 17 | program_id: &Pubkey, 18 | accounts: &[AccountInfo], 19 | name: String, 20 | maybe_data: Option, 21 | ) -> ProgramResult { 22 | let account_info_iter = &mut accounts.iter(); 23 | 24 | let signer_info = next_account_info(account_info_iter)?; 25 | let subscription_info = next_account_info(account_info_iter)?; 26 | let merchant_info = next_account_info(account_info_iter)?; 27 | let order_info = next_account_info(account_info_iter)?; 28 | let system_program_info = next_account_info(account_info_iter)?; 29 | let rent_sysvar_info = next_account_info(account_info_iter)?; 30 | 31 | let (order_account, package) = subscribe_checks( 32 | program_id, 33 | signer_info, 34 | merchant_info, 35 | order_info, 36 | subscription_info, 37 | &name, 38 | )?; 39 | 40 | // ensure the amount paid is as expected 41 | if package.price > order_account.paid_amount { 42 | return Err(PaymentProcessorError::NotFullyPaid.into()); 43 | } 44 | // get subscription account size 45 | let data = match maybe_data { 46 | None => String::from(DEFAULT_DATA), 47 | Some(value) => value, 48 | }; 49 | let account_size = get_subscription_account_size(&name, &data); 50 | // the address of the subscription account is derived using the program id, 51 | // the signer address, the merchant address, and the subscription package name 52 | // thus ensuring a unique address for each signer + merchant + name 53 | let (_subscribe_account_address, bump_seed) = Pubkey::find_program_address( 54 | &[ 55 | &signer_info.key.to_bytes(), 56 | &merchant_info.key.to_bytes(), 57 | &name.as_bytes(), 58 | ], 59 | program_id, 60 | ); 61 | // get signer seeds 62 | let signer_seeds: &[&[_]] = &[ 63 | &signer_info.key.to_bytes(), 64 | &merchant_info.key.to_bytes(), 65 | &name.as_bytes(), 66 | &[bump_seed], 67 | ]; 68 | 69 | // Fund the subscription account with the minimum balance to be rent exempt 70 | invoke( 71 | &system_instruction::transfer( 72 | &signer_info.key, 73 | subscription_info.key, 74 | Rent::default().minimum_balance(account_size), 75 | ), 76 | &[ 77 | signer_info.clone(), 78 | subscription_info.clone(), 79 | system_program_info.clone(), 80 | ], 81 | )?; 82 | // Allocate space for the subscription account 83 | invoke_signed( 84 | &system_instruction::allocate(subscription_info.key, account_size as u64), 85 | &[subscription_info.clone(), system_program_info.clone()], 86 | &[&signer_seeds], 87 | )?; 88 | // Assign the subscription account to the SolPayments program 89 | invoke_signed( 90 | &system_instruction::assign(subscription_info.key, &program_id), 91 | &[subscription_info.clone(), system_program_info.clone()], 92 | &[&signer_seeds], 93 | )?; 94 | 95 | let rent = &Rent::from_account_info(rent_sysvar_info)?; 96 | let timestamp = Clock::get()?.unix_timestamp; 97 | 98 | // get the trial period duration 99 | let trial_duration: i64 = match package.trial { 100 | None => 0, 101 | Some(value) => value, 102 | }; 103 | // get the subscription account 104 | // TODO: ensure this account is not already initialized 105 | let mut subscription_data = subscription_info.try_borrow_mut_data()?; 106 | // Saving subscription information... 107 | let subscription = SubscriptionAccount { 108 | discriminator: Discriminator::Subscription as u8, 109 | status: SubscriptionStatus::Initialized as u8, 110 | owner: signer_info.key.to_bytes(), 111 | merchant: merchant_info.key.to_bytes(), 112 | name, 113 | joined: timestamp, 114 | period_start: timestamp, 115 | period_end: timestamp + trial_duration + package.duration, 116 | data, 117 | }; 118 | subscription.pack(&mut subscription_data); 119 | 120 | // ensure subscription account is rent exempt 121 | if !rent.is_exempt(subscription_info.lamports(), account_size) { 122 | return Err(ProgramError::AccountNotRentExempt); 123 | } 124 | 125 | Ok(()) 126 | } 127 | -------------------------------------------------------------------------------- /src/engine/withdraw.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | engine::common::{get_subscription_package, transfer_sol, verify_subscription_order}, 3 | engine::constants::PDA_SEED, 4 | error::PaymentProcessorError, 5 | state::{ 6 | Discriminator, IsClosed, MerchantAccount, OrderAccount, OrderStatus, Serdes, 7 | SubscriptionAccount, 8 | }, 9 | }; 10 | use solana_program::program_pack::Pack; 11 | use solana_program::{ 12 | account_info::{next_account_info, AccountInfo}, 13 | entrypoint::ProgramResult, 14 | msg, 15 | program::invoke_signed, 16 | program_error::ProgramError, 17 | program_pack::IsInitialized, 18 | pubkey::Pubkey, 19 | sysvar::{clock::Clock, Sysvar}, 20 | }; 21 | use spl_token::{self, state::Account as TokenAccount}; 22 | 23 | pub fn process_withdraw_payment( 24 | program_id: &Pubkey, 25 | accounts: &[AccountInfo], 26 | close_order_account: bool, 27 | ) -> ProgramResult { 28 | let account_info_iter = &mut accounts.iter(); 29 | let signer_info = next_account_info(account_info_iter)?; 30 | let order_info = next_account_info(account_info_iter)?; 31 | let merchant_info = next_account_info(account_info_iter)?; 32 | let order_payment_token_info = next_account_info(account_info_iter)?; 33 | let merchant_token_info = next_account_info(account_info_iter)?; 34 | let account_to_receive_sol_refund_info = next_account_info(account_info_iter)?; 35 | let pda_info = next_account_info(account_info_iter)?; 36 | let token_program_info = next_account_info(account_info_iter)?; 37 | 38 | let timestamp = Clock::get()?.unix_timestamp; 39 | 40 | // ensure signer can sign 41 | if !signer_info.is_signer { 42 | return Err(ProgramError::MissingRequiredSignature); 43 | } 44 | // ensure merchant and order accounts are owned by this program 45 | if *merchant_info.owner != *program_id { 46 | msg!("Error: Wrong owner for merchant account"); 47 | return Err(ProgramError::IncorrectProgramId); 48 | } 49 | if *order_info.owner != *program_id { 50 | msg!("Error: Wrong owner for order account"); 51 | return Err(ProgramError::IncorrectProgramId); 52 | } 53 | // ensure buyer token account is owned by token program 54 | if *merchant_token_info.owner != spl_token::id() { 55 | msg!("Error: Token account must be owned by token program"); 56 | return Err(ProgramError::IncorrectProgramId); 57 | } 58 | // check that provided pda is correct 59 | let (pda, pda_nonce) = Pubkey::find_program_address(&[PDA_SEED], &program_id); 60 | if pda_info.key != &pda { 61 | return Err(ProgramError::InvalidSeeds); 62 | } 63 | // get the merchant account 64 | let merchant_account = MerchantAccount::unpack(&merchant_info.data.borrow())?; 65 | if merchant_account.is_closed() { 66 | return Err(PaymentProcessorError::ClosedAccount.into()); 67 | } 68 | if !merchant_account.is_initialized() { 69 | return Err(ProgramError::UninitializedAccount); 70 | } 71 | // ensure that the token account that we will withdraw to is owned by this 72 | // merchant. This ensures that anyone can call the withdraw instruction 73 | // and the money will still go to the right place 74 | let merchant_token_data = TokenAccount::unpack(&merchant_token_info.data.borrow())?; 75 | if merchant_token_data.owner != Pubkey::new_from_array(merchant_account.owner) { 76 | return Err(PaymentProcessorError::WrongMerchant.into()); 77 | } 78 | // get the order account 79 | let mut order_account = OrderAccount::unpack(&order_info.data.borrow())?; 80 | if order_account.is_closed() { 81 | return Err(PaymentProcessorError::ClosedAccount.into()); 82 | } 83 | if !order_account.is_initialized() { 84 | return Err(ProgramError::UninitializedAccount); 85 | } 86 | // ensure order belongs to this merchant 87 | if merchant_info.key.to_bytes() != order_account.merchant { 88 | return Err(ProgramError::InvalidAccountData); 89 | } 90 | // ensure the order payment token account is the right one 91 | if order_payment_token_info.key.to_bytes() != order_account.token { 92 | return Err(ProgramError::InvalidAccountData); 93 | } 94 | // ensure order is not already paid out 95 | if order_account.status != OrderStatus::Paid as u8 { 96 | return Err(PaymentProcessorError::AlreadyWithdrawn.into()); 97 | } 98 | // check if this is for a subscription payment that has a trial period 99 | if merchant_account.discriminator == Discriminator::MerchantSubscriptionWithTrial as u8 { 100 | let subscription_info = next_account_info(account_info_iter)?; 101 | // ensure subscription account is owned by this program 102 | if *subscription_info.owner != *program_id { 103 | msg!("Error: Wrong owner for subscription account"); 104 | return Err(ProgramError::IncorrectProgramId); 105 | } 106 | // ensure this order is for this subscription 107 | verify_subscription_order(subscription_info, &order_account)?; 108 | // get the subscription account 109 | let subscription_account = SubscriptionAccount::unpack(&subscription_info.data.borrow())?; 110 | if subscription_account.is_closed() { 111 | return Err(PaymentProcessorError::ClosedAccount.into()); 112 | } 113 | if !subscription_account.is_initialized() { 114 | return Err(ProgramError::UninitializedAccount); 115 | } 116 | let package = get_subscription_package(&subscription_account.name, &merchant_account)?; 117 | // get the trial period duration 118 | let trial_duration: i64 = match package.trial { 119 | None => 0, 120 | Some(value) => value, 121 | }; 122 | // don't allow withdrawal if still within trial period 123 | if timestamp < (subscription_account.joined + trial_duration) { 124 | return Err(PaymentProcessorError::CantWithdrawDuringTrial.into()); 125 | } 126 | } 127 | // Transferring payment to the merchant... 128 | invoke_signed( 129 | &spl_token::instruction::transfer( 130 | token_program_info.key, 131 | order_payment_token_info.key, 132 | merchant_token_info.key, 133 | &pda, 134 | &[&pda], 135 | order_account.paid_amount, 136 | ) 137 | .unwrap(), 138 | &[ 139 | token_program_info.clone(), 140 | order_payment_token_info.clone(), 141 | merchant_token_info.clone(), 142 | pda_info.clone(), 143 | ], 144 | &[&[&PDA_SEED, &[pda_nonce]]], 145 | )?; 146 | // Close the order token account since it will never be needed again 147 | invoke_signed( 148 | &spl_token::instruction::close_account( 149 | token_program_info.key, 150 | order_payment_token_info.key, 151 | account_to_receive_sol_refund_info.key, 152 | &pda, 153 | &[&pda], 154 | ) 155 | .unwrap(), 156 | &[ 157 | token_program_info.clone(), 158 | order_payment_token_info.clone(), 159 | account_to_receive_sol_refund_info.clone(), 160 | pda_info.clone(), 161 | ], 162 | &[&[&PDA_SEED, &[pda_nonce]]], 163 | )?; 164 | 165 | if close_order_account { 166 | if merchant_account.owner != signer_info.key.to_bytes() { 167 | msg!("Error: Only merchant account owner can close order account"); 168 | return Err(ProgramError::MissingRequiredSignature); 169 | } 170 | // mark account as closed 171 | order_account.discriminator = Discriminator::Closed as u8; 172 | // Transfer all the sol from the order account to the sol_destination. 173 | transfer_sol( 174 | order_info.clone(), 175 | account_to_receive_sol_refund_info.clone(), 176 | order_info.lamports(), 177 | )?; 178 | } 179 | 180 | // Updating order account information... 181 | order_account.status = OrderStatus::Withdrawn as u8; 182 | order_account.modified = timestamp; 183 | OrderAccount::pack(&order_account, &mut order_info.data.borrow_mut()); 184 | 185 | Ok(()) 186 | } 187 | -------------------------------------------------------------------------------- /src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | #![cfg(not(feature = "no-entrypoint"))] 2 | 3 | use crate::error::PaymentProcessorError; 4 | use crate::instruction::PaymentProcessorInstruction; 5 | use solana_program::{ 6 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 7 | program_error::PrintProgramError, pubkey::Pubkey, 8 | }; 9 | 10 | entrypoint!(process_instruction); 11 | fn process_instruction( 12 | program_id: &Pubkey, 13 | accounts: &[AccountInfo], 14 | instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | if let Err(error) = PaymentProcessorInstruction::process(program_id, accounts, instruction_data) 17 | { 18 | // catch the error so we can print it 19 | error.print::(); 20 | return Err(error); 21 | } 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use solana_program::{ 5 | decode_error::DecodeError, 6 | msg, 7 | program_error::{PrintProgramError, ProgramError}, 8 | }; 9 | use thiserror::Error; 10 | 11 | #[derive(Clone, Debug, Eq, Error, PartialEq, FromPrimitive)] 12 | pub enum PaymentProcessorError { 13 | /// The Amount Is Already Withdrawn 14 | #[error("Error: The Amount Is Already Withdrawn")] 15 | AlreadyWithdrawn, 16 | /// Cannot withdraw during trial period 17 | #[error("Error: Cannot withdraw during trial period")] 18 | CantWithdrawDuringTrial, 19 | /// Account already closed 20 | #[error("Error: Account already closed")] 21 | ClosedAccount, 22 | /// Invalid instruction 23 | #[error("Error: Invalid Instruction")] 24 | InvalidInstruction, 25 | /// Invalid Merchant Data 26 | #[error("Error: Invalid Merchant Data")] 27 | InvalidMerchantData, 28 | /// Invalid Subscription Data 29 | #[error("Error: Invalid Subscription Data")] 30 | InvalidSubscriptionData, 31 | /// Invalid Subscription Package 32 | #[error("Error: Invalid Subscription Package")] 33 | InvalidSubscriptionPackage, 34 | /// The Order Account Is Invalid 35 | #[error("Error: The Order Account Is Invalid")] 36 | InvalidOrder, 37 | /// The Order Data Is Invalid 38 | #[error("Error: The Order Data Is Invalid")] 39 | InvalidOrderData, 40 | /// Seller And Buyer Mints Not The Same 41 | #[error("Error: Seller And Buyer Mints Not The Same")] 42 | MintNotEqual, 43 | /// The Payment Has Not Been Received In Full 44 | #[error("Error: The Payment Has Not Been Received In Full")] 45 | NotFullyPaid, 46 | /// The Payment Has Not Yet Been Made 47 | #[error("Error: The Payment Has Not Yet Been Made")] 48 | NotPaid, 49 | /// The Provided Merchant Is Wrong 50 | #[error("Error: The Provided Merchant Is Wrong")] 51 | WrongMerchant, 52 | /// The Provided Order Account Is Wrong 53 | #[error("Error: The Provided Order Account Is Wrong")] 54 | WrongOrderAccount, 55 | /// The Payer Is Wrong 56 | #[error("Error: The Payer Is Wrong")] 57 | WrongPayer, 58 | /// The Provided Program Owner Is Wrong 59 | #[error("Error: The Provided Program Owner Is Wrong")] 60 | WrongProgramOwner, 61 | /// The Provided Sponsor Is Wrong 62 | #[error("Error: The Provided Sponsor Is Wrong")] 63 | WrongSponsor, 64 | /// The Provided mint Is Wrong 65 | #[error("Error: The Provided mint Is Wrong")] 66 | WrongMint, 67 | } 68 | 69 | impl From for ProgramError { 70 | fn from(e: PaymentProcessorError) -> Self { 71 | ProgramError::Custom(e as u32) 72 | } 73 | } 74 | 75 | impl DecodeError for PaymentProcessorError { 76 | fn type_of() -> &'static str { 77 | "Solana Payment Processor Error" 78 | } 79 | } 80 | 81 | impl PrintProgramError for PaymentProcessorError { 82 | fn print(&self) { 83 | msg!(&self.to_string()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/instruction.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::json::OrderItems; 2 | use borsh::{BorshDeserialize, BorshSerialize}; 3 | use solana_program::{ 4 | instruction::{AccountMeta, Instruction}, 5 | pubkey::Pubkey, 6 | sysvar, 7 | }; 8 | use spl_token::{self}; 9 | use std::collections::BTreeMap; 10 | 11 | #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] 12 | pub enum PaymentProcessorInstruction { 13 | /// Register for a merchant account. 14 | /// 15 | /// Accounts expected: 16 | /// 17 | /// 0. `[signer]` The account of the person initializing the merchant account 18 | /// 1. `[writable]` The merchant account. Owned by this program 19 | /// 2. `[]` System program 20 | /// 3. `[]` The rent sysvar 21 | /// 4. `[optional]` The sponsor account 22 | RegisterMerchant { 23 | /// the seed used when creating the account 24 | #[allow(dead_code)] // not dead code.. 25 | seed: Option, 26 | /// the amount (in SOL lamports) that will be charged as a fee 27 | #[allow(dead_code)] // not dead code.. 28 | fee: Option, 29 | /// arbitrary merchant data (maybe as a JSON string) 30 | #[allow(dead_code)] // not dead code.. 31 | data: Option, 32 | }, 33 | /// Express Checkout 34 | /// 35 | /// Meant to be used to process payments initialized by systems that reside off-chain 36 | /// such as traditional e-commerce software. 37 | /// 38 | /// Accounts expected: 39 | /// 40 | /// 0. `[signer]` The account of the person initializing the transaction 41 | /// 1. `[writable]` The order account. Owned by this program 42 | /// 2. `[]` The merchant account. Owned by this program 43 | /// 3. `[writable]` The seller token account - this is where the amount paid will go. Owned by this program 44 | /// 4. `[writable]` The buyer token account 45 | /// 5. `[writable]` The program owner account (where we will send program owner fee) 46 | /// 6. `[writable]` The sponsor account (where we will send sponsor fee) 47 | /// 7. `[]` The token mint account - represents the 'currency' being used 48 | /// 8. `[]` This program's derived address 49 | /// 9. `[]` The token program 50 | /// 10. `[]` The System program 51 | /// 11. `[]` The rent sysvar 52 | ExpressCheckout { 53 | #[allow(dead_code)] // not dead code.. 54 | amount: u64, 55 | /// the external order id (as in issued by the merchant) 56 | #[allow(dead_code)] // not dead code.. 57 | order_id: String, 58 | // An extra field that can store an encrypted (ot not encrypted) string 59 | // that the merchant can use to assert if a transaction is authentic 60 | #[allow(dead_code)] // not dead code.. 61 | secret: String, 62 | /// arbitrary merchant data (maybe as a JSON string) 63 | #[allow(dead_code)] // not dead code.. 64 | data: Option, 65 | }, 66 | /// Chain Checkout 67 | /// 68 | /// Meant to process payments that are validated completely on chain. That is, 69 | /// all the information required to check that the payment is valid is available 70 | /// on-chain. 71 | /// 72 | /// This is made possible by relying on a merchant account that has certain defined 73 | /// items for which payment can be made. See the engine::json::Item struct as well 74 | /// as the chain checkout tests for more on how this works. 75 | /// 76 | /// Accounts expected: 77 | /// 78 | /// 0. `[signer]` The account of the person initializing the transaction 79 | /// 1. `[writable]` The order account. Owned by this program 80 | /// 2. `[]` The merchant account. Owned by this program 81 | /// 3. `[writable]` The seller token account - this is where the amount paid will go. Owned by this program 82 | /// 4. `[writable]` The buyer token account 83 | /// 5. `[writable]` The program owner account (where we will send program owner fee) 84 | /// 6. `[writable]` The sponsor account (where we will send sponsor fee) 85 | /// 7. `[]` The token mint account - represents the 'currency' being used 86 | /// 8. `[]` This program's derived address 87 | /// 9. `[]` The token program 88 | /// 10. `[]` The System program 89 | /// 11. `[]` The rent sysvar 90 | ChainCheckout { 91 | #[allow(dead_code)] // not dead code.. 92 | amount: u64, 93 | /// the external order id (as in issued by the merchant) 94 | #[allow(dead_code)] // not dead code.. 95 | order_items: BTreeMap, // use this instead of OrderItems for readability in API 96 | /// arbitrary merchant data (maybe as a JSON string) 97 | #[allow(dead_code)] // not dead code.. 98 | data: Option, 99 | }, 100 | /// Withdraw funds for a particular order 101 | /// 102 | /// Note that a payment cannot be withdrawn for an order made for a subscription 103 | /// payment that is still within the subscription trial period. 104 | /// 105 | /// Accounts expected: 106 | /// 107 | /// 0. `[signer]` The account of the person initializing the transaction 108 | /// 1. `[writable]` The order account. Owned by this program 109 | /// 2. `[]` The merchant account. Owned by this program 110 | /// 3. `[writable]` The order token account (where the money was put during payment) 111 | /// 4. `[writable]` The merchant token account (where we will withdraw to) 112 | /// 5. `[writable]` This account receives the refunded SOL after closing order token account 113 | /// 6. `[]` This program's derived address 114 | /// 7. `[]` The token program 115 | Withdraw { 116 | /// should we close the order account? 117 | /// can be sent as 0 for false; 1 for true from a dApp 118 | #[allow(dead_code)] // not dead code.. 119 | close_order_account: bool, 120 | }, 121 | /// Initialize a subscription 122 | /// 123 | /// A subscription is possible by relying on a merchant account that was created with 124 | /// the correct subscription package information. See engine::json::Packages as well as 125 | /// subscription tests for more on this. 126 | /// 127 | /// A complete subscription transaction includes an ExpressCheckout instruction followed 128 | /// by a Subscribe instruction. The actual payment is made in the ExpressCheckout instruction 129 | /// and subsequently thr subscription is activated in the Subscribe instruction. 130 | /// 131 | /// Accounts expected: 132 | /// 133 | /// 0. `[signer]` The account of the person initializing the transaction 134 | /// 1. `[writable]` The subscription account. Owned by this program 135 | /// 2. `[]` The merchant account. Owned by this program 136 | /// 3. `[]` The order account. Owned by this program 137 | /// 4. `[]` The System program 138 | /// 5. `[]` The rent sysvar 139 | Subscribe { 140 | /// the subscription package name 141 | #[allow(dead_code)] // not dead code.. 142 | name: String, 143 | /// arbitrary merchant data (maybe as a JSON string) 144 | #[allow(dead_code)] // not dead code.. 145 | data: Option, 146 | }, 147 | /// Renew a subscription 148 | /// 149 | /// A complete RenewSubscription transaction includes an ExpressCheckout instruction 150 | /// followed by a RenewSubscription instruction. The actual payment is made in the 151 | /// ExpressCheckout instruction and subsequently thr subscription is activated in the 152 | /// RenewSubscription instruction. 153 | /// 154 | /// Accounts expected: 155 | /// 156 | /// 0. `[signer]` The account of the person initializing the transaction 157 | /// 1. `[writable]` The subscription account. Owned by this program 158 | /// 2. `[]` The merchant account. Owned by this program 159 | /// 3. `[]` The order account. Owned by this program 160 | RenewSubscription { 161 | /// the number of periods to renew e.g. if the subscription period is a year 162 | /// you can choose to renew for 1 year, 2 years, n years, etc 163 | #[allow(dead_code)] // not dead code.. 164 | quantity: i64, 165 | }, 166 | /// Cancel a subscription 167 | /// 168 | /// If a CancelSubscription instruction is sent during the trial period of a 169 | /// subscription, the amount initially paid for the subscription will be refunded in 170 | /// full. 171 | /// 172 | /// Accounts expected: 173 | /// 174 | /// 0. `[signer]` The account of the person initializing the transaction 175 | /// 1. `[writable]` The subscription account. Owned by this program 176 | /// 2. `[]` The merchant account. Owned by this program 177 | /// 3. `[writable]` The order account. Owned by this program 178 | /// 4. `[writable]` The order token account - this is where the amount was paid into. Owned by this program 179 | /// 5. `[writable]` The refund token account - this is where the refund will go 180 | /// 6. `[writable]` This account receives the refunded SOL after closing order token account 181 | /// 7. `[]` This program's derived address 182 | /// 8. `[]` The token program 183 | CancelSubscription, 184 | } 185 | 186 | /// Creates an 'RegisterMerchant' instruction. 187 | pub fn register_merchant( 188 | program_id: Pubkey, 189 | signer: Pubkey, 190 | merchant: Pubkey, 191 | seed: Option, 192 | fee: Option, 193 | data: Option, 194 | sponsor: Option<&Pubkey>, 195 | ) -> Instruction { 196 | let mut account_metas = vec![ 197 | AccountMeta::new(signer, true), 198 | AccountMeta::new(merchant, false), 199 | AccountMeta::new_readonly(solana_program::system_program::id(), false), 200 | AccountMeta::new_readonly(sysvar::rent::id(), false), 201 | ]; 202 | 203 | if let Some(sponsor) = sponsor { 204 | account_metas.push(AccountMeta::new_readonly(*sponsor, false)); 205 | } 206 | 207 | Instruction { 208 | program_id, 209 | accounts: account_metas, 210 | data: PaymentProcessorInstruction::RegisterMerchant { seed, fee, data } 211 | .try_to_vec() 212 | .unwrap(), 213 | } 214 | } 215 | 216 | /// Creates an 'ExpressCheckout' instruction. 217 | pub fn express_checkout( 218 | program_id: Pubkey, 219 | signer: Pubkey, 220 | order: Pubkey, 221 | merchant: Pubkey, 222 | seller_token: Pubkey, 223 | buyer_token: Pubkey, 224 | mint: Pubkey, 225 | program_owner: Pubkey, 226 | sponsor: Pubkey, 227 | pda: Pubkey, 228 | amount: u64, 229 | order_id: String, 230 | secret: String, 231 | data: Option, 232 | ) -> Instruction { 233 | Instruction { 234 | program_id, 235 | accounts: vec![ 236 | AccountMeta::new(signer, true), 237 | AccountMeta::new(order, true), 238 | AccountMeta::new_readonly(merchant, false), 239 | AccountMeta::new(seller_token, false), 240 | AccountMeta::new(buyer_token, false), 241 | AccountMeta::new(program_owner, false), 242 | AccountMeta::new(sponsor, false), 243 | AccountMeta::new_readonly(mint, false), 244 | AccountMeta::new_readonly(pda, false), 245 | AccountMeta::new_readonly(spl_token::id(), false), 246 | AccountMeta::new_readonly(solana_program::system_program::id(), false), 247 | AccountMeta::new_readonly(sysvar::rent::id(), false), 248 | ], 249 | data: PaymentProcessorInstruction::ExpressCheckout { 250 | amount, 251 | order_id, 252 | secret, 253 | data, 254 | } 255 | .try_to_vec() 256 | .unwrap(), 257 | } 258 | } 259 | 260 | /// Creates an 'ChainCheckout' instruction. 261 | pub fn chain_checkout( 262 | program_id: Pubkey, 263 | signer: Pubkey, 264 | order: Pubkey, 265 | merchant: Pubkey, 266 | seller_token: Pubkey, 267 | buyer_token: Pubkey, 268 | mint: Pubkey, 269 | program_owner: Pubkey, 270 | sponsor: Pubkey, 271 | pda: Pubkey, 272 | amount: u64, 273 | order_items: OrderItems, 274 | data: Option, 275 | ) -> Instruction { 276 | Instruction { 277 | program_id, 278 | accounts: vec![ 279 | AccountMeta::new(signer, true), 280 | AccountMeta::new(order, true), 281 | AccountMeta::new_readonly(merchant, false), 282 | AccountMeta::new(seller_token, false), 283 | AccountMeta::new(buyer_token, false), 284 | AccountMeta::new(program_owner, false), 285 | AccountMeta::new(sponsor, false), 286 | AccountMeta::new_readonly(mint, false), 287 | AccountMeta::new_readonly(pda, false), 288 | AccountMeta::new_readonly(spl_token::id(), false), 289 | AccountMeta::new_readonly(solana_program::system_program::id(), false), 290 | AccountMeta::new_readonly(sysvar::rent::id(), false), 291 | ], 292 | data: PaymentProcessorInstruction::ChainCheckout { 293 | amount, 294 | order_items, 295 | data, 296 | } 297 | .try_to_vec() 298 | .unwrap(), 299 | } 300 | } 301 | 302 | /// Creates an 'Withdraw' instruction. 303 | pub fn withdraw( 304 | program_id: Pubkey, 305 | signer: Pubkey, 306 | order: Pubkey, 307 | merchant: Pubkey, 308 | order_payment_token: Pubkey, 309 | merchant_token: Pubkey, 310 | account_to_receive_sol_refund: Pubkey, 311 | pda: Pubkey, 312 | subscription: Option, 313 | close_order_account: bool, 314 | ) -> Instruction { 315 | let mut account_metas = vec![ 316 | AccountMeta::new(signer, true), 317 | AccountMeta::new(order, false), 318 | AccountMeta::new_readonly(merchant, false), 319 | AccountMeta::new(order_payment_token, false), 320 | AccountMeta::new(merchant_token, false), 321 | AccountMeta::new(account_to_receive_sol_refund, false), 322 | AccountMeta::new_readonly(pda, false), 323 | AccountMeta::new_readonly(spl_token::id(), false), 324 | ]; 325 | 326 | if let Some(subscription) = subscription { 327 | account_metas.push(AccountMeta::new_readonly(subscription, false)); 328 | } 329 | 330 | Instruction { 331 | program_id, 332 | accounts: account_metas, 333 | data: PaymentProcessorInstruction::Withdraw { 334 | close_order_account, 335 | } 336 | .try_to_vec() 337 | .unwrap(), 338 | } 339 | } 340 | 341 | /// creates a 'Subscribe' instruction 342 | pub fn subscribe( 343 | program_id: Pubkey, 344 | signer: Pubkey, 345 | subscription: Pubkey, 346 | merchant: Pubkey, 347 | order: Pubkey, 348 | name: String, 349 | data: Option, 350 | ) -> Instruction { 351 | Instruction { 352 | program_id, 353 | accounts: vec![ 354 | AccountMeta::new(signer, true), 355 | AccountMeta::new(subscription, false), 356 | AccountMeta::new_readonly(merchant, false), 357 | AccountMeta::new_readonly(order, false), 358 | AccountMeta::new_readonly(solana_program::system_program::id(), false), 359 | AccountMeta::new_readonly(sysvar::rent::id(), false), 360 | ], 361 | data: PaymentProcessorInstruction::Subscribe { name, data } 362 | .try_to_vec() 363 | .unwrap(), 364 | } 365 | } 366 | 367 | /// creates a 'RenewSubscription' instruction 368 | pub fn renew_subscription( 369 | program_id: Pubkey, 370 | signer: Pubkey, 371 | subscription: Pubkey, 372 | merchant: Pubkey, 373 | order: Pubkey, 374 | quantity: i64, 375 | ) -> Instruction { 376 | Instruction { 377 | program_id, 378 | accounts: vec![ 379 | AccountMeta::new(signer, true), 380 | AccountMeta::new(subscription, false), 381 | AccountMeta::new_readonly(merchant, false), 382 | AccountMeta::new_readonly(order, false), 383 | ], 384 | data: PaymentProcessorInstruction::RenewSubscription { quantity } 385 | .try_to_vec() 386 | .unwrap(), 387 | } 388 | } 389 | 390 | /// creates a 'CancelSubscription' instruction 391 | pub fn cancel_subscription( 392 | program_id: Pubkey, 393 | signer: Pubkey, 394 | subscription: Pubkey, 395 | merchant: Pubkey, 396 | order: Pubkey, 397 | order_token: Pubkey, 398 | refund_token: Pubkey, 399 | account_to_receive_sol_refund: Pubkey, 400 | pda: Pubkey, 401 | ) -> Instruction { 402 | Instruction { 403 | program_id, 404 | accounts: vec![ 405 | AccountMeta::new(signer, true), 406 | AccountMeta::new(subscription, false), 407 | AccountMeta::new_readonly(merchant, false), 408 | AccountMeta::new(order, false), 409 | AccountMeta::new(order_token, false), 410 | AccountMeta::new(refund_token, false), 411 | AccountMeta::new(account_to_receive_sol_refund, false), 412 | AccountMeta::new_readonly(pda, false), 413 | AccountMeta::new_readonly(spl_token::id(), false), 414 | ], 415 | data: PaymentProcessorInstruction::CancelSubscription 416 | .try_to_vec() 417 | .unwrap(), 418 | } 419 | } 420 | 421 | #[cfg(test)] 422 | mod test { 423 | use { 424 | super::*, 425 | crate::engine::constants::{ 426 | DEFAULT_FEE_IN_LAMPORTS, INITIAL, MERCHANT, MIN_FEE_IN_LAMPORTS, PAID, PDA_SEED, 427 | PROGRAM_OWNER, SPONSOR_FEE, 428 | }, 429 | crate::error::PaymentProcessorError, 430 | crate::instruction::PaymentProcessorInstruction, 431 | crate::state::{ 432 | MerchantAccount, OrderAccount, OrderStatus, Serdes, SubscriptionAccount, 433 | SubscriptionStatus, 434 | }, 435 | crate::utils::{get_amounts, get_order_account_size}, 436 | assert_matches::*, 437 | serde_json::{json, Value}, 438 | solana_program::{ 439 | hash::Hash, 440 | program_pack::{IsInitialized, Pack}, 441 | rent::Rent, 442 | system_instruction, 443 | }, 444 | solana_program_test::*, 445 | solana_sdk::{ 446 | instruction::InstructionError, 447 | signature::{Keypair, Signer}, 448 | transaction::{Transaction, TransactionError}, 449 | transport::TransportError, 450 | }, 451 | spl_token::{ 452 | instruction::{initialize_account, initialize_mint, mint_to}, 453 | state::{Account as TokenAccount, Mint}, 454 | }, 455 | std::str::FromStr, 456 | }; 457 | 458 | type MerchantResult = (Pubkey, Pubkey, BanksClient, Keypair, Hash); 459 | 460 | fn create_mint_transaction( 461 | payer: &Keypair, 462 | mint: &Keypair, 463 | mint_authority: &Keypair, 464 | recent_blockhash: Hash, 465 | ) -> Transaction { 466 | let instructions = [ 467 | system_instruction::create_account( 468 | &payer.pubkey(), 469 | &mint.pubkey(), 470 | Rent::default().minimum_balance(Mint::LEN), 471 | Mint::LEN as u64, 472 | &spl_token::id(), 473 | ), 474 | initialize_mint( 475 | &spl_token::id(), 476 | &mint.pubkey(), 477 | &mint_authority.pubkey(), 478 | None, 479 | 0, 480 | ) 481 | .unwrap(), 482 | ]; 483 | let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey())); 484 | transaction.partial_sign(&[payer, mint], recent_blockhash); 485 | transaction 486 | } 487 | 488 | fn create_token_account_transaction( 489 | payer: &Keypair, 490 | mint: &Keypair, 491 | recent_blockhash: Hash, 492 | token_account: &Keypair, 493 | token_account_owner: &Pubkey, 494 | amount: u64, 495 | ) -> Transaction { 496 | let instructions = [ 497 | system_instruction::create_account( 498 | &payer.pubkey(), 499 | &token_account.pubkey(), 500 | Rent::default().minimum_balance(TokenAccount::LEN), 501 | TokenAccount::LEN as u64, 502 | &spl_token::id(), 503 | ), 504 | initialize_account( 505 | &spl_token::id(), 506 | &token_account.pubkey(), 507 | &mint.pubkey(), 508 | token_account_owner, 509 | ) 510 | .unwrap(), 511 | mint_to( 512 | &spl_token::id(), 513 | &mint.pubkey(), 514 | &token_account.pubkey(), 515 | token_account_owner, 516 | &[&payer.pubkey()], 517 | amount, 518 | ) 519 | .unwrap(), 520 | ]; 521 | let mut transaction = Transaction::new_with_payer(&instructions, Some(&payer.pubkey())); 522 | transaction.partial_sign(&[payer, token_account], recent_blockhash); 523 | transaction 524 | } 525 | 526 | async fn create_merchant_account( 527 | seed: Option, 528 | fee: Option, 529 | sponsor: Option<&Pubkey>, 530 | data: Option, 531 | ) -> MerchantResult { 532 | let program_id = Pubkey::from_str(&"mosh111111111111111111111111111111111111111").unwrap(); 533 | 534 | let (mut banks_client, payer, recent_blockhash) = ProgramTest::new( 535 | "sol_payment_processor", 536 | program_id, 537 | processor!(PaymentProcessorInstruction::process), 538 | ) 539 | .start() 540 | .await; 541 | 542 | let real_seed = match &seed { 543 | None => MERCHANT, 544 | Some(value) => &value, 545 | }; 546 | 547 | // first we create a public key for the merchant account 548 | let merchant_acc_pubkey = 549 | Pubkey::create_with_seed(&payer.pubkey(), real_seed, &program_id).unwrap(); 550 | 551 | // then call register merchant ix 552 | let mut transaction = Transaction::new_with_payer( 553 | &[register_merchant( 554 | program_id, 555 | payer.pubkey(), 556 | merchant_acc_pubkey, 557 | Some(real_seed.to_string()), 558 | fee, 559 | data, 560 | sponsor, 561 | )], 562 | Some(&payer.pubkey()), 563 | ); 564 | transaction.sign(&[&payer], recent_blockhash); 565 | assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); 566 | return ( 567 | program_id, 568 | merchant_acc_pubkey, 569 | banks_client, 570 | payer, 571 | recent_blockhash, 572 | ); 573 | } 574 | 575 | async fn prepare_order( 576 | program_id: &Pubkey, 577 | merchant: &Pubkey, 578 | mint: &Pubkey, 579 | banks_client: &mut BanksClient, 580 | ) -> (Keypair, Pubkey, Pubkey, MerchantAccount) { 581 | let order_acc_keypair = Keypair::new(); 582 | 583 | let (pda, _bump_seed) = Pubkey::find_program_address(&[PDA_SEED], &program_id); 584 | 585 | let (seller_token, _bump_seed) = Pubkey::find_program_address( 586 | &[ 587 | &order_acc_keypair.pubkey().to_bytes(), 588 | &spl_token::id().to_bytes(), 589 | &mint.to_bytes(), 590 | ], 591 | program_id, 592 | ); 593 | 594 | let merchant_account = banks_client.get_account(*merchant).await; 595 | let merchant_data = match merchant_account { 596 | Ok(data) => match data { 597 | None => panic!("Oo"), 598 | Some(value) => match MerchantAccount::unpack(&value.data) { 599 | Ok(data) => data, 600 | Err(error) => panic!("Problem: {:?}", error), 601 | }, 602 | }, 603 | Err(error) => panic!("Problem: {:?}", error), 604 | }; 605 | 606 | (order_acc_keypair, seller_token, pda, merchant_data) 607 | } 608 | 609 | async fn create_token_account( 610 | amount: u64, 611 | mint_keypair: &Keypair, 612 | merchant_result: &mut MerchantResult, 613 | ) -> Keypair { 614 | // next create token account for test 615 | let buyer_token_keypair = Keypair::new(); 616 | 617 | // create and initialize mint 618 | assert_matches!( 619 | merchant_result 620 | .2 621 | .process_transaction(create_mint_transaction( 622 | &merchant_result.3, 623 | &mint_keypair, 624 | &merchant_result.3, 625 | merchant_result.4 626 | )) 627 | .await, 628 | Ok(()) 629 | ); 630 | // create and initialize buyer token account 631 | assert_matches!( 632 | merchant_result 633 | .2 634 | .process_transaction(create_token_account_transaction( 635 | &merchant_result.3, 636 | &mint_keypair, 637 | merchant_result.4, 638 | &buyer_token_keypair, 639 | &merchant_result.3.pubkey(), 640 | amount + 2000000, 641 | )) 642 | .await, 643 | Ok(()) 644 | ); 645 | 646 | buyer_token_keypair 647 | } 648 | 649 | async fn create_order_express_checkout( 650 | amount: u64, 651 | order_id: &String, 652 | secret: &String, 653 | data: Option, 654 | merchant_result: &mut MerchantResult, 655 | mint_keypair: &Keypair, 656 | ) -> (Pubkey, Pubkey) { 657 | let buyer_token_keypair = create_token_account(amount, mint_keypair, merchant_result).await; 658 | let (order_acc_keypair, seller_token, pda, merchant_data) = prepare_order( 659 | &merchant_result.0, 660 | &merchant_result.1, 661 | &mint_keypair.pubkey(), 662 | &mut merchant_result.2, 663 | ) 664 | .await; 665 | 666 | // call express checkout ix 667 | let mut transaction = Transaction::new_with_payer( 668 | &[express_checkout( 669 | merchant_result.0, 670 | merchant_result.3.pubkey(), 671 | order_acc_keypair.pubkey(), 672 | merchant_result.1, 673 | seller_token, 674 | buyer_token_keypair.pubkey(), 675 | mint_keypair.pubkey(), 676 | Pubkey::from_str(PROGRAM_OWNER).unwrap(), 677 | Pubkey::new_from_array(merchant_data.sponsor), 678 | pda, 679 | amount, 680 | (&order_id).to_string(), 681 | (&secret).to_string(), 682 | data, 683 | )], 684 | Some(&merchant_result.3.pubkey()), 685 | ); 686 | transaction.sign(&[&merchant_result.3, &order_acc_keypair], merchant_result.4); 687 | assert_matches!( 688 | &mut merchant_result.2.process_transaction(transaction).await, 689 | Ok(()) 690 | ); 691 | 692 | (order_acc_keypair.pubkey(), seller_token) 693 | } 694 | 695 | async fn create_chain_checkout_transaction( 696 | amount: u64, 697 | order_items: &OrderItems, 698 | data: Option, 699 | merchant_result: &mut MerchantResult, 700 | mint_keypair: &Keypair, 701 | ) -> Result<(Pubkey, Pubkey), TransportError> { 702 | let buyer_token_keypair = create_token_account(amount, mint_keypair, merchant_result).await; 703 | let (order_acc_keypair, seller_token, pda, merchant_data) = prepare_order( 704 | &merchant_result.0, 705 | &merchant_result.1, 706 | &mint_keypair.pubkey(), 707 | &mut merchant_result.2, 708 | ) 709 | .await; 710 | let order_items = order_items.clone(); 711 | 712 | // call chain checkout ix 713 | let mut transaction = Transaction::new_with_payer( 714 | &[chain_checkout( 715 | merchant_result.0, 716 | merchant_result.3.pubkey(), 717 | order_acc_keypair.pubkey(), 718 | merchant_result.1, 719 | seller_token, 720 | buyer_token_keypair.pubkey(), 721 | mint_keypair.pubkey(), 722 | Pubkey::from_str(PROGRAM_OWNER).unwrap(), 723 | Pubkey::new_from_array(merchant_data.sponsor), 724 | pda, 725 | amount, 726 | order_items, 727 | data, 728 | )], 729 | Some(&merchant_result.3.pubkey()), 730 | ); 731 | transaction.sign(&[&merchant_result.3, &order_acc_keypair], merchant_result.4); 732 | let _result = merchant_result.2.process_transaction(transaction).await?; 733 | Ok((order_acc_keypair.pubkey(), seller_token)) 734 | } 735 | 736 | async fn create_order_chain_checkout( 737 | amount: u64, 738 | order_items: &OrderItems, 739 | data: Option, 740 | merchant_result: &mut MerchantResult, 741 | mint_keypair: &Keypair, 742 | ) -> (Pubkey, Pubkey) { 743 | let transaction = create_chain_checkout_transaction( 744 | amount, 745 | order_items, 746 | data, 747 | merchant_result, 748 | mint_keypair, 749 | ) 750 | .await; 751 | 752 | assert!(transaction.is_ok()); 753 | transaction.unwrap() 754 | } 755 | 756 | async fn run_merchant_tests(result: MerchantResult) -> MerchantAccount { 757 | let program_id = result.0; 758 | let merchant = result.1; 759 | let mut banks_client = result.2; 760 | let payer = result.3; 761 | // test contents of merchant account 762 | let merchant_account = banks_client.get_account(merchant).await; 763 | let merchant_account = match merchant_account { 764 | Ok(data) => match data { 765 | None => panic!("Oo"), 766 | Some(value) => value, 767 | }, 768 | Err(error) => panic!("Problem: {:?}", error), 769 | }; 770 | assert_eq!(merchant_account.owner, program_id); 771 | let merchant_data = MerchantAccount::unpack(&merchant_account.data); 772 | let merchant_data = match merchant_data { 773 | Ok(data) => data, 774 | Err(error) => panic!("Problem: {:?}", error), 775 | }; 776 | assert_eq!(true, merchant_data.is_initialized()); 777 | assert_eq!(payer.pubkey(), Pubkey::new_from_array(merchant_data.owner)); 778 | 779 | merchant_data 780 | } 781 | 782 | #[tokio::test] 783 | async fn test_register_merchant() { 784 | let result = 785 | create_merchant_account(Option::None, Option::None, Option::None, Option::None).await; 786 | let merchant_data = run_merchant_tests(result).await; 787 | assert_eq!(DEFAULT_FEE_IN_LAMPORTS, merchant_data.fee); 788 | assert_eq!(String::from("{}"), merchant_data.data); 789 | } 790 | 791 | #[tokio::test] 792 | async fn test_register_merchant_with_seed() { 793 | let result = create_merchant_account( 794 | Some(String::from("mosh")), 795 | Option::None, 796 | Option::None, 797 | Option::None, 798 | ) 799 | .await; 800 | let merchant = result.1; 801 | let payer = result.3; 802 | let program_id = result.0; 803 | assert_eq!( 804 | merchant, 805 | Pubkey::create_with_seed(&payer.pubkey(), "mosh", &program_id).unwrap() 806 | ); 807 | } 808 | 809 | #[tokio::test] 810 | /// assert that the minimum fee is used when custom fee too low 811 | async fn test_register_merchant_fee_default() { 812 | let result = 813 | create_merchant_account(Option::None, Some(10), Option::None, Option::None).await; 814 | let merchant_data = run_merchant_tests(result).await; 815 | assert_eq!(MIN_FEE_IN_LAMPORTS, merchant_data.fee); 816 | } 817 | 818 | #[tokio::test] 819 | async fn test_register_merchant_with_all_stuff() { 820 | let seed = String::from("mosh"); 821 | let sponsor_pk = Pubkey::new_unique(); 822 | let data = String::from( 823 | r#"{"code":200,"success":true,"payload":{"features":["awesome","easyAPI","lowLearningCurve"]}}"#, 824 | ); 825 | let datas = data.clone(); 826 | let result = 827 | create_merchant_account(Some(seed), Some(90000), Some(&sponsor_pk), Some(data)).await; 828 | let merchant_data = run_merchant_tests(result).await; 829 | assert_eq!(datas, merchant_data.data); 830 | assert_eq!(90000, merchant_data.fee); 831 | assert_eq!(sponsor_pk, Pubkey::new_from_array(merchant_data.sponsor)); 832 | // just for sanity verify that you can get some of the JSON values 833 | let json_value: Value = serde_json::from_str(&merchant_data.data).unwrap(); 834 | assert_eq!(200, json_value["code"]); 835 | assert_eq!(true, json_value["success"]); 836 | } 837 | 838 | async fn run_common_checkout_tests( 839 | amount: u64, 840 | merchant_result: &mut MerchantResult, 841 | order_acc_pubkey: &Pubkey, 842 | seller_account_pubkey: &Pubkey, 843 | mint_keypair: &Keypair, 844 | ) -> OrderAccount { 845 | // program_id => merchant_result.0; 846 | // merchant_account_pubkey => merchant_result.1; 847 | // banks_client => merchant_result.2; 848 | // payer => merchant_result.3; 849 | 850 | let order_account = merchant_result.2.get_account(*order_acc_pubkey).await; 851 | let order_account = match order_account { 852 | Ok(data) => match data { 853 | None => panic!("Oo"), 854 | Some(value) => value, 855 | }, 856 | Err(error) => panic!("Problem: {:?}", error), 857 | }; 858 | assert_eq!(order_account.owner, merchant_result.0,); 859 | 860 | let order_data = OrderAccount::unpack(&order_account.data); 861 | let order_data = match order_data { 862 | Ok(data) => data, 863 | Err(error) => panic!("Problem: {:?}", error), 864 | }; 865 | assert_eq!(true, order_data.is_initialized()); 866 | assert_eq!(OrderStatus::Paid as u8, order_data.status); 867 | assert_eq!(merchant_result.1.to_bytes(), order_data.merchant); 868 | assert_eq!(mint_keypair.pubkey().to_bytes(), order_data.mint); 869 | assert_eq!(seller_account_pubkey.to_bytes(), order_data.token); 870 | assert_eq!(merchant_result.3.pubkey().to_bytes(), order_data.payer); 871 | assert_eq!(amount, order_data.expected_amount); 872 | assert_eq!(amount, order_data.paid_amount); 873 | assert_eq!( 874 | order_account.lamports, 875 | Rent::default().minimum_balance(get_order_account_size( 876 | &order_data.order_id, 877 | &order_data.secret, 878 | &order_data.data, 879 | )) 880 | ); 881 | 882 | // test contents of seller token account 883 | let seller_token_account = merchant_result.2.get_account(*seller_account_pubkey).await; 884 | let seller_token_account = match seller_token_account { 885 | Ok(data) => match data { 886 | None => panic!("Oo"), 887 | Some(value) => value, 888 | }, 889 | Err(error) => panic!("Problem: {:?}", error), 890 | }; 891 | let seller_account_data = spl_token::state::Account::unpack(&seller_token_account.data); 892 | let seller_account_data = match seller_account_data { 893 | Ok(data) => data, 894 | Err(error) => panic!("Problem: {:?}", error), 895 | }; 896 | let (pda, _bump_seed) = Pubkey::find_program_address(&[PDA_SEED], &merchant_result.0); 897 | assert_eq!(amount, seller_account_data.amount); 898 | assert_eq!(pda, seller_account_data.owner); 899 | assert_eq!(mint_keypair.pubkey(), seller_account_data.mint); 900 | 901 | // test that sponsor was saved okay 902 | let merchant_account = merchant_result.2.get_account(merchant_result.1).await; 903 | let merchant_data = match merchant_account { 904 | Ok(data) => match data { 905 | None => panic!("Oo"), 906 | Some(value) => match MerchantAccount::unpack(&value.data) { 907 | Ok(data) => data, 908 | Err(error) => panic!("Problem: {:?}", error), 909 | }, 910 | }, 911 | Err(error) => panic!("Problem: {:?}", error), 912 | }; 913 | 914 | let program_owner_key = Pubkey::from_str(PROGRAM_OWNER).unwrap(); 915 | let sponsor = Pubkey::new_from_array(merchant_data.sponsor); 916 | 917 | let program_owner_account = merchant_result.2.get_account(program_owner_key).await; 918 | let program_owner_account = match program_owner_account { 919 | Ok(data) => match data { 920 | None => panic!("Oo"), 921 | Some(value) => value, 922 | }, 923 | Err(error) => panic!("Problem: {:?}", error), 924 | }; 925 | 926 | if sponsor == program_owner_key { 927 | // test contents of program owner account 928 | assert_eq!(merchant_data.fee, program_owner_account.lamports); 929 | } else { 930 | // test contents of program owner account and sponsor account 931 | let (program_owner_fee, sponsor_fee) = get_amounts(merchant_data.fee, SPONSOR_FEE); 932 | let sponsor_account = merchant_result.2.get_account(sponsor).await; 933 | let sponsor_account = match sponsor_account { 934 | Ok(data) => match data { 935 | None => panic!("Oo"), 936 | Some(value) => value, 937 | }, 938 | Err(error) => panic!("Problem: {:?}", error), 939 | }; 940 | assert_eq!(program_owner_fee, program_owner_account.lamports); 941 | assert_eq!(sponsor_fee, sponsor_account.lamports); 942 | } 943 | 944 | order_data 945 | } 946 | 947 | async fn run_checkout_tests( 948 | amount: u64, 949 | order_id: String, 950 | secret: String, 951 | data: Option, 952 | merchant_result: &mut MerchantResult, 953 | order_acc_pubkey: &Pubkey, 954 | seller_account_pubkey: &Pubkey, 955 | mint_keypair: &Keypair, 956 | ) { 957 | let order_data = run_common_checkout_tests( 958 | amount, 959 | merchant_result, 960 | order_acc_pubkey, 961 | seller_account_pubkey, 962 | mint_keypair, 963 | ) 964 | .await; 965 | 966 | let data_string = match data { 967 | None => String::from("{}"), 968 | Some(value) => value, 969 | }; 970 | assert_eq!(order_id, order_data.order_id); 971 | assert_eq!(secret, order_data.secret); 972 | assert_eq!(data_string, order_data.data); 973 | } 974 | 975 | async fn run_chain_checkout_tests( 976 | amount: u64, 977 | order_items: &OrderItems, 978 | data: Option, 979 | merchant_result: &mut MerchantResult, 980 | order_acc_pubkey: &Pubkey, 981 | seller_account_pubkey: &Pubkey, 982 | mint_keypair: &Keypair, 983 | ) { 984 | // test contents of order account 985 | let order_data = run_common_checkout_tests( 986 | amount, 987 | merchant_result, 988 | order_acc_pubkey, 989 | seller_account_pubkey, 990 | mint_keypair, 991 | ) 992 | .await; 993 | match data { 994 | None => { 995 | assert_eq!(json!({ PAID: order_items }).to_string(), order_data.data); 996 | } 997 | Some(value) => { 998 | let json_data: Value = match serde_json::from_str(&value) { 999 | Err(error) => panic!("Problem: {:?}", error), 1000 | Ok(data) => data, 1001 | }; 1002 | assert_eq!( 1003 | json!({ INITIAL: json_data, PAID: order_items }).to_string(), 1004 | order_data.data 1005 | ); 1006 | } 1007 | } 1008 | } 1009 | 1010 | #[tokio::test] 1011 | async fn test_chain_checkout() { 1012 | let mint_keypair = Keypair::new(); 1013 | let amount: u64 = 2000000000; 1014 | 1015 | let mut order_items: OrderItems = BTreeMap::new(); 1016 | order_items.insert("1".to_string(), 1); 1017 | order_items.insert("3".to_string(), 1); 1018 | 1019 | let merchant_data = format!( 1020 | r#"{{ 1021 | "1": {{"price": 2000000, "mint": "{mint_key}"}}, 1022 | "2": {{"price": 3000000, "mint": "{mint_key}"}}, 1023 | "3": {{"price": 4000000, "mint": "{mint_key}"}}, 1024 | "4": {{"price": 4000000, "mint": "{mint_key}"}}, 1025 | "5": {{"price": 4000000, "mint": "{mint_key}"}} 1026 | }}"#, 1027 | mint_key = mint_keypair.pubkey() 1028 | ); 1029 | 1030 | let mut merchant_result = create_merchant_account( 1031 | Some("chain".to_string()), 1032 | Option::None, 1033 | Option::None, 1034 | Some(merchant_data), 1035 | ) 1036 | .await; 1037 | let (order_acc_pubkey, seller_account_pubkey) = create_order_chain_checkout( 1038 | amount, 1039 | &order_items, 1040 | Option::None, 1041 | &mut merchant_result, 1042 | &mint_keypair, 1043 | ) 1044 | .await; 1045 | 1046 | run_chain_checkout_tests( 1047 | amount, 1048 | &order_items, 1049 | Option::None, 1050 | &mut merchant_result, 1051 | &order_acc_pubkey, 1052 | &seller_account_pubkey, 1053 | &mint_keypair, 1054 | ) 1055 | .await; 1056 | } 1057 | 1058 | #[tokio::test] 1059 | async fn test_chain_checkout_with_data() { 1060 | let mint_keypair = Keypair::new(); 1061 | let amount: u64 = 2000000000; 1062 | 1063 | let mut order_items: OrderItems = BTreeMap::new(); 1064 | order_items.insert("1".to_string(), 1); 1065 | 1066 | let merchant_data = format!( 1067 | r#"{{ 1068 | "1": {{"price": 2000000, "mint": "{mint_key}"}}, 1069 | "2": {{"price": 3000000, "mint": "{mint_key}"}} 1070 | }}"#, 1071 | mint_key = mint_keypair.pubkey() 1072 | ); 1073 | 1074 | let mut merchant_result = create_merchant_account( 1075 | Some("chain2".to_string()), 1076 | Option::None, 1077 | Option::None, 1078 | Some(merchant_data), 1079 | ) 1080 | .await; 1081 | let (order_acc_pubkey, seller_account_pubkey) = create_order_chain_checkout( 1082 | amount, 1083 | &order_items, 1084 | Some(String::from(r#"{"foo": "bar"}"#)), 1085 | &mut merchant_result, 1086 | &mint_keypair, 1087 | ) 1088 | .await; 1089 | 1090 | run_chain_checkout_tests( 1091 | amount, 1092 | &order_items, 1093 | Some(String::from(r#"{"foo": "bar"}"#)), 1094 | &mut merchant_result, 1095 | &order_acc_pubkey, 1096 | &seller_account_pubkey, 1097 | &mint_keypair, 1098 | ) 1099 | .await; 1100 | } 1101 | 1102 | async fn chain_checkout_failing_test_helper( 1103 | order_item_id: u8, 1104 | paid_amount: u64, 1105 | input_mint: &Keypair, 1106 | registered_item_id: u8, 1107 | expected_amount: u64, 1108 | registered_mint: &Keypair, 1109 | expected_error: InstructionError, 1110 | ) -> bool { 1111 | let mut order_items: OrderItems = BTreeMap::new(); 1112 | order_items.insert(format!("{}", order_item_id), 1); 1113 | 1114 | let mut merchant_data = String::from("5"); 1115 | 1116 | if registered_item_id != 0 { 1117 | merchant_data = format!( 1118 | r#"{{"{registered_item_id}": {{"price": {expected_amount}, "mint": "{mint_key}"}}}}"#, 1119 | registered_item_id = registered_item_id, 1120 | expected_amount = expected_amount, 1121 | mint_key = registered_mint.pubkey() 1122 | ); 1123 | } 1124 | 1125 | let mut merchant_result = create_merchant_account( 1126 | Some("test".to_string()), 1127 | Option::None, 1128 | Option::None, 1129 | Some(merchant_data), 1130 | ) 1131 | .await; 1132 | 1133 | match create_chain_checkout_transaction( 1134 | paid_amount, 1135 | &order_items, 1136 | Option::None, 1137 | &mut merchant_result, 1138 | &input_mint, 1139 | ) 1140 | .await 1141 | { 1142 | Err(error) => { 1143 | assert_eq!( 1144 | error.unwrap(), 1145 | TransactionError::InstructionError(0, expected_error) 1146 | ); 1147 | } 1148 | Ok(_value) => panic!("Oo... we expect an error"), 1149 | }; 1150 | 1151 | true 1152 | } 1153 | 1154 | #[tokio::test] 1155 | async fn test_chain_checkout_failure() { 1156 | let mint_a = Keypair::new(); 1157 | let mint_b = Keypair::new(); 1158 | 1159 | // insufficient funds 1160 | assert!( 1161 | chain_checkout_failing_test_helper( 1162 | 1, // id of item being ordered 1163 | 20, // amount to pay 1164 | &mint_a, // mint being used for payment 1165 | 1, // registered item id 1166 | 30, // expected amount 1167 | &mint_a, // expected mint 1168 | InstructionError::InsufficientFunds 1169 | ) 1170 | .await 1171 | ); 1172 | 1173 | // wrong item id in order 1174 | assert!( 1175 | chain_checkout_failing_test_helper( 1176 | 7, // id of item being ordered 1177 | 20, // amount to pay 1178 | &mint_a, // mint being used for payment 1179 | 1, // registered item id 1180 | 30, // expected amount 1181 | &mint_a, // expected mint 1182 | InstructionError::Custom(PaymentProcessorError::InvalidOrderData as u32) 1183 | ) 1184 | .await 1185 | ); 1186 | 1187 | // wrong mint in order 1188 | assert!( 1189 | chain_checkout_failing_test_helper( 1190 | 1, // id of item being ordered 1191 | 20, // amount to pay 1192 | &mint_a, // mint being used for payment 1193 | 1, // registered item id 1194 | 20, // expected amount 1195 | &mint_b, // expected mint 1196 | InstructionError::Custom(PaymentProcessorError::WrongMint as u32) 1197 | ) 1198 | .await 1199 | ); 1200 | 1201 | // invalid merchant data 1202 | assert!( 1203 | chain_checkout_failing_test_helper( 1204 | 1, // id of item being ordered 1205 | 20, // amount to pay 1206 | &mint_a, // mint being used for payment 1207 | 0, // registered item id 1208 | 20, // expected amount 1209 | &mint_a, // expected mint 1210 | InstructionError::Custom(PaymentProcessorError::InvalidMerchantData as u32) 1211 | ) 1212 | .await 1213 | ); 1214 | } 1215 | 1216 | #[tokio::test] 1217 | async fn test_express_checkout() { 1218 | let amount: u64 = 2000000000; 1219 | let order_id = String::from("1337"); 1220 | let secret = String::from("hunter2"); 1221 | let mut merchant_result = 1222 | create_merchant_account(Option::None, Option::None, Option::None, Option::None).await; 1223 | let mint_keypair = Keypair::new(); 1224 | let (order_acc_pubkey, seller_account_pubkey) = create_order_express_checkout( 1225 | amount, 1226 | &order_id, 1227 | &secret, 1228 | Option::None, 1229 | &mut merchant_result, 1230 | &mint_keypair, 1231 | ) 1232 | .await; 1233 | 1234 | run_checkout_tests( 1235 | amount, 1236 | order_id, 1237 | secret, 1238 | Option::None, 1239 | &mut merchant_result, 1240 | &order_acc_pubkey, 1241 | &seller_account_pubkey, 1242 | &mint_keypair, 1243 | ) 1244 | .await; 1245 | } 1246 | 1247 | #[tokio::test] 1248 | /// test checkout with all merchant options 1249 | async fn test_express_checkout_with_all_options() { 1250 | let sponsor_pk = Pubkey::new_unique(); 1251 | let amount: u64 = 2000000000; 1252 | let order_id = String::from("123-SQT-MX"); 1253 | let secret = String::from("supersecret"); 1254 | let mut merchant_result = create_merchant_account( 1255 | Some(String::from("Oo")), 1256 | Some(123456), 1257 | Some(&sponsor_pk), 1258 | Some(String::from(r#"{"foo": "bar"}"#)), 1259 | ) 1260 | .await; 1261 | let mint_keypair = Keypair::new(); 1262 | let (order_acc_pubkey, seller_account_pubkey) = create_order_express_checkout( 1263 | amount, 1264 | &order_id, 1265 | &secret, 1266 | Some(String::from(r#"{"a": "b"}"#)), 1267 | &mut merchant_result, 1268 | &mint_keypair, 1269 | ) 1270 | .await; 1271 | run_checkout_tests( 1272 | amount, 1273 | order_id, 1274 | secret, 1275 | Some(String::from(r#"{"a": "b"}"#)), 1276 | &mut merchant_result, 1277 | &order_acc_pubkey, 1278 | &seller_account_pubkey, 1279 | &mint_keypair, 1280 | ) 1281 | .await; 1282 | } 1283 | 1284 | async fn run_order_token_account_refund_tests( 1285 | order_payment_token_acc: &Option, 1286 | account_to_receive_sol_refund_before: &Option, 1287 | account_to_receive_sol_refund_after: &Option, 1288 | previous_order_account: &Option, 1289 | ) { 1290 | // order token account is closed 1291 | assert!(order_payment_token_acc.is_none()); 1292 | let order_account_rent = match previous_order_account { 1293 | None => 0, 1294 | Some(account) => account.lamports, 1295 | }; 1296 | match account_to_receive_sol_refund_before { 1297 | None => panic!("Oo"), 1298 | Some(account_before) => match account_to_receive_sol_refund_after { 1299 | None => panic!("Oo"), 1300 | Some(account_after) => { 1301 | // the before balance has increased by the rent amount of both token and order account 1302 | assert_eq!( 1303 | account_before.lamports, 1304 | account_after.lamports 1305 | - (Rent::default().minimum_balance(TokenAccount::LEN) 1306 | + order_account_rent) 1307 | ); 1308 | } 1309 | }, 1310 | }; 1311 | } 1312 | 1313 | async fn withdraw_helper( 1314 | amount: u64, 1315 | close_order_account: bool, 1316 | ) -> ( 1317 | BanksClient, 1318 | Option, 1319 | Pubkey, 1320 | Pubkey, 1321 | Option, 1322 | Option, 1323 | ) { 1324 | let mut merchant_result = 1325 | create_merchant_account(Option::None, Option::None, Option::None, Option::None).await; 1326 | let merchant_token_keypair = Keypair::new(); 1327 | let order_id = String::from("PD17CUSZ75"); 1328 | let secret = String::from("i love oov"); 1329 | let mint_keypair = Keypair::new(); 1330 | let (order_acc_pubkey, _seller_account_pubkey) = create_order_express_checkout( 1331 | amount, 1332 | &order_id, 1333 | &secret, 1334 | Option::None, 1335 | &mut merchant_result, 1336 | &mint_keypair, 1337 | ) 1338 | .await; 1339 | let program_id = merchant_result.0; 1340 | let merchant_account_pubkey = merchant_result.1; 1341 | let mut banks_client = merchant_result.2; 1342 | let payer = merchant_result.3; 1343 | let recent_blockhash = merchant_result.4; 1344 | let (pda, _bump_seed) = Pubkey::find_program_address(&[PDA_SEED], &program_id); 1345 | 1346 | // create and initialize merchant token account 1347 | assert_matches!( 1348 | banks_client 1349 | .process_transaction(create_token_account_transaction( 1350 | &payer, 1351 | &mint_keypair, 1352 | recent_blockhash, 1353 | &merchant_token_keypair, 1354 | &payer.pubkey(), 1355 | 0, 1356 | )) 1357 | .await, 1358 | Ok(()) 1359 | ); 1360 | let (order_payment_token_acc_pubkey, _bump_seed) = Pubkey::find_program_address( 1361 | &[ 1362 | &order_acc_pubkey.to_bytes(), 1363 | &spl_token::id().to_bytes(), 1364 | &mint_keypair.pubkey().to_bytes(), 1365 | ], 1366 | &program_id, 1367 | ); 1368 | 1369 | let account_to_receive_sol_refund_pubkey = Pubkey::from_str(PROGRAM_OWNER).unwrap(); 1370 | let account_to_receive_sol_refund_before = banks_client 1371 | .get_account(account_to_receive_sol_refund_pubkey) 1372 | .await 1373 | .unwrap(); 1374 | 1375 | let previous_order_account = banks_client.get_account(order_acc_pubkey).await; 1376 | let previous_order_account = match previous_order_account { 1377 | Err(error) => panic!("Problem: {:?}", error), 1378 | Ok(value) => value, 1379 | }; 1380 | 1381 | // call withdraw ix 1382 | let mut transaction = Transaction::new_with_payer( 1383 | &[withdraw( 1384 | program_id, 1385 | payer.pubkey(), 1386 | order_acc_pubkey, 1387 | merchant_account_pubkey, 1388 | order_payment_token_acc_pubkey, 1389 | merchant_token_keypair.pubkey(), 1390 | account_to_receive_sol_refund_pubkey, 1391 | pda, 1392 | Option::None, 1393 | close_order_account, 1394 | )], 1395 | Some(&payer.pubkey()), 1396 | ); 1397 | transaction.sign(&[&payer], recent_blockhash); 1398 | assert_matches!(banks_client.process_transaction(transaction).await, Ok(())); 1399 | 1400 | // test contents of merchant token account 1401 | let merchant_token_account = banks_client 1402 | .get_account(merchant_token_keypair.pubkey()) 1403 | .await; 1404 | let merchant_account_data = match merchant_token_account { 1405 | Ok(data) => match data { 1406 | None => panic!("Oo"), 1407 | Some(value) => match spl_token::state::Account::unpack(&value.data) { 1408 | Ok(data) => data, 1409 | Err(error) => panic!("Problem: {:?}", error), 1410 | }, 1411 | }, 1412 | Err(error) => panic!("Problem: {:?}", error), 1413 | }; 1414 | assert_eq!(amount, merchant_account_data.amount); 1415 | 1416 | let order_account = banks_client.get_account(order_acc_pubkey).await; 1417 | let order_account = match order_account { 1418 | Err(error) => panic!("Problem: {:?}", error), 1419 | Ok(value) => value, 1420 | }; 1421 | 1422 | ( 1423 | banks_client, 1424 | order_account, 1425 | order_payment_token_acc_pubkey, 1426 | account_to_receive_sol_refund_pubkey, 1427 | account_to_receive_sol_refund_before, 1428 | previous_order_account, 1429 | ) 1430 | } 1431 | 1432 | #[tokio::test] 1433 | async fn test_withdraw() { 1434 | let amount: u64 = 1234567890; 1435 | let ( 1436 | mut banks_client, 1437 | order_account, 1438 | order_payment_token_acc_pubkey, 1439 | account_to_receive_sol_refund_pubkey, 1440 | account_to_receive_sol_refund_before, 1441 | _previous_order_account, 1442 | ) = withdraw_helper(amount, false).await; 1443 | // test contents of order account 1444 | let order_data = match order_account.clone() { 1445 | None => panic!("Oo"), 1446 | Some(value) => match OrderAccount::unpack(&value.data) { 1447 | Ok(data) => data, 1448 | Err(error) => panic!("Problem: {:?}", error), 1449 | }, 1450 | }; 1451 | assert_eq!(OrderStatus::Withdrawn as u8, order_data.status); 1452 | assert_eq!(amount, order_data.expected_amount); 1453 | assert_eq!(amount, order_data.paid_amount); 1454 | // test that token account was closed and that the refund was sent to expected account 1455 | let order_payment_token_acc = banks_client 1456 | .get_account(order_payment_token_acc_pubkey) 1457 | .await 1458 | .unwrap(); 1459 | let account_to_receive_sol_refund_after = banks_client 1460 | .get_account(account_to_receive_sol_refund_pubkey) 1461 | .await 1462 | .unwrap(); 1463 | run_order_token_account_refund_tests( 1464 | &order_payment_token_acc, 1465 | &account_to_receive_sol_refund_before, 1466 | &account_to_receive_sol_refund_after, 1467 | &Option::None, 1468 | ) 1469 | .await; 1470 | } 1471 | 1472 | #[tokio::test] 1473 | async fn test_withdraw_close_order_account() { 1474 | let amount: u64 = 10001; 1475 | let ( 1476 | mut banks_client, 1477 | order_account, 1478 | order_payment_token_acc_pubkey, 1479 | account_to_receive_sol_refund_pubkey, 1480 | account_to_receive_sol_refund_before, 1481 | previous_order_account, 1482 | ) = withdraw_helper(amount, true).await; 1483 | // test closure of order account 1484 | assert!(order_account.is_none()); 1485 | // test that accounts were closed and that refunds sent to expected account 1486 | let order_payment_token_acc = banks_client 1487 | .get_account(order_payment_token_acc_pubkey) 1488 | .await 1489 | .unwrap(); 1490 | let account_to_receive_sol_refund_after = banks_client 1491 | .get_account(account_to_receive_sol_refund_pubkey) 1492 | .await 1493 | .unwrap(); 1494 | run_order_token_account_refund_tests( 1495 | &order_payment_token_acc, 1496 | &account_to_receive_sol_refund_before, 1497 | &account_to_receive_sol_refund_after, 1498 | &previous_order_account, 1499 | ) 1500 | .await; 1501 | } 1502 | 1503 | async fn run_subscribe_tests( 1504 | amount: u64, 1505 | package_name: &str, 1506 | merchant_data: &str, 1507 | mint_keypair: &Keypair, 1508 | ) -> ( 1509 | Result<(), TransportError>, 1510 | Option<(SubscriptionAccount, MerchantResult, Pubkey, Pubkey)>, 1511 | ) { 1512 | let mut merchant_result = create_merchant_account( 1513 | Some(String::from("subscription test")), 1514 | Option::None, 1515 | Option::None, 1516 | Some(String::from(merchant_data)), 1517 | ) 1518 | .await; 1519 | 1520 | let (subscription, _bump_seed) = Pubkey::find_program_address( 1521 | &[ 1522 | &merchant_result.3.pubkey().to_bytes(), // payer 1523 | &merchant_result.1.to_bytes(), // merchant 1524 | &package_name.as_bytes(), 1525 | ], 1526 | &merchant_result.0, // program id 1527 | ); 1528 | 1529 | let order_data = format!(r#"{{"subscription": "{}"}}"#, subscription.to_string()); 1530 | 1531 | let (order_acc_pubkey, _seller_account_pubkey) = create_order_express_checkout( 1532 | amount, 1533 | &String::from(package_name), 1534 | &String::from(""), 1535 | Some(order_data), 1536 | &mut merchant_result, 1537 | &mint_keypair, 1538 | ) 1539 | .await; 1540 | 1541 | let program_id = merchant_result.0; 1542 | let merchant_account_pubkey = merchant_result.1; 1543 | let payer = &merchant_result.3; 1544 | let recent_blockhash = merchant_result.4; 1545 | 1546 | // call subscribe ix 1547 | let mut transaction = Transaction::new_with_payer( 1548 | &[subscribe( 1549 | program_id, 1550 | payer.pubkey(), 1551 | subscription, 1552 | merchant_account_pubkey, 1553 | order_acc_pubkey, 1554 | String::from(package_name), 1555 | Option::None, 1556 | )], 1557 | Some(&payer.pubkey()), 1558 | ); 1559 | transaction.sign(&[payer], recent_blockhash); 1560 | 1561 | let result = merchant_result.2.process_transaction(transaction).await; 1562 | 1563 | if result.is_ok() { 1564 | // test contents of subscription token account 1565 | let subscription_account = &merchant_result.2.get_account(subscription).await; 1566 | let subscription_data = match subscription_account { 1567 | Ok(data) => match data { 1568 | None => panic!("Oo"), 1569 | Some(value) => match SubscriptionAccount::unpack(&value.data) { 1570 | Ok(data) => data, 1571 | Err(error) => panic!("Problem: {:?}", error), 1572 | }, 1573 | }, 1574 | Err(error) => panic!("Problem: {:?}", error), 1575 | }; 1576 | assert_eq!( 1577 | (SubscriptionStatus::Initialized as u8), 1578 | subscription_data.status 1579 | ); 1580 | assert_eq!(String::from(package_name), subscription_data.name); 1581 | assert_eq!( 1582 | payer.pubkey(), 1583 | Pubkey::new_from_array(subscription_data.owner) 1584 | ); 1585 | assert_eq!( 1586 | merchant_account_pubkey, 1587 | Pubkey::new_from_array(subscription_data.merchant) 1588 | ); 1589 | assert_eq!(String::from("{}"), subscription_data.data); 1590 | 1591 | return ( 1592 | result, 1593 | Some(( 1594 | subscription_data, 1595 | merchant_result, 1596 | order_acc_pubkey, 1597 | subscription, 1598 | )), 1599 | ); 1600 | } 1601 | 1602 | (result, Option::None) 1603 | } 1604 | 1605 | #[tokio::test] 1606 | async fn test_subscribe() { 1607 | let mint_keypair = Keypair::new(); 1608 | let packages = format!( 1609 | r#"{{"packages":[{{"name":"basic","price":1000000,"duration":720,"mint":"{mint}"}},{{"name":"annual","price":11000000,"duration":262800,"mint":"{mint}"}}]}}"#, 1610 | mint = mint_keypair.pubkey().to_string() 1611 | ); 1612 | assert!( 1613 | (run_subscribe_tests(1000000, "basic", &packages, &mint_keypair).await) 1614 | .0 1615 | .is_ok() 1616 | ); 1617 | } 1618 | 1619 | #[tokio::test] 1620 | /// test what happens when there are 0 packages 1621 | async fn test_subscribe_no_packages() { 1622 | let mint_keypair = Keypair::new(); 1623 | let packages = r#"{"packages":[]}"#; 1624 | assert!( 1625 | (run_subscribe_tests(1337, "basic", packages, &mint_keypair).await) 1626 | .0 1627 | .is_err() 1628 | ); 1629 | } 1630 | 1631 | #[tokio::test] 1632 | /// test what happens when there are duplicate packages 1633 | async fn test_subscribe_duplicate_packages() { 1634 | let mint_keypair = Keypair::new(); 1635 | let packages = format!( 1636 | r#"{{"packages":[{{"name":"a","price":100,"duration":720,"mint":"{mint}"}},{{"name":"a","price":222,"duration":262800,"mint":"{mint}"}}]}}"#, 1637 | mint = mint_keypair.pubkey().to_string() 1638 | ); 1639 | 1640 | let result = run_subscribe_tests(100, "a", &packages, &mint_keypair).await; 1641 | assert!(result.0.is_ok()); 1642 | 1643 | let _ = match result.1 { 1644 | None => (), 1645 | Some(value) => { 1646 | let subscription_account = value.0; 1647 | // use the duration of the first package in the array to check 1648 | // that the subscription was created using the first array element 1649 | assert_eq!( 1650 | 720, 1651 | subscription_account.period_end - subscription_account.period_start 1652 | ); 1653 | () 1654 | } 1655 | }; 1656 | } 1657 | 1658 | #[tokio::test] 1659 | /// test what happens when the package is not found 1660 | async fn test_subscribe_package_not_found() { 1661 | let mint_keypair = Keypair::new(); 1662 | let packages = format!( 1663 | r#"{{"packages":[{{"name":"a","price":100,"duration":720,"mint":"{mint}"}}]}}"#, 1664 | mint = mint_keypair.pubkey().to_string() 1665 | ); 1666 | assert!( 1667 | (run_subscribe_tests(100, "zz", &packages, &mint_keypair).await) 1668 | .0 1669 | .is_err() 1670 | ); 1671 | } 1672 | 1673 | #[tokio::test] 1674 | /// test what happens when there is no packages object in the JSON 1675 | async fn test_subscribe_no_packages_json() { 1676 | let mint_keypair = Keypair::new(); 1677 | assert!( 1678 | (run_subscribe_tests(250, "package", r#"{}"#, &mint_keypair).await) 1679 | .0 1680 | .is_err() 1681 | ); 1682 | } 1683 | 1684 | #[tokio::test] 1685 | /// test what happens when there is no valid JSON 1686 | async fn test_subscribe_no_json() { 1687 | let mint_keypair = Keypair::new(); 1688 | assert!( 1689 | (run_subscribe_tests(250, "package", "what is?", &mint_keypair).await) 1690 | .0 1691 | .is_err() 1692 | ); 1693 | } 1694 | 1695 | #[tokio::test] 1696 | /// test what happens when the amount paid is insufficient 1697 | async fn test_subscribe_not_enough_paid() { 1698 | let mint_keypair = Keypair::new(); 1699 | let packages = format!( 1700 | r#"{{"packages":[{{"name":"basic","price":100,"duration":720,"mint":"{mint}"}}]}}"#, 1701 | mint = mint_keypair.pubkey().to_string() 1702 | ); 1703 | assert!( 1704 | (run_subscribe_tests(10, "Netflix-basic", &packages, &mint_keypair).await) 1705 | .0 1706 | .is_err() 1707 | ); 1708 | } 1709 | 1710 | #[tokio::test] 1711 | async fn test_subscription_renewal() { 1712 | let mint_keypair = Keypair::new(); 1713 | let name = "short"; 1714 | // create a package that lasts only 1 second 1715 | let packages = format!( 1716 | r#"{{"packages":[{{"name":"{name}","price":999999,"duration":1,"mint":"{mint}"}}]}}"#, 1717 | mint = mint_keypair.pubkey().to_string(), 1718 | name = name 1719 | ); 1720 | let result = run_subscribe_tests(1000000, name, &packages, &mint_keypair).await; 1721 | assert!(result.0.is_ok()); 1722 | let subscribe_result = result.1; 1723 | let _ = match subscribe_result { 1724 | None => (), 1725 | Some(mut subscribe_result) => { 1726 | let subscription_account = subscribe_result.0; 1727 | let subscription = subscribe_result.3; // the subscription pubkey 1728 | 1729 | let order_data = format!(r#"{{"subscription": "{}"}}"#, subscription.to_string()); 1730 | 1731 | let (order_acc_pubkey, _seller_account_pubkey) = create_order_express_checkout( 1732 | 999999 * 600, 1733 | &format!("{name}", name = name), 1734 | &String::from(""), 1735 | Some(order_data), 1736 | &mut subscribe_result.1, 1737 | &mint_keypair, 1738 | ) 1739 | .await; 1740 | 1741 | // call subscription ix 1742 | let mut transaction = Transaction::new_with_payer( 1743 | &[renew_subscription( 1744 | subscribe_result.1 .0, // program_id, 1745 | subscribe_result.1 .3.pubkey(), // payer, 1746 | subscription, 1747 | Pubkey::new_from_array(subscription_account.merchant), 1748 | order_acc_pubkey, 1749 | 600, 1750 | )], 1751 | Some(&subscribe_result.1 .3.pubkey()), 1752 | ); 1753 | transaction.sign(&[&subscribe_result.1 .3], subscribe_result.1 .4); 1754 | assert_matches!( 1755 | subscribe_result.1 .2.process_transaction(transaction).await, 1756 | Ok(()) 1757 | ); 1758 | 1759 | // assert that period end has been updated 1760 | let subscription_account2 = subscribe_result.1 .2.get_account(subscription).await; 1761 | let subscription_account2 = match subscription_account2 { 1762 | Ok(data) => match data { 1763 | None => panic!("Oo"), 1764 | Some(value) => match SubscriptionAccount::unpack(&value.data) { 1765 | Ok(data) => data, 1766 | Err(error) => panic!("Problem: {:?}", error), 1767 | }, 1768 | }, 1769 | Err(error) => panic!("Problem: {:?}", error), 1770 | }; 1771 | assert_eq!( 1772 | // the new period_end is equal to the old period_end + (1 * 600) 1773 | subscription_account.period_end + 600, 1774 | subscription_account2.period_end 1775 | ); 1776 | 1777 | return (); 1778 | } 1779 | }; 1780 | } 1781 | 1782 | async fn run_subscription_withdrawal_tests( 1783 | name: &str, 1784 | packages: &str, 1785 | mint_keypair: &Keypair, 1786 | error_expected: bool, 1787 | ) { 1788 | // create the subscription 1789 | let result = run_subscribe_tests(1000000, name, &packages, &mint_keypair).await; 1790 | assert!(result.0.is_ok()); 1791 | let subscribe_result = result.1; 1792 | let _ = match subscribe_result { 1793 | None => (), 1794 | Some(mut subscribe_result) => { 1795 | let subscription = subscribe_result.3; // the subscription pubkey 1796 | let order_acc_pubkey = subscribe_result.2; 1797 | let merchant_token_keypair = Keypair::new(); 1798 | let (pda, _bump_seed) = 1799 | Pubkey::find_program_address(&[PDA_SEED], &subscribe_result.1 .0); 1800 | 1801 | // create and initialize merchant token account 1802 | assert_matches!( 1803 | subscribe_result 1804 | .1 1805 | .2 1806 | .process_transaction(create_token_account_transaction( 1807 | &subscribe_result.1 .3, 1808 | &mint_keypair, 1809 | subscribe_result.1 .4, // recent_blockhash 1810 | &merchant_token_keypair, 1811 | &subscribe_result.1 .3.pubkey(), // payer, 1812 | 0, 1813 | )) 1814 | .await, 1815 | Ok(()) 1816 | ); 1817 | let (order_payment_token_acc_pubkey, _bump_seed) = Pubkey::find_program_address( 1818 | &[ 1819 | &order_acc_pubkey.to_bytes(), 1820 | &spl_token::id().to_bytes(), 1821 | &mint_keypair.pubkey().to_bytes(), 1822 | ], 1823 | &subscribe_result.1 .0, // program_id 1824 | ); 1825 | 1826 | // call withdraw ix 1827 | let mut transaction = Transaction::new_with_payer( 1828 | &[withdraw( 1829 | subscribe_result.1 .0, // program_id 1830 | subscribe_result.1 .3.pubkey(), // payer, 1831 | order_acc_pubkey, 1832 | subscribe_result.1 .1, // the merchant pubkey 1833 | order_payment_token_acc_pubkey, 1834 | merchant_token_keypair.pubkey(), 1835 | Pubkey::from_str(PROGRAM_OWNER).unwrap(), 1836 | pda, 1837 | Some(subscription), 1838 | false, 1839 | )], 1840 | Some(&subscribe_result.1 .3.pubkey()), 1841 | ); 1842 | transaction.sign(&[&subscribe_result.1 .3], subscribe_result.1 .4); 1843 | 1844 | if error_expected { 1845 | assert!(subscribe_result 1846 | .1 1847 | .2 1848 | .process_transaction(transaction) 1849 | .await 1850 | .is_err()); 1851 | } else { 1852 | assert!(subscribe_result 1853 | .1 1854 | .2 1855 | .process_transaction(transaction) 1856 | .await 1857 | .is_ok()); 1858 | } 1859 | 1860 | return (); 1861 | } 1862 | }; 1863 | } 1864 | 1865 | #[tokio::test] 1866 | async fn test_withdraw_during_trial() { 1867 | let mint_keypair = Keypair::new(); 1868 | let name = "trialFirst"; 1869 | // create a package that has a short trial period 1870 | let packages = format!( 1871 | r#"{{"packages":[{{"name":"{name}","price":99,"trial":0,"duration":604800,"mint":"{mint}"}}]}}"#, 1872 | mint = mint_keypair.pubkey().to_string(), 1873 | name = name 1874 | ); 1875 | // withdraw goes okay 1876 | run_subscription_withdrawal_tests(name, &packages, &mint_keypair, false).await; 1877 | } 1878 | 1879 | #[tokio::test] 1880 | async fn test_cannot_withdraw_during_trial() { 1881 | let mint_keypair = Keypair::new(); 1882 | let name = "try1st"; 1883 | // create a package that has a week long trial period 1884 | let packages = format!( 1885 | r#"{{"packages":[{{"name":"{name}","price":99,"trial":604800,"duration":604800,"mint":"{mint}"}}]}}"#, 1886 | mint = mint_keypair.pubkey().to_string(), 1887 | name = name 1888 | ); 1889 | // withdrawal errors out as you cant withdraw during trial 1890 | run_subscription_withdrawal_tests(name, &packages, &mint_keypair, true).await; 1891 | } 1892 | 1893 | async fn run_subscription_cancel_tests( 1894 | amount: u64, 1895 | name: &str, 1896 | packages: &str, 1897 | mint_keypair: &Keypair, 1898 | ) -> Option<( 1899 | SubscriptionAccount, 1900 | Option, 1901 | Option, 1902 | spl_token::state::Account, 1903 | SubscriptionAccount, 1904 | Option, 1905 | Option, 1906 | Option, 1907 | )> { 1908 | // create the subscription 1909 | let result = run_subscribe_tests(amount, name, &packages, &mint_keypair).await; 1910 | assert!(result.0.is_ok()); 1911 | let subscribe_result = result.1; 1912 | match subscribe_result { 1913 | None => Option::None, 1914 | Some(mut subscribe_result) => { 1915 | let subscription = subscribe_result.3; // the subscription pubkey 1916 | 1917 | let previous_subscription_account = 1918 | subscribe_result.1 .2.get_account(subscription).await; 1919 | let previous_subscription_account = match previous_subscription_account { 1920 | Ok(data) => match data { 1921 | None => panic!("Oo"), 1922 | Some(value) => match SubscriptionAccount::unpack(&value.data) { 1923 | Ok(data) => data, 1924 | Err(error) => panic!("Problem: {:?}", error), 1925 | }, 1926 | }, 1927 | Err(error) => panic!("Problem: {:?}", error), 1928 | }; 1929 | 1930 | let order_acc_pubkey = subscribe_result.2; 1931 | let previous_order_account = 1932 | subscribe_result.1 .2.get_account(order_acc_pubkey).await; 1933 | let previous_order_account = match previous_order_account { 1934 | Err(error) => panic!("Problem: {:?}", error), 1935 | Ok(value) => value, 1936 | }; 1937 | 1938 | let refund_token_acc_keypair = Keypair::new(); 1939 | let (pda, _bump_seed) = 1940 | Pubkey::find_program_address(&[PDA_SEED], &subscribe_result.1 .0); 1941 | 1942 | // create and initialize refund token account 1943 | assert_matches!( 1944 | subscribe_result 1945 | .1 1946 | .2 1947 | .process_transaction(create_token_account_transaction( 1948 | &subscribe_result.1 .3, 1949 | &mint_keypair, 1950 | subscribe_result.1 .4, // recent_blockhash 1951 | &refund_token_acc_keypair, 1952 | &subscribe_result.1 .3.pubkey(), // payer, 1953 | 0, 1954 | )) 1955 | .await, 1956 | Ok(()) 1957 | ); 1958 | let (order_token_acc_pubkey, _bump_seed) = Pubkey::find_program_address( 1959 | &[ 1960 | &order_acc_pubkey.to_bytes(), 1961 | &spl_token::id().to_bytes(), 1962 | &mint_keypair.pubkey().to_bytes(), 1963 | ], 1964 | &subscribe_result.1 .0, // program_id 1965 | ); 1966 | 1967 | let account_to_receive_sol_refund_pubkey = Pubkey::from_str(PROGRAM_OWNER).unwrap(); 1968 | let account_to_receive_sol_refund_before = subscribe_result 1969 | .1 1970 | .2 1971 | .get_account(account_to_receive_sol_refund_pubkey) 1972 | .await 1973 | .unwrap(); 1974 | 1975 | // call cancel ix 1976 | let mut transaction = Transaction::new_with_payer( 1977 | &[cancel_subscription( 1978 | subscribe_result.1 .0, // program_id 1979 | subscribe_result.1 .3.pubkey(), // payer, 1980 | subscription, 1981 | subscribe_result.1 .1, // the merchant pubkey 1982 | order_acc_pubkey, 1983 | order_token_acc_pubkey, 1984 | refund_token_acc_keypair.pubkey(), 1985 | account_to_receive_sol_refund_pubkey, 1986 | pda, 1987 | )], 1988 | Some(&subscribe_result.1 .3.pubkey()), 1989 | ); 1990 | transaction.sign(&[&subscribe_result.1 .3], subscribe_result.1 .4); 1991 | 1992 | let _cancel_result = subscribe_result.1 .2.process_transaction(transaction).await; 1993 | 1994 | let subscription_account = subscribe_result.1 .2.get_account(subscription).await; 1995 | let subscription_account = match subscription_account { 1996 | Ok(data) => match data { 1997 | None => panic!("Oo"), 1998 | Some(value) => match SubscriptionAccount::unpack(&value.data) { 1999 | Ok(data) => data, 2000 | Err(error) => panic!("Problem: {:?}", error), 2001 | }, 2002 | }, 2003 | Err(error) => panic!("Problem: {:?}", error), 2004 | }; 2005 | let order_account = subscribe_result.1 .2.get_account(order_acc_pubkey).await; 2006 | let order_account = match order_account { 2007 | Ok(value) => value, 2008 | Err(error) => panic!("Problem: {:?}", error), 2009 | }; 2010 | let order_token_account = subscribe_result 2011 | .1 2012 | .2 2013 | .get_account(order_token_acc_pubkey) 2014 | .await 2015 | .unwrap(); 2016 | let refund_token_account = subscribe_result 2017 | .1 2018 | .2 2019 | .get_account(refund_token_acc_keypair.pubkey()) 2020 | .await; 2021 | let refund_token_account = match refund_token_account { 2022 | Ok(data) => match data { 2023 | None => panic!("Oo"), 2024 | Some(value) => match TokenAccount::unpack(&value.data) { 2025 | Ok(data) => data, 2026 | Err(error) => panic!("Problem: {:?}", error), 2027 | }, 2028 | }, 2029 | Err(error) => panic!("Problem: {:?}", error), 2030 | }; 2031 | 2032 | let account_to_receive_sol_refund_after = subscribe_result 2033 | .1 2034 | .2 2035 | .get_account(account_to_receive_sol_refund_pubkey) 2036 | .await 2037 | .unwrap(); 2038 | 2039 | Some(( 2040 | subscription_account, 2041 | order_account, 2042 | order_token_account, 2043 | refund_token_account, 2044 | previous_subscription_account, 2045 | previous_order_account, 2046 | account_to_receive_sol_refund_before, 2047 | account_to_receive_sol_refund_after, 2048 | )) 2049 | } 2050 | } 2051 | } 2052 | 2053 | #[tokio::test] 2054 | async fn test_cancel_subscription_during_trial() { 2055 | let mint_keypair = Keypair::new(); 2056 | let name = "trialFirst"; 2057 | // create a package that has a short trial period 2058 | let packages = format!( 2059 | r#"{{"packages":[{{"name":"{name}","price":6699,"trial":604800,"duration":604800,"mint":"{mint}"}}]}}"#, 2060 | mint = mint_keypair.pubkey().to_string(), 2061 | name = name 2062 | ); 2063 | // cancel goes okay 2064 | let result = run_subscription_cancel_tests(6699, name, &packages, &mint_keypair) 2065 | .await 2066 | .unwrap(); 2067 | let ( 2068 | subscription_account, 2069 | order_account, 2070 | order_token_account, 2071 | refund_token_account, 2072 | previous_subscription_account, 2073 | previous_order_account, 2074 | account_to_receive_sol_refund_before, 2075 | account_to_receive_sol_refund_after, 2076 | ) = result; 2077 | // subscription was canceled 2078 | assert_eq!( 2079 | SubscriptionStatus::Initialized as u8, 2080 | previous_subscription_account.status 2081 | ); 2082 | assert_eq!( 2083 | SubscriptionStatus::Cancelled as u8, 2084 | subscription_account.status 2085 | ); 2086 | // period end has changed to an earlier time 2087 | assert!(previous_subscription_account.period_end > subscription_account.period_end); 2088 | // order account was closed 2089 | assert!(order_account.is_none()); 2090 | // amount was withdrawn 2091 | assert_eq!(6699, refund_token_account.amount); 2092 | // order token account was closed and SOL refunded 2093 | run_order_token_account_refund_tests( 2094 | &order_token_account, 2095 | &account_to_receive_sol_refund_before, 2096 | &account_to_receive_sol_refund_after, 2097 | &previous_order_account, 2098 | ) 2099 | .await; 2100 | } 2101 | 2102 | #[tokio::test] 2103 | async fn test_cancel_subscription_after_trial() { 2104 | let mint_keypair = Keypair::new(); 2105 | let name = "trialFirst"; 2106 | // create a package that has a short trial period 2107 | let packages = format!( 2108 | r#"{{"packages":[{{"name":"{name}","price":1337,"trial":0,"duration":604800,"mint":"{mint}"}}]}}"#, 2109 | mint = mint_keypair.pubkey().to_string(), 2110 | name = name 2111 | ); 2112 | // cancel goes okay but no refund 2113 | let result = run_subscription_cancel_tests(1337, name, &packages, &mint_keypair) 2114 | .await 2115 | .unwrap(); 2116 | let ( 2117 | subscription_account, 2118 | order_account, 2119 | order_token_account, 2120 | refund_token_account, 2121 | previous_subscription_account, 2122 | previous_order_account, 2123 | account_to_receive_sol_refund_before, 2124 | account_to_receive_sol_refund_after, 2125 | ) = result; 2126 | // subscription was canceled 2127 | assert_eq!( 2128 | SubscriptionStatus::Initialized as u8, 2129 | previous_subscription_account.status 2130 | ); 2131 | assert_eq!( 2132 | SubscriptionStatus::Cancelled as u8, 2133 | subscription_account.status 2134 | ); 2135 | assert_eq!( 2136 | previous_subscription_account.period_end, 2137 | subscription_account.period_end 2138 | ); 2139 | // order account was not changed 2140 | let order_account = match order_account { 2141 | None => panic!("Oo"), 2142 | Some(value) => match OrderAccount::unpack(&value.data) { 2143 | Ok(data) => data, 2144 | Err(error) => panic!("Problem: {:?}", error), 2145 | }, 2146 | }; 2147 | let previous_order_account = match previous_order_account { 2148 | None => panic!("Oo"), 2149 | Some(value) => match OrderAccount::unpack(&value.data) { 2150 | Ok(data) => data, 2151 | Err(error) => panic!("Problem: {:?}", error), 2152 | }, 2153 | }; 2154 | assert_eq!(order_account, previous_order_account); 2155 | assert_eq!(OrderStatus::Paid as u8, order_account.status); 2156 | // nothing was refunded 2157 | assert_eq!(0, refund_token_account.amount); 2158 | let order_token_account = match order_token_account { 2159 | None => panic!("Oo"), 2160 | Some(value) => match TokenAccount::unpack(&value.data) { 2161 | Ok(data) => data, 2162 | Err(error) => panic!("Problem: {:?}", error), 2163 | }, 2164 | }; 2165 | assert_eq!(order_account.paid_amount, order_token_account.amount); 2166 | match account_to_receive_sol_refund_before { 2167 | None => panic!("Oo"), 2168 | Some(account_before) => match account_to_receive_sol_refund_after { 2169 | None => panic!("Oo"), 2170 | Some(account_after) => { 2171 | assert_eq!(account_before.lamports, account_after.lamports); 2172 | } 2173 | }, 2174 | }; 2175 | } 2176 | } 2177 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod entrypoint; 3 | pub mod instruction; 4 | pub mod processor; 5 | pub mod state; 6 | pub mod utils; 7 | pub mod engine; 8 | -------------------------------------------------------------------------------- /src/processor.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | engine::cancel_subscription::process_cancel_subscription, 3 | engine::pay::process_express_checkout, engine::pay::process_chain_checkout, engine::register::process_register_merchant, 4 | engine::renew::process_renew_subscription, engine::subscribe::process_subscribe, 5 | engine::withdraw::process_withdraw_payment, instruction::PaymentProcessorInstruction, 6 | }; 7 | use borsh::BorshDeserialize; 8 | use solana_program::{ 9 | account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError, 10 | pubkey::Pubkey, 11 | }; 12 | 13 | /// Processes the instruction 14 | impl PaymentProcessorInstruction { 15 | pub fn process( 16 | program_id: &Pubkey, 17 | accounts: &[AccountInfo], 18 | instruction_data: &[u8], 19 | ) -> ProgramResult { 20 | let instruction = PaymentProcessorInstruction::try_from_slice(&instruction_data) 21 | .map_err(|_| ProgramError::InvalidInstructionData)?; 22 | match instruction { 23 | PaymentProcessorInstruction::RegisterMerchant { seed, fee, data } => { 24 | msg!("SolPayments: RegisterMerchant"); 25 | process_register_merchant(program_id, accounts, seed, fee, data) 26 | } 27 | PaymentProcessorInstruction::ExpressCheckout { 28 | amount, 29 | order_id, 30 | secret, 31 | data, 32 | } => { 33 | msg!("SolPayments: ExpressCheckout"); 34 | process_express_checkout(program_id, accounts, amount, order_id, secret, data) 35 | } 36 | PaymentProcessorInstruction::ChainCheckout { 37 | amount, 38 | order_items, 39 | data, 40 | } => { 41 | msg!("SolPayments: ChainCheckout"); 42 | process_chain_checkout(program_id, accounts, amount, order_items, data) 43 | } 44 | PaymentProcessorInstruction::Withdraw { close_order_account } => { 45 | msg!("SolPayments: Withdraw"); 46 | process_withdraw_payment(program_id, accounts, close_order_account) 47 | } 48 | PaymentProcessorInstruction::Subscribe { name, data } => { 49 | msg!("SolPayments: Subscribe"); 50 | process_subscribe(program_id, accounts, name, data) 51 | } 52 | PaymentProcessorInstruction::RenewSubscription { quantity } => { 53 | msg!("SolPayments: RenewSubscription"); 54 | process_renew_subscription(program_id, accounts, quantity) 55 | } 56 | PaymentProcessorInstruction::CancelSubscription => { 57 | msg!("SolPayments: CancelSubscription"); 58 | process_cancel_subscription(program_id, accounts) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; 2 | use solana_program::{ 3 | clock::UnixTimestamp, 4 | program_pack::{IsInitialized, Sealed}, 5 | }; 6 | use std::mem::size_of; 7 | 8 | pub type PublicKey = [u8; 32]; 9 | 10 | pub trait Serdes: Sized + BorshSerialize + BorshDeserialize { 11 | fn pack(&self, dst: &mut [u8]) { 12 | let encoded = self.try_to_vec().unwrap(); 13 | dst[..encoded.len()].copy_from_slice(&encoded); 14 | } 15 | fn unpack(src: &[u8]) -> Result { 16 | Self::try_from_slice(src) 17 | } 18 | } 19 | 20 | #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] 21 | pub enum Discriminator { 22 | Uninitialized = 0, 23 | Merchant = 10, 24 | MerchantSubscription = 11, 25 | MerchantSubscriptionWithTrial = 12, 26 | MerchantChainCheckout = 15, 27 | OrderExpressCheckout = 20, 28 | OrderChainCheckout = 21, 29 | Subscription = 30, 30 | Closed = 255, 31 | } 32 | 33 | #[derive(BorshSerialize, BorshSchema, BorshDeserialize, Debug, PartialEq)] 34 | pub struct MerchantAccount { 35 | pub discriminator: u8, 36 | pub owner: PublicKey, 37 | pub sponsor: PublicKey, 38 | /// represents the fee (in SOL lamports) that will be charged for transactions 39 | pub fee: u64, 40 | /// this is represented as a string but really is meant to hold JSON 41 | /// found this to be a convenient hack to allow flexible data 42 | pub data: String, 43 | } 44 | 45 | #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] 46 | pub enum OrderStatus { 47 | Uninitialized = 0, 48 | Pending = 1, 49 | Paid = 2, 50 | Withdrawn = 3, 51 | Cancelled = 4, 52 | } 53 | 54 | #[derive(BorshSerialize, BorshSchema, BorshDeserialize, Debug, PartialEq)] 55 | pub struct OrderAccount { 56 | pub discriminator: u8, 57 | pub status: u8, 58 | pub created: UnixTimestamp, 59 | pub modified: UnixTimestamp, 60 | pub merchant: PublicKey, 61 | pub mint: PublicKey, // represents the token/currency in use 62 | pub token: PublicKey, // represents the token account that holds the money 63 | pub payer: PublicKey, 64 | pub expected_amount: u64, 65 | pub paid_amount: u64, 66 | pub order_id: String, 67 | pub secret: String, 68 | /// this is represented as a string but really is meant to hold JSON 69 | /// found this to be a convenient hack to allow flexible data 70 | pub data: String, 71 | } 72 | 73 | #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq)] 74 | pub enum SubscriptionStatus { 75 | Uninitialized = 0, 76 | Initialized = 1, 77 | Cancelled = 2, 78 | } 79 | 80 | #[derive(BorshSerialize, BorshSchema, BorshDeserialize, Debug, PartialEq)] 81 | pub struct SubscriptionAccount { 82 | pub discriminator: u8, 83 | pub status: u8, 84 | pub owner: PublicKey, 85 | pub merchant: PublicKey, 86 | pub name: String, 87 | pub joined: UnixTimestamp, 88 | pub period_start: UnixTimestamp, 89 | pub period_end: UnixTimestamp, 90 | /// this is represented as a string but really is meant to hold JSON 91 | /// found this to be a convenient hack to allow flexible data 92 | pub data: String, 93 | } 94 | 95 | // impl for MerchantAccount 96 | impl Sealed for MerchantAccount {} 97 | 98 | impl Serdes for MerchantAccount {} 99 | 100 | impl MerchantAccount { 101 | pub const MIN_LEN: usize = 102 | size_of::() + size_of::() + size_of::() + size_of::(); 103 | } 104 | 105 | // impl for OrderAccount 106 | impl Sealed for OrderAccount {} 107 | 108 | impl Serdes for OrderAccount {} 109 | 110 | impl OrderAccount { 111 | pub const MIN_LEN: usize = size_of::() 112 | + size_of::() 113 | + size_of::() 114 | + size_of::() 115 | + size_of::() 116 | + size_of::() 117 | + size_of::() 118 | + size_of::() 119 | + size_of::() 120 | + size_of::(); 121 | } 122 | 123 | // impl for SubscriptionAccount 124 | impl Sealed for SubscriptionAccount {} 125 | 126 | impl Serdes for SubscriptionAccount {} 127 | 128 | impl SubscriptionAccount { 129 | pub const MIN_LEN: usize = size_of::() 130 | + size_of::() 131 | + size_of::() 132 | + size_of::() 133 | + size_of::() 134 | + size_of::() 135 | + size_of::(); 136 | } 137 | 138 | /// Check if a program account state is closed 139 | pub trait IsClosed { 140 | /// Is closed 141 | fn is_closed(&self) -> bool; 142 | } 143 | 144 | macro_rules! impl_IsInitialized { 145 | (for $($t:ty),+) => { 146 | $(impl IsInitialized for $t { 147 | fn is_initialized(&self) -> bool { 148 | self.discriminator != Discriminator::Uninitialized as u8 149 | } 150 | })* 151 | } 152 | } 153 | 154 | macro_rules! impl_IsClosed { 155 | (for $($t:ty),+) => { 156 | $(impl IsClosed for $t { 157 | fn is_closed(&self) -> bool { 158 | self.discriminator == Discriminator::Closed as u8 159 | } 160 | })* 161 | } 162 | } 163 | 164 | impl_IsInitialized!(for MerchantAccount, OrderAccount, SubscriptionAccount); 165 | impl_IsClosed!(for MerchantAccount, OrderAccount, SubscriptionAccount); 166 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::engine::constants::STRING_SIZE; 2 | use crate::state::{MerchantAccount, OrderAccount, SubscriptionAccount}; 3 | 4 | /// Given the expected amount, calculate the fee and take home amount 5 | /// Currently fee is 0.3% with a minimum fee of 1 lamport 6 | /// If the amount is less than 100 lamports the fee is 0 7 | pub fn get_amounts(amount: u64, fee_percentage: u128) -> (u64, u64) { 8 | let mut fee_amount: u64 = 0; 9 | let mut take_home_amount: u64 = amount; 10 | 11 | if amount >= 100 { 12 | let possible_fee_amount: u128 = (amount as u128 * fee_percentage) / 1000; 13 | fee_amount = 1; 14 | if possible_fee_amount > 0 { 15 | fee_amount = possible_fee_amount as u64; 16 | } 17 | take_home_amount = amount - fee_amount; 18 | } 19 | 20 | (take_home_amount, fee_amount) 21 | } 22 | 23 | pub fn get_account_size(min_len: usize, strings: &Vec<&String>) -> usize { 24 | let mut size = min_len; 25 | for item in strings { 26 | size = size + item.chars().count() + STRING_SIZE; 27 | } 28 | 29 | size 30 | } 31 | 32 | /// get order account size 33 | pub fn get_order_account_size(order_id: &String, secret: &String, data: &String) -> usize { 34 | get_account_size(OrderAccount::MIN_LEN, &vec![order_id, secret, data]) 35 | } 36 | 37 | /// get merchant account size 38 | pub fn get_merchant_account_size(data: &String) -> usize { 39 | get_account_size(MerchantAccount::MIN_LEN, &vec![data]) 40 | } 41 | 42 | /// get subscription account size 43 | pub fn get_subscription_account_size(name: &String, data: &String) -> usize { 44 | get_account_size(SubscriptionAccount::MIN_LEN, &vec![name, data]) 45 | } 46 | 47 | #[cfg(test)] 48 | mod test { 49 | use {super::*, solana_program_test::*}; 50 | 51 | #[tokio::test] 52 | async fn test_get_amounts() { 53 | assert_eq!((997000000, 3000000), get_amounts(1000000000, 3)); 54 | assert_eq!((1994000, 6000), get_amounts(2000000, 3)); 55 | assert_eq!((1994, 6), get_amounts(2000, 3)); 56 | assert_eq!((100, 1), get_amounts(101, 3)); 57 | assert_eq!((99, 1), get_amounts(100, 3)); 58 | assert_eq!((99, 0), get_amounts(99, 3)); 59 | assert_eq!((80, 0), get_amounts(80, 3)); 60 | assert_eq!((0, 0), get_amounts(0, 3)); 61 | assert_eq!((990, 10), get_amounts(1000, 10)); 62 | assert_eq!((996, 4), get_amounts(1000, 4)); 63 | } 64 | 65 | #[tokio::test] 66 | async fn test_get_order_account_size() { 67 | assert_eq!( 68 | 198, 69 | get_order_account_size( 70 | &String::from("123456"), 71 | &String::from("password"), 72 | &String::from(r#"{"a": "b"}"#) 73 | ) 74 | ); 75 | assert_eq!( 76 | 190, 77 | get_order_account_size( 78 | &String::from("test-6"), 79 | &String::from(""), 80 | &String::from(r#"{"a": "b"}"#) 81 | ) 82 | ); 83 | assert_eq!(423, get_order_account_size(&String::from("WSUDUBDG2"), &String::from("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type"), &String::from(r#"{"a": "b"}"#))); 84 | } 85 | 86 | #[tokio::test] 87 | async fn test_get_merchant_account_size() { 88 | assert_eq!(79, get_merchant_account_size(&String::from("{}"))); 89 | assert_eq!( 90 | 168, 91 | get_merchant_account_size(&String::from( 92 | r#"{"code":200,"success":true,"payload":{"features":["awesome","easyAPI","lowLearningCurve"]}}"# 93 | )) 94 | ); 95 | } 96 | 97 | #[tokio::test] 98 | async fn test_get_subscription_account_size() { 99 | assert_eq!( 100 | 100, 101 | get_subscription_account_size(&String::from("a"), &String::from("b")) 102 | ); 103 | assert_eq!( 104 | 132, 105 | get_subscription_account_size( 106 | &String::from("Annual"), 107 | &String::from(r#"{"foo": "bar", "price": 200}"#) 108 | ) 109 | ); 110 | } 111 | } 112 | --------------------------------------------------------------------------------