1#![forbid(unsafe_code)]
2#![deny(clippy::all)]
3#![deny(clippy::integer_arithmetic)]
4#![cfg(not(feature = "no-entrypoint"))]
5
6mod error;
7
8use std::mem::{self};
9
10use borsh::{BorshDeserialize, BorshSerialize};
11use error::Error;
12use human_common::utils::{
13 next_atoken_wallet, next_expected_account, next_expected_token_wallet, next_signer_account,
14};
15use solana_program::{
16 account_info::{next_account_info, AccountInfo},
17 clock::{Clock, UnixTimestamp},
18 entrypoint,
19 entrypoint::ProgramResult,
20 msg,
21 program::{invoke, invoke_signed},
22 program_error::ProgramError,
23 program_memory::{sol_memcpy, sol_memset},
24 program_option::COption,
25 program_pack::Pack,
26 pubkey::Pubkey,
27 rent::Rent,
28 system_instruction, system_program,
29 sysvar::Sysvar,
30};
31use spl_associated_token_account::get_associated_token_address;
32use spl_token::instruction as token_inst;
33use spl_token::state as token_state;
34
35#[derive(Debug, BorshDeserialize, BorshSerialize)]
36enum Request {
37 Funded(FundedRequest),
39 Unfunded(UnfundedRequest),
41}
42
43#[derive(Debug, BorshDeserialize, BorshSerialize)]
44struct FundedRequest {
45 author: Pubkey,
47}
48
49#[derive(Debug, BorshDeserialize, BorshSerialize)]
50struct UnfundedRequest {
51 collected: u64,
53 deadline: Option<UnixTimestamp>,
54 accept_threshold: u64,
55}
56
57#[repr(u8)]
58#[derive(Debug, BorshDeserialize, BorshSerialize, PartialEq)]
59enum RequestStatus {
60 Unitialized,
61 Open,
62 Declined,
63 Accepted,
64}
65
66#[derive(Debug, BorshDeserialize, BorshSerialize)]
67struct State {
68 request_status: RequestStatus,
69 created_at: UnixTimestamp,
71 wallet: Pubkey,
73 destination: Pubkey,
75 payer: Pubkey,
77 request: Request,
79}
80
81#[derive(Debug, BorshDeserialize, BorshSerialize, PartialEq)]
82struct Voucher {
83 state: Pubkey,
85 user: Pubkey,
87 amount: u64,
89}
90
91impl State {
92 fn expired(&self, now: UnixTimestamp) -> bool {
94 if !self.is_open() {
95 return false;
96 }
97
98 if let Request::Unfunded(UnfundedRequest {
99 deadline: Some(deadline),
100 collected,
101 accept_threshold,
102 }) = self.request
103 {
104 return now > deadline && collected < accept_threshold;
105 }
106
107 false
108 }
109
110 fn try_accept(&mut self) -> Result<(), ProgramError> {
111 if self.request_status != RequestStatus::Open {
112 return Err(ProgramError::Custom(0x16));
113 }
114
115 if let Request::Unfunded(request) = &self.request {
116 if request.collected < request.accept_threshold {
117 msg!("not enough collected to accept");
118 return Err(ProgramError::Custom(0x16));
119 }
120 }
121
122 self.request_status = RequestStatus::Accepted;
123
124 Ok(())
125 }
126
127 fn is_funded(&self) -> bool {
128 matches!(self.request, Request::Funded(_))
129 }
130
131 fn is_open(&self) -> bool {
132 self.request_status == RequestStatus::Open
133 }
134}
135
136#[derive(Debug, BorshDeserialize, BorshSerialize)]
137#[repr(u8)]
138#[non_exhaustive]
139enum Instruction {
140 Create(CreateInstruction),
141
142 Contribute(ContributeInstruction),
151
152 Refund,
156
157 Cancel,
160
161 Accept,
164
165 Decline,
168}
169
170#[derive(Debug, BorshDeserialize, BorshSerialize)]
171struct ContributeInstruction {
172 user: Pubkey,
173}
174
175#[derive(Debug, BorshDeserialize, BorshSerialize)]
176struct CreateInstruction {
177 dest: Pubkey,
178 payer: Pubkey,
179 rtype: CreateInstructionRequest,
180}
181
182#[derive(Debug, BorshDeserialize, BorshSerialize)]
183enum CreateInstructionRequest {
184 Funded {
185 author: Pubkey,
186 },
187 Unfunded {
188 deadline: Option<UnixTimestamp>,
189 accept_threshold: u64,
190 },
191}
192
193entrypoint!(process_instruction);
194pub fn process_instruction(
195 program_id: &Pubkey,
196 accounts: &[AccountInfo],
197 instruction_data: &[u8],
198) -> ProgramResult {
199 let instruction = Instruction::try_from_slice(instruction_data).map_err(|e| {
200 msg!("error parsing instruction: {}", e);
201 ProgramError::InvalidInstructionData
202 })?;
203
204 match instruction {
205 Instruction::Create(inst) => {
206 msg!("creating funded request");
207 process_create(program_id, accounts, inst)
208 }
209 Instruction::Contribute(ContributeInstruction { user }) => {
210 msg!("contributing to unfunded request");
211 process_contribute(program_id, accounts, &user)
212 }
213 Instruction::Refund => {
214 msg!("refunding request");
215 process_refund(program_id, accounts)
216 }
217
218 Instruction::Accept => {
219 msg!("accepting request");
220 process_accept(program_id, accounts)
221 }
222 Instruction::Decline => {
223 msg!("rejecting redeem request");
224 process_decline(program_id, accounts)
225 }
226 Instruction::Cancel => {
227 msg!("cancelling funded redeem request");
228 process_cancel(program_id, accounts)
229 }
230 }
231}
232
233pub const V1: &[u8] = b"HMN_R1";
234pub const AUTHORITY_SEED: &[u8] = b"A";
235
236#[macro_export]
237macro_rules! find_keyed_address {
238 ($program_id:expr, $seed:expr) => {
239 $crate::find_keyed_address!($program_id, $seed, &[])
240 };
241 ($program_id:expr, $seed:expr, $token:expr) => {{
242 let _: (&Pubkey, &[u8]) = ($program_id, $token);
243
244 let seeds = &[V1, $seed, ($token)];
245 let (addr, bump) = Pubkey::find_program_address(seeds, $program_id);
246
247 (addr, &[V1, $seed, $token, &[bump]])
248 }};
249}
250
251#[macro_export]
253macro_rules! authority {
254 ($program_id:expr, $state_addr:expr) => {
255 $crate::find_keyed_address!($program_id, AUTHORITY_SEED, $state_addr.as_ref())
256 };
257}
258
259const STATE_SIZE: usize = 138;
260
261fn process_create(
264 program_id: &Pubkey,
265 accounts: &[AccountInfo],
266 args: CreateInstruction,
267) -> ProgramResult {
268 let account_info_iter = &mut accounts.iter();
269
270 let state_acc = next_account_info(account_info_iter)?;
271 let (wallet_addr, wallet) =
272 next_owned_token_wallet(account_info_iter, program_id, state_acc.key)?;
273
274 let rent = Rent::get()?;
275 let clock = Clock::get()?;
276
277 let mut state_data = state_acc.try_borrow_mut_data()?;
278
279 if !rent.is_exempt(state_acc.lamports(), STATE_SIZE) {
280 return Err(ProgramError::AccountNotRentExempt);
281 }
282
283 match args.rtype {
285 CreateInstructionRequest::Funded { .. } if wallet.amount == 0 => {
286 return Err(ProgramError::InsufficientFunds);
288 }
289 CreateInstructionRequest::Unfunded { .. } if wallet.amount != 0 => {
290 return Err(ProgramError::InvalidAccountData);
292 }
293 _ => {}
294 }
295
296 let request = match args.rtype {
297 CreateInstructionRequest::Funded { author } => Request::Funded(FundedRequest { author }),
298 CreateInstructionRequest::Unfunded {
299 deadline,
300 accept_threshold,
301 } => Request::Unfunded(UnfundedRequest {
302 collected: 0,
303 deadline,
304 accept_threshold,
305 }),
306 };
307
308 let state = State {
309 request_status: RequestStatus::Open,
310 created_at: clock.unix_timestamp,
311 wallet: wallet_addr,
312 payer: args.payer,
313 destination: args.dest,
314 request,
315 };
316
317 write_state(&mut state_data, state)?;
318
319 Ok(())
320}
321
322fn write_state(data: &mut [u8], state: State) -> Result<(), ProgramError> {
323 if data.len() < STATE_SIZE {
324 return Err(ProgramError::AccountDataTooSmall);
325 }
326
327 if matches!(State::try_from_slice(data), Ok(State { request_status, .. }) if request_status != RequestStatus::Unitialized)
329 {
330 return Err(ProgramError::AccountAlreadyInitialized);
331 }
332
333 let state = state.try_to_vec()?;
334
335 copy_slice(data, &state);
336
337 Ok(())
338}
339
340pub fn next_owned_token_wallet<'a, 'b: 'a, I>(
341 i: &mut I,
342 program_id: &Pubkey,
343 state_addr: &Pubkey,
344) -> Result<(Pubkey, token_state::Account), ProgramError>
345where
346 I: Iterator<Item = &'a AccountInfo<'b>>,
347{
348 let wallet = next_account_info(i)?;
349
350 if !spl_token::check_id(wallet.owner) {
351 return Err(ProgramError::InvalidArgument);
352 }
353
354 let account = token_state::Account::unpack(&wallet.data.borrow())?;
355
356 let (derived_authority, _) = authority!(program_id, state_addr);
357 if account.owner != derived_authority {
358 return Err(ProgramError::IllegalOwner);
359 }
360
361 if account.close_authority.is_some() {
362 return Err(ProgramError::IllegalOwner);
363 }
364
365 Ok((*wallet.key, account))
366}
367
368fn next_state_account<'a, 'b, I: Iterator<Item = &'a AccountInfo<'b>>>(
369 i: &mut I,
370 program_id: &Pubkey,
371) -> Result<(State, &'a AccountInfo<'b>), ProgramError> {
372 let state_acc = next_account_info(i)?;
373
374 if state_acc.owner != program_id {
375 msg!(
376 "illegal state owner ({} != {})",
377 state_acc.owner,
378 program_id
379 );
380 return Err(ProgramError::IllegalOwner);
381 }
382
383 let data = state_acc.try_borrow_data()?;
384
385 let state = State::try_from_slice(&data)?;
386
387 if state.request_status == RequestStatus::Unitialized {
388 return Err(ProgramError::UninitializedAccount);
389 }
390
391 Ok((state, state_acc))
392}
393
394fn save_state(state: State, state_acc: &AccountInfo) -> Result<(), ProgramError> {
395 let serialized = state.try_to_vec()?;
396
397 let mut data = state_acc.try_borrow_mut_data()?;
398
399 copy_slice(&mut data[..], &serialized);
400
401 Ok(())
402}
403
404fn erase_state(
405 state: State,
406 state_acc: &AccountInfo,
407 payer_acc: &AccountInfo,
408) -> Result<(), ProgramError> {
409 assert_ne!(state.request_status, RequestStatus::Open);
411
412 if let Request::Unfunded(UnfundedRequest { collected, .. }) = state.request {
413 assert_eq!(collected, 0, "error: not all vouchers are refunded")
414 };
415
416 let lamports = state_acc.lamports();
418
419 **state_acc.try_borrow_mut_lamports()? = 0;
420 let mut payer_lamports = payer_acc.try_borrow_mut_lamports()?;
421 **payer_lamports = payer_lamports
422 .checked_add(lamports)
423 .ok_or(Error::Overflow)?;
424
425 let mut data = state_acc.try_borrow_mut_data()?;
426 let l = data.len();
427 sol_memset(&mut data, 0, l);
428
429 Ok(())
430}
431
432#[inline]
433fn copy_slice(dst: &mut [u8], src: &[u8]) {
434 sol_memcpy(dst, src, src.len());
435}
436
437fn process_contribute(
445 program_id: &Pubkey,
446 accounts: &[AccountInfo],
447 user: &Pubkey,
448) -> ProgramResult {
449 let account_info_iter = &mut accounts.iter();
450
451 let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
452 let _wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
453
454 let source_wallet = next_signer_account(account_info_iter, &state.destination)?;
455
456 if !spl_token::check_id(source_wallet.owner) {
457 return Err(ProgramError::IllegalOwner);
458 }
459
460 let source_wallet_data = token_state::Account::unpack(&source_wallet.try_borrow_data()?)?;
461
462 let amount = source_wallet_data.delegated_amount;
463 if amount == 0 {
464 return Err(ProgramError::InvalidArgument);
465 }
466
467 let delegate = source_wallet_data
468 .delegate
469 .ok_or(ProgramError::InvalidArgument)?;
470
471 next_signer_account(account_info_iter, &delegate)?;
472
473 let voucher_acc = next_account_info(account_info_iter)?;
474 let rent = Rent::from_account_info(next_account_info(account_info_iter)?)?;
475 let clock = Clock::from_account_info(next_account_info(account_info_iter)?)?;
476
477 if state.expired(clock.unix_timestamp) {
478 state.request_status = RequestStatus::Declined;
479 save_state(state, state_acc)?;
480 return Err(ProgramError::Custom(0x17));
481 }
482
483 if !state.is_open() {
484 return Err(ProgramError::Custom(0x12));
485 }
486
487 let mut unfunded_state = match state.request {
488 Request::Funded(_) => return Err(ProgramError::Custom(0x12)),
489 Request::Unfunded(ref mut s) => s,
490 };
491
492 let inst = token_inst::transfer(
494 &spl_token::ID,
495 source_wallet.key,
496 &state.wallet,
497 &delegate,
498 &[],
499 amount,
500 )?;
501
502 invoke(&inst, accounts)?;
503
504 let mut previous_amount = 0;
506
507 if let Some(v) = get_voucher(voucher_acc, program_id, &rent)? {
508 if v.user != *user {
509 msg!("refusing to override another user's voucher");
510 return Err(ProgramError::IllegalOwner);
511 }
512
513 previous_amount = v.amount
514 }
515
516 let voucher = Voucher {
517 state: *state_acc.key,
518 user: *user,
519 amount: previous_amount.checked_add(amount).ok_or(Error::Overflow)?,
520 }
521 .try_to_vec()?;
522
523 let mut data = voucher_acc.try_borrow_mut_data()?;
524 copy_slice(&mut data, &voucher);
525
526 unfunded_state.collected = unfunded_state
528 .collected
529 .checked_add(amount)
530 .ok_or(Error::Overflow)?;
531
532 save_state(state, state_acc)?;
533
534 Ok(())
535}
536
537fn get_voucher(
538 voucher_acc: &AccountInfo,
539 program_id: &Pubkey,
540 rent: &Rent,
541) -> Result<Option<Voucher>, ProgramError> {
542 if !voucher_acc.is_writable {
543 return Err(ProgramError::InvalidArgument);
544 }
545
546 if voucher_acc.owner != program_id {
547 return Err(ProgramError::IncorrectProgramId);
548 }
549
550 if !rent.is_exempt(voucher_acc.lamports(), voucher_acc.data_len()) {
551 return Err(ProgramError::AccountNotRentExempt);
552 }
553
554 let data = voucher_acc.try_borrow_data()?;
555
556 match Voucher::try_from_slice(&data) {
557 Ok(v) if v.amount > 0 => Ok(Some(v)),
558 _ => Ok(None),
559 }
560}
561
562fn process_refund(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
571 let account_info_iter = &mut accounts.iter().peekable();
572
573 let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
574 let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
575
576 let payer = next_account_info(account_info_iter)?;
577 let rent = Rent::from_account_info(next_account_info(account_info_iter)?)?;
578 let _clock = Clock::from_account_info(next_account_info(account_info_iter)?)?;
579
580 if *payer.key != state.payer {
581 return Err(ProgramError::IllegalOwner);
583 }
584
585 if state.is_open() {
586 return Err(ProgramError::InvalidArgument);
587 }
588
589 let request = match state.request {
590 Request::Funded(_) => return Err(ProgramError::Custom(0x12)),
591 Request::Unfunded(ref mut s) => s,
592 };
593
594 let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
595
596 let mut payer_lamports = payer.try_borrow_mut_lamports()?;
597
598 while account_info_iter.peek().is_some() {
600 let voucher_acc = next_account_info(account_info_iter)?;
601
602 let voucher = get_voucher(voucher_acc, program_id, &rent)?
603 .ok_or(ProgramError::UninitializedAccount)?;
604
605 redeem_voucher(voucher_acc, &mut payer_lamports)?;
607
608 request.collected = request
609 .collected
610 .checked_sub(voucher.amount)
611 .ok_or(ProgramError::Custom(0x14))?;
612
613 let user_wallet = next_account_info(account_info_iter)?;
614
615 let derived_wallet = get_associated_token_address(&voucher.user, &wallet.mint);
616 if derived_wallet != *user_wallet.key {
617 return Err(ProgramError::InvalidArgument);
618 }
619
620 if state.request_status == RequestStatus::Accepted {
621 continue;
623 }
624
625 let transfer = token_inst::transfer(
627 &spl_token::ID,
628 &state.wallet,
629 user_wallet.key,
630 &derived_authority,
631 &[],
632 voucher.amount,
633 )?;
634
635 invoke_signed(&transfer, accounts, &[authority_seed])?;
636 }
637
638 if request.collected > 0 {
639 return Ok(());
641 }
642
643 if wallet.amount != 0 {
644 msg!("sanity check failed: collected == 0 but token amount is still not zero");
645 return Err(ProgramError::Custom(0x15));
646 }
647
648 let close = token_inst::close_account(
650 &spl_token::ID,
651 &state.wallet,
652 payer.key,
653 &derived_authority,
654 &[],
655 )?;
656
657 invoke_signed(&close, accounts, &[authority_seed])?;
658
659 erase_state(state, state_acc, payer)?;
660
661 Ok(())
662}
663
664fn redeem_voucher(state_acc: &AccountInfo, payer_lamports: &mut u64) -> Result<(), ProgramError> {
665 *payer_lamports = payer_lamports
667 .checked_add(mem::take(*state_acc.try_borrow_mut_lamports()?))
668 .ok_or(Error::Overflow)?;
669
670 let mut data = state_acc.try_borrow_mut_data()?;
671 let l = data.len();
672 sol_memset(&mut data, 0, l);
673
674 Ok(())
675}
676
677fn process_accept(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
686 let account_info_iter = &mut accounts.iter();
687
688 let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
689 let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
690
691 let _dest = next_signer_account(account_info_iter, &state.destination)?;
692 let destination_wallet = next_account_info(account_info_iter)?;
693
694 let payer = next_account_info(account_info_iter)?;
695 let clock = Clock::from_account_info(next_account_info(account_info_iter)?)?;
696
697 if *payer.key != state.payer {
698 return Err(ProgramError::IllegalOwner);
700 }
701
702 if state.expired(clock.unix_timestamp) {
703 state.request_status = RequestStatus::Declined;
704 save_state(state, state_acc)?;
705 return Err(ProgramError::Custom(0x17));
706 }
707
708 if !state.is_open() {
709 return Err(ProgramError::Custom(0x10));
710 }
711
712 if wallet.amount == 0 {
714 return Err(ProgramError::Custom(0x11));
715 }
716
717 state.try_accept()?;
718
719 let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
720
721 let transfer = token_inst::transfer(
723 &spl_token::ID,
724 &state.wallet,
725 destination_wallet.key,
726 &derived_authority,
727 &[],
728 wallet.amount,
729 )?;
730
731 invoke_signed(&transfer, accounts, &[authority_seed])?;
732
733 if state.is_funded() {
734 let close = token_inst::close_account(
736 &spl_token::ID,
737 &state.wallet,
738 payer.key,
739 &derived_authority,
740 &[],
741 )?;
742
743 invoke_signed(&close, accounts, &[authority_seed])?;
744
745 erase_state(state, state_acc, payer)?;
747 return Ok(());
748 }
749
750 save_state(state, state_acc)?;
752
753 Ok(())
754}
755
756fn process_cancel(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
763 let account_info_iter = &mut accounts.iter();
764
765 let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
766 let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
767
768 let author = match state.request {
769 Request::Funded(FundedRequest { author }) => author,
770 Request::Unfunded { .. } => return Err(ProgramError::Custom(0x13)),
771 };
772
773 let _dest = next_signer_account(account_info_iter, &author)?;
774 let payer = next_expected_account(account_info_iter, &state.payer)?;
775
776 if !state.is_open() {
777 return Err(ProgramError::Custom(0x10));
778 }
779
780 state.request_status = RequestStatus::Declined;
781 let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
782 let _authority = next_expected_account(account_info_iter, &derived_authority)?;
783
784 if let COption::Some(rent_balance) = wallet.is_native {
785 let atoken_addr = get_associated_token_address(&author, &wallet.mint);
786 next_expected_account(account_info_iter, &atoken_addr)?;
787 next_expected_account(account_info_iter, &spl_token::ID)?;
788 next_expected_account(account_info_iter, &system_program::ID)?;
789
790 let close = token_inst::close_account(
792 &spl_token::ID,
793 &state.wallet,
794 &derived_authority,
795 &derived_authority,
796 &[],
797 )?;
798 invoke_signed(&close, accounts, &[authority_seed])?;
799
800 msg!("transfer sol back to dest");
802 let transfer = system_instruction::transfer(&derived_authority, &author, wallet.amount);
803 invoke_signed(&transfer, accounts, &[authority_seed])?;
804
805 msg!("transfer rent back to payer");
807 let transfer = system_instruction::transfer(&derived_authority, payer.key, rent_balance);
808 invoke_signed(&transfer, accounts, &[authority_seed])?;
809 } else {
810 let (author_wallet, _) = next_atoken_wallet(account_info_iter, &author, &wallet.mint)?;
811 next_expected_account(account_info_iter, &spl_token::ID)?;
812
813 msg!("transfer tokens back to dest");
815 let transfer = token_inst::transfer(
816 &spl_token::ID,
817 &state.wallet,
818 &author_wallet,
819 &derived_authority,
820 &[],
821 wallet.amount,
822 )?;
823
824 invoke_signed(&transfer, accounts, &[authority_seed])?;
825
826 msg!("close token wallet");
827 let close = token_inst::close_account(
829 &spl_token::ID,
830 &state.wallet,
831 payer.key,
832 &derived_authority,
833 &[],
834 )?;
835 invoke_signed(&close, accounts, &[authority_seed])?;
836 }
837 erase_state(state, state_acc, payer)?;
838
839 Ok(())
840}
841
842fn process_decline(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
851 let account_info_iter = &mut accounts.iter();
852
853 let (mut state, state_acc) = next_state_account(account_info_iter, program_id)?;
854 let wallet = next_expected_token_wallet(account_info_iter, &state.wallet)?;
855
856 let _dest = next_signer_account(account_info_iter, &state.destination)?;
857 let payer = next_expected_account(account_info_iter, &state.payer)?;
858
859 if !state.is_open() {
860 return Err(ProgramError::Custom(0x10));
861 }
862
863 state.request_status = RequestStatus::Declined;
864
865 match state.request {
866 Request::Funded(ref r) => {
867 let (derived_authority, authority_seed) = authority!(program_id, state_acc.key);
868 if let COption::Some(rent_balance) = wallet.is_native {
871 let _author = next_expected_account(account_info_iter, &r.author)?;
872
873 let close = token_inst::close_account(
875 &spl_token::ID,
876 &state.wallet,
877 &derived_authority,
878 &derived_authority,
879 &[],
880 )?;
881 invoke_signed(&close, accounts, &[authority_seed])?;
882
883 next_expected_account(account_info_iter, &spl_token::ID)?; next_expected_account(account_info_iter, &system_program::ID)?; let transfer =
888 system_instruction::transfer(&derived_authority, &r.author, wallet.amount);
889 invoke_signed(&transfer, accounts, &[authority_seed])?;
890
891 let transfer =
893 system_instruction::transfer(&derived_authority, payer.key, rent_balance);
894 invoke_signed(&transfer, accounts, &[authority_seed])?;
895 } else {
896 let (author_wallet, _) =
897 next_atoken_wallet(account_info_iter, &r.author, &wallet.mint)?;
898
899 next_expected_account(account_info_iter, &r.author)?;
900
901 let transfer = token_inst::transfer(
903 &spl_token::ID,
904 &state.wallet,
905 &author_wallet,
906 &derived_authority,
907 &[],
908 wallet.amount,
909 )?;
910
911 invoke_signed(&transfer, accounts, &[authority_seed])?;
912
913 let close = token_inst::close_account(
915 &spl_token::ID,
916 &state.wallet,
917 payer.key,
918 &derived_authority,
919 &[],
920 )?;
921
922 invoke_signed(&close, accounts, &[authority_seed])?;
923 }
924
925 erase_state(state, state_acc, payer)?;
927 Ok(())
928 }
929 Request::Unfunded(_) => {
930 save_state(state, state_acc)?;
932 Ok(())
933 }
934 }
935}
936#[cfg(test)]
937mod tests {
938 use solana_program::pubkey::Pubkey;
939
940 use crate::{
941 Request::{self, *},
942 *,
943 };
944
945 #[test]
946 fn test_state_is_funded() {
947 assert!(
948 from_request(Unfunded(UnfundedRequest {
949 collected: 0,
950 deadline: None,
951 accept_threshold: 0,
952 }))
953 .is_funded()
954 == false
955 );
956
957 assert!(
958 from_request(Funded(FundedRequest {
959 author: Pubkey::default(),
960 }))
961 .is_funded()
962 == true
963 );
964 }
965
966 #[test]
967 fn test_state_expired() {
968 let now = 100000000;
969
970 let expired = from_request(Unfunded(UnfundedRequest {
971 collected: 1000,
972 deadline: Some(now - 100), accept_threshold: 100000,
974 }));
975
976 assert!(expired.expired(now) == true);
977
978 let funded_in_time = from_request(Unfunded(UnfundedRequest {
980 collected: 9999,
981 deadline: Some(now - 100), accept_threshold: 1000,
983 }));
984
985 assert!(funded_in_time.expired(now) == false);
986
987 let not_expired = from_request(Unfunded(UnfundedRequest {
988 collected: 1000,
989 deadline: Some(now + 100), accept_threshold: 100000,
991 }));
992
993 assert!(not_expired.expired(now) == false);
994
995 let no_deadline = from_request(Unfunded(UnfundedRequest {
996 collected: 1000,
997 deadline: None, accept_threshold: 100000,
999 }));
1000
1001 assert!(no_deadline.expired(now) == false);
1002 }
1003
1004 #[test]
1005 fn test_state_try_accept() {
1006 let mut funded = from_request(Funded(FundedRequest {
1007 author: Pubkey::default(),
1008 }));
1009
1010 funded.try_accept().unwrap();
1012 assert!(!funded.is_open());
1013
1014 let mut unfunded = from_request(Unfunded(UnfundedRequest {
1015 collected: 1000,
1016 deadline: None,
1017 accept_threshold: 100000,
1018 }));
1019
1020 unfunded.try_accept().unwrap_err();
1021
1022 assert_eq!(unfunded.request_status, RequestStatus::Open);
1023 assert!(unfunded.is_open());
1024
1025 let mut acceptable = from_request(Unfunded(UnfundedRequest {
1026 collected: 77777,
1027 deadline: None,
1028 accept_threshold: 10,
1029 }));
1030
1031 acceptable.try_accept().unwrap();
1032
1033 assert_eq!(acceptable.request_status, RequestStatus::Accepted);
1034 assert!(!acceptable.is_open());
1035 }
1036
1037 fn from_request(request: Request) -> State {
1038 let s = State {
1039 request_status: RequestStatus::Open,
1040 wallet: Pubkey::default(),
1041 destination: Pubkey::default(),
1042 payer: Pubkey::default(),
1043 created_at: 0,
1044 request,
1045 };
1046
1047 s
1051 }
1052
1053 #[test]
1054 fn test_get_voucher() {
1055 let rent = Rent::with_slots_per_epoch(432000);
1056
1057 let data_len = 72;
1058 let mut data = vec![0; data_len];
1059 let mut lamports = rent.minimum_balance(data_len);
1060
1061 let key = Pubkey::new_from_array([1; 32]);
1062 let pid = Pubkey::new_from_array([23; 32]);
1063
1064 {
1066 let acc = AccountInfo::new(&key, false, true, &mut lamports, &mut data, &pid, false, 0);
1067 assert!(get_voucher(&acc, &pid, &rent).unwrap().is_none());
1068 }
1069
1070 {
1071 let acc = AccountInfo::new(&key, false, true, &mut lamports, &mut data, &pid, false, 0);
1073 assert_eq!(
1074 get_voucher(&acc, &Pubkey::default(), &rent).unwrap_err(),
1075 ProgramError::IncorrectProgramId
1076 );
1077 }
1078
1079 {
1080 let mut ins = 12;
1082
1083 let acc = AccountInfo::new(&key, false, true, &mut ins, &mut data, &pid, false, 0);
1084 assert_eq!(
1085 get_voucher(&acc, &pid, &rent).unwrap_err(),
1086 ProgramError::AccountNotRentExempt
1087 );
1088 }
1089
1090 {
1091 let state_addr = Pubkey::new_from_array([66; 32]);
1093 let user_addr = Pubkey::new_from_array([77; 32]);
1094
1095 let v = Voucher {
1096 state: state_addr,
1097 user: user_addr,
1098 amount: 123,
1099 };
1100
1101 let mut data = v.try_to_vec().unwrap();
1102
1103 let acc = AccountInfo::new(&key, false, true, &mut lamports, &mut data, &pid, false, 0);
1104 assert_eq!(get_voucher(&acc, &pid, &rent).unwrap().unwrap(), v);
1105 }
1106 }
1107}