@sudobility/contracts 1.10.0 → 1.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/solana/solana/mailer-client.d.ts +7 -0
  2. package/dist/solana/solana/mailer-client.d.ts.map +1 -1
  3. package/dist/solana/solana/mailer-client.js +30 -1
  4. package/dist/solana/solana/mailer-client.js.map +1 -1
  5. package/dist/unified/src/react/hooks/useMailerMutations.d.ts +1 -1
  6. package/dist/unified/src/react/hooks/useMailerMutations.js +1 -1
  7. package/dist/unified/src/solana/mailer-client.d.ts +7 -0
  8. package/dist/unified/src/solana/mailer-client.d.ts.map +1 -1
  9. package/dist/unified/src/solana/mailer-client.js +30 -1
  10. package/dist/unified/src/solana/mailer-client.js.map +1 -1
  11. package/dist/unified/src/unified/onchain-mailer-client.d.ts +2 -1
  12. package/dist/unified/src/unified/onchain-mailer-client.d.ts.map +1 -1
  13. package/dist/unified/src/unified/onchain-mailer-client.js +23 -2
  14. package/dist/unified/src/unified/onchain-mailer-client.js.map +1 -1
  15. package/dist/unified-esm/src/react/hooks/useMailerMutations.d.ts +1 -1
  16. package/dist/unified-esm/src/react/hooks/useMailerMutations.js +1 -1
  17. package/dist/unified-esm/src/solana/mailer-client.d.ts +7 -0
  18. package/dist/unified-esm/src/solana/mailer-client.d.ts.map +1 -1
  19. package/dist/unified-esm/src/solana/mailer-client.js +30 -1
  20. package/dist/unified-esm/src/solana/mailer-client.js.map +1 -1
  21. package/dist/unified-esm/src/unified/onchain-mailer-client.d.ts +2 -1
  22. package/dist/unified-esm/src/unified/onchain-mailer-client.d.ts.map +1 -1
  23. package/dist/unified-esm/src/unified/onchain-mailer-client.js +23 -2
  24. package/dist/unified-esm/src/unified/onchain-mailer-client.js.map +1 -1
  25. package/package.json +6 -1
  26. package/programs/mailer/src/lib.rs +429 -102
  27. package/programs/mailer/tests/integration_tests.rs +387 -65
@@ -1,6 +1,6 @@
1
1
  //! # Native Solana Mailer Program
2
2
  //!
3
- //! A native Solana program for decentralized messaging with delegation management,
3
+ //! A native Solana program for decentralized messaging with delegation management,
4
4
  //! USDC fees and revenue sharing - no Anchor dependencies.
5
5
  //!
6
6
  //! ## Key Features
@@ -25,7 +25,6 @@
25
25
  //! - Standard: Sender pays 10% fee only
26
26
  //! - Owner gets 10% of all fees
27
27
 
28
-
29
28
  use borsh::{BorshDeserialize, BorshSerialize};
30
29
  use solana_program::{
31
30
  account_info::{next_account_info, AccountInfo},
@@ -138,6 +137,22 @@ pub enum MailerInstruction {
138
137
  resolve_sender_to_name: bool,
139
138
  },
140
139
 
140
+ /// Send prepared message with optional revenue sharing (references off-chain content via mailId)
141
+ /// Accounts:
142
+ /// 0. `[signer]` Sender
143
+ /// 1. `[writable]` Recipient claim account (PDA)
144
+ /// 2. `[]` Mailer state account (PDA)
145
+ /// 3. `[writable]` Sender USDC account
146
+ /// 4. `[writable]` Mailer USDC account
147
+ /// 5. `[]` Token program
148
+ /// 6. `[]` System program
149
+ SendPrepared {
150
+ to: Pubkey,
151
+ mail_id: String,
152
+ revenue_share_to_receiver: bool,
153
+ resolve_sender_to_name: bool,
154
+ },
155
+
141
156
  /// Send message to email address (no wallet address known)
142
157
  /// Charges only 10% owner fee since recipient wallet is unknown
143
158
  /// Accounts:
@@ -160,10 +175,7 @@ pub enum MailerInstruction {
160
175
  /// 2. `[writable]` Sender USDC account
161
176
  /// 3. `[writable]` Mailer USDC account
162
177
  /// 4. `[]` Token program
163
- SendPreparedToEmail {
164
- to_email: String,
165
- mail_id: String,
166
- },
178
+ SendPreparedToEmail { to_email: String, mail_id: String },
167
179
 
168
180
  /// Claim recipient share
169
181
  /// Accounts:
@@ -205,6 +217,7 @@ pub enum MailerInstruction {
205
217
  /// Accounts:
206
218
  /// 0. `[signer]` Rejector
207
219
  /// 1. `[writable]` Delegation account (PDA)
220
+ /// 2. `[]` Mailer state account (PDA)
208
221
  RejectDelegation,
209
222
 
210
223
  /// Set delegation fee (owner only)
@@ -241,13 +254,13 @@ pub enum MailerInstruction {
241
254
  /// 3. `[writable]` Mailer USDC account
242
255
  /// 4. `[]` Token program
243
256
  Pause,
244
-
257
+
245
258
  /// Unpause the contract (owner only)
246
259
  /// Accounts:
247
260
  /// 0. `[signer]` Owner
248
261
  /// 1. `[writable]` Mailer state account (PDA)
249
262
  Unpause,
250
-
263
+
251
264
  /// Distribute claimable funds (when paused)
252
265
  /// Accounts:
253
266
  /// 0. `[signer]` Anyone can call
@@ -258,6 +271,13 @@ pub enum MailerInstruction {
258
271
  /// 5. `[]` Token program
259
272
  DistributeClaimableFunds { recipient: Pubkey },
260
273
 
274
+ /// Claim expired recipient shares (owner only)
275
+ /// Accounts:
276
+ /// 0. `[signer]` Owner
277
+ /// 1. `[writable]` Mailer state account (PDA)
278
+ /// 2. `[writable]` Recipient claim account (PDA)
279
+ ClaimExpiredShares { recipient: Pubkey },
280
+
261
281
  /// Emergency unpause without fund distribution (owner only)
262
282
  /// Accounts:
263
283
  /// 0. `[signer]` Owner
@@ -316,12 +336,39 @@ pub fn process_instruction(
316
336
  MailerInstruction::Initialize { usdc_mint } => {
317
337
  process_initialize(program_id, accounts, usdc_mint)
318
338
  }
319
- MailerInstruction::Send { to, subject, _body, revenue_share_to_receiver, resolve_sender_to_name } => {
320
- process_send(program_id, accounts, to, subject, _body, revenue_share_to_receiver, resolve_sender_to_name)
321
- }
322
- MailerInstruction::SendToEmail { to_email, subject, _body } => {
323
- process_send_to_email(program_id, accounts, to_email, subject, _body)
324
- }
339
+ MailerInstruction::Send {
340
+ to,
341
+ subject,
342
+ _body,
343
+ revenue_share_to_receiver,
344
+ resolve_sender_to_name,
345
+ } => process_send(
346
+ program_id,
347
+ accounts,
348
+ to,
349
+ subject,
350
+ _body,
351
+ revenue_share_to_receiver,
352
+ resolve_sender_to_name,
353
+ ),
354
+ MailerInstruction::SendPrepared {
355
+ to,
356
+ mail_id,
357
+ revenue_share_to_receiver,
358
+ resolve_sender_to_name,
359
+ } => process_send_prepared(
360
+ program_id,
361
+ accounts,
362
+ to,
363
+ mail_id,
364
+ revenue_share_to_receiver,
365
+ resolve_sender_to_name,
366
+ ),
367
+ MailerInstruction::SendToEmail {
368
+ to_email,
369
+ subject,
370
+ _body,
371
+ } => process_send_to_email(program_id, accounts, to_email, subject, _body),
325
372
  MailerInstruction::SendPreparedToEmail { to_email, mail_id } => {
326
373
  process_send_prepared_to_email(program_id, accounts, to_email, mail_id)
327
374
  }
@@ -337,24 +384,22 @@ pub fn process_instruction(
337
384
  MailerInstruction::SetDelegationFee { new_fee } => {
338
385
  process_set_delegation_fee(program_id, accounts, new_fee)
339
386
  }
340
- MailerInstruction::SetCustomFeePercentage { account, percentage } => {
341
- process_set_custom_fee_percentage(program_id, accounts, account, percentage)
342
- }
387
+ MailerInstruction::SetCustomFeePercentage {
388
+ account,
389
+ percentage,
390
+ } => process_set_custom_fee_percentage(program_id, accounts, account, percentage),
343
391
  MailerInstruction::ClearCustomFeePercentage { account } => {
344
392
  process_clear_custom_fee_percentage(program_id, accounts, account)
345
393
  }
346
- MailerInstruction::Pause => {
347
- process_pause(program_id, accounts)
348
- }
349
- MailerInstruction::Unpause => {
350
- process_unpause(program_id, accounts)
351
- }
394
+ MailerInstruction::Pause => process_pause(program_id, accounts),
395
+ MailerInstruction::Unpause => process_unpause(program_id, accounts),
352
396
  MailerInstruction::DistributeClaimableFunds { recipient } => {
353
397
  process_distribute_claimable_funds(program_id, accounts, recipient)
354
398
  }
355
- MailerInstruction::EmergencyUnpause => {
356
- process_emergency_unpause(program_id, accounts)
399
+ MailerInstruction::ClaimExpiredShares { recipient } => {
400
+ process_claim_expired_shares(program_id, accounts, recipient)
357
401
  }
402
+ MailerInstruction::EmergencyUnpause => process_emergency_unpause(program_id, accounts),
358
403
  }
359
404
  }
360
405
 
@@ -392,7 +437,11 @@ fn process_initialize(
392
437
  space as u64,
393
438
  program_id,
394
439
  ),
395
- &[owner.clone(), mailer_account.clone(), system_program.clone()],
440
+ &[
441
+ owner.clone(),
442
+ mailer_account.clone(),
443
+ system_program.clone(),
444
+ ],
396
445
  &[&[b"mailer", &[bump]]],
397
446
  )?;
398
447
 
@@ -440,6 +489,11 @@ fn process_send(
440
489
  }
441
490
 
442
491
  // Load mailer state
492
+ let (mailer_pda, _) = Pubkey::find_program_address(&[b"mailer"], program_id);
493
+ if mailer_account.key != &mailer_pda {
494
+ return Err(MailerError::InvalidPDA.into());
495
+ }
496
+
443
497
  let mailer_data = mailer_account.try_borrow_data()?;
444
498
  let mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
445
499
  drop(mailer_data);
@@ -450,16 +504,15 @@ fn process_send(
450
504
  }
451
505
 
452
506
  // Calculate effective fee based on custom discount (if any)
453
- let effective_fee = calculate_fee_with_discount(program_id, sender.key, accounts, mailer_state.send_fee)?;
507
+ let effective_fee =
508
+ calculate_fee_with_discount(program_id, sender.key, accounts, mailer_state.send_fee)?;
454
509
 
455
510
  if revenue_share_to_receiver {
456
511
  // Priority mode: full fee with revenue sharing
457
512
 
458
513
  // Create or load recipient claim account
459
- let (claim_pda, claim_bump) = Pubkey::find_program_address(
460
- &[b"claim", to.as_ref()],
461
- program_id
462
- );
514
+ let (claim_pda, claim_bump) =
515
+ Pubkey::find_program_address(&[b"claim", to.as_ref()], program_id);
463
516
 
464
517
  if recipient_claim.key != &claim_pda {
465
518
  return Err(MailerError::InvalidPDA.into());
@@ -479,13 +532,18 @@ fn process_send(
479
532
  space as u64,
480
533
  program_id,
481
534
  ),
482
- &[sender.clone(), recipient_claim.clone(), system_program.clone()],
535
+ &[
536
+ sender.clone(),
537
+ recipient_claim.clone(),
538
+ system_program.clone(),
539
+ ],
483
540
  &[&[b"claim", to.as_ref(), &[claim_bump]]],
484
541
  )?;
485
542
 
486
543
  // Initialize claim account
487
544
  let mut claim_data = recipient_claim.try_borrow_mut_data()?;
488
- claim_data[0..8].copy_from_slice(&hash_discriminator("account:RecipientClaim").to_le_bytes());
545
+ claim_data[0..8]
546
+ .copy_from_slice(&hash_discriminator("account:RecipientClaim").to_le_bytes());
489
547
 
490
548
  let claim_state = RecipientClaim {
491
549
  recipient: to,
@@ -552,7 +610,166 @@ fn process_send(
552
610
  mailer_state.owner_claimable += owner_fee;
553
611
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
554
612
 
555
- msg!("Standard mail sent from {} to {}: {} (resolve sender: {}, effective fee: {})", sender.key, to, subject, _resolve_sender_to_name, effective_fee);
613
+ msg!(
614
+ "Standard mail sent from {} to {}: {} (resolve sender: {}, effective fee: {})",
615
+ sender.key,
616
+ to,
617
+ subject,
618
+ _resolve_sender_to_name,
619
+ effective_fee
620
+ );
621
+ }
622
+
623
+ Ok(())
624
+ }
625
+
626
+ /// Send prepared message with optional revenue sharing (references off-chain content via mailId)
627
+ fn process_send_prepared(
628
+ program_id: &Pubkey,
629
+ accounts: &[AccountInfo],
630
+ to: Pubkey,
631
+ mail_id: String,
632
+ revenue_share_to_receiver: bool,
633
+ _resolve_sender_to_name: bool,
634
+ ) -> ProgramResult {
635
+ let account_iter = &mut accounts.iter();
636
+ let sender = next_account_info(account_iter)?;
637
+ let recipient_claim = next_account_info(account_iter)?;
638
+ let mailer_account = next_account_info(account_iter)?;
639
+ let sender_usdc = next_account_info(account_iter)?;
640
+ let mailer_usdc = next_account_info(account_iter)?;
641
+ let token_program = next_account_info(account_iter)?;
642
+ let system_program = next_account_info(account_iter)?;
643
+
644
+ if !sender.is_signer {
645
+ return Err(ProgramError::MissingRequiredSignature);
646
+ }
647
+
648
+ // Load mailer state
649
+ let mailer_data = mailer_account.try_borrow_data()?;
650
+ let mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
651
+ drop(mailer_data);
652
+
653
+ // Check if contract is paused
654
+ if mailer_state.paused {
655
+ return Err(MailerError::ContractPaused.into());
656
+ }
657
+
658
+ // Calculate effective fee based on custom discount (if any)
659
+ let effective_fee =
660
+ calculate_fee_with_discount(program_id, sender.key, accounts, mailer_state.send_fee)?;
661
+
662
+ if revenue_share_to_receiver {
663
+ // Priority mode: full fee with revenue sharing
664
+
665
+ // Create or load recipient claim account
666
+ let (claim_pda, claim_bump) =
667
+ Pubkey::find_program_address(&[b"claim", to.as_ref()], program_id);
668
+
669
+ if recipient_claim.key != &claim_pda {
670
+ return Err(MailerError::InvalidPDA.into());
671
+ }
672
+
673
+ // Create claim account if needed
674
+ if recipient_claim.lamports() == 0 {
675
+ let rent = Rent::get()?;
676
+ let space = 8 + RecipientClaim::LEN;
677
+ let lamports = rent.minimum_balance(space);
678
+
679
+ invoke_signed(
680
+ &system_instruction::create_account(
681
+ sender.key,
682
+ recipient_claim.key,
683
+ lamports,
684
+ space as u64,
685
+ program_id,
686
+ ),
687
+ &[
688
+ sender.clone(),
689
+ recipient_claim.clone(),
690
+ system_program.clone(),
691
+ ],
692
+ &[&[b"claim", to.as_ref(), &[claim_bump]]],
693
+ )?;
694
+
695
+ // Initialize claim account
696
+ let mut claim_data = recipient_claim.try_borrow_mut_data()?;
697
+ claim_data[0..8]
698
+ .copy_from_slice(&hash_discriminator("account:RecipientClaim").to_le_bytes());
699
+
700
+ let claim_state = RecipientClaim {
701
+ recipient: to,
702
+ amount: 0,
703
+ timestamp: 0,
704
+ bump: claim_bump,
705
+ };
706
+
707
+ claim_state.serialize(&mut &mut claim_data[8..])?;
708
+ drop(claim_data);
709
+ }
710
+
711
+ // Transfer effective fee (may be discounted)
712
+ if effective_fee > 0 {
713
+ invoke(
714
+ &spl_token::instruction::transfer(
715
+ token_program.key,
716
+ sender_usdc.key,
717
+ mailer_usdc.key,
718
+ sender.key,
719
+ &[],
720
+ effective_fee,
721
+ )?,
722
+ &[
723
+ sender_usdc.clone(),
724
+ mailer_usdc.clone(),
725
+ sender.clone(),
726
+ token_program.clone(),
727
+ ],
728
+ )?;
729
+
730
+ // Record revenue shares (only if fee > 0)
731
+ record_shares(recipient_claim, mailer_account, to, effective_fee)?;
732
+ }
733
+
734
+ msg!("Priority prepared mail sent from {} to {} (mailId: {}, revenue share enabled, resolve sender: {}, effective fee: {})", sender.key, to, mail_id, _resolve_sender_to_name, effective_fee);
735
+ } else {
736
+ // Standard mode: 10% fee only, no revenue sharing
737
+ let owner_fee = (effective_fee * 10) / 100; // 10% of effective fee
738
+
739
+ // Transfer only owner fee (10%)
740
+ if owner_fee > 0 {
741
+ invoke(
742
+ &spl_token::instruction::transfer(
743
+ token_program.key,
744
+ sender_usdc.key,
745
+ mailer_usdc.key,
746
+ sender.key,
747
+ &[],
748
+ owner_fee,
749
+ )?,
750
+ &[
751
+ sender_usdc.clone(),
752
+ mailer_usdc.clone(),
753
+ sender.clone(),
754
+ token_program.clone(),
755
+ ],
756
+ )?;
757
+ }
758
+
759
+ // Update owner claimable
760
+ let mut mailer_data = mailer_account.try_borrow_mut_data()?;
761
+ let mut mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
762
+ mailer_state.owner_claimable += owner_fee;
763
+ mailer_state.serialize(&mut &mut mailer_data[8..])?;
764
+
765
+ msg!(
766
+ "Standard prepared mail sent from {} to {} (mailId: {}, resolve sender: {}, effective fee: {})",
767
+ sender.key,
768
+ to,
769
+ mail_id,
770
+ _resolve_sender_to_name,
771
+ effective_fee
772
+ );
556
773
  }
557
774
 
558
775
  Ok(())
@@ -588,7 +805,8 @@ fn process_send_to_email(
588
805
  }
589
806
 
590
807
  // Calculate effective fee based on custom discount (if any)
591
- let effective_fee = calculate_fee_with_discount(_program_id, sender.key, accounts, mailer_state.send_fee)?;
808
+ let effective_fee =
809
+ calculate_fee_with_discount(_program_id, sender.key, accounts, mailer_state.send_fee)?;
592
810
 
593
811
  // Calculate 10% owner fee (no revenue share since no wallet address)
594
812
  let owner_fee = (effective_fee * 10) / 100;
@@ -621,7 +839,13 @@ fn process_send_to_email(
621
839
  mailer_state.owner_claimable += owner_fee;
622
840
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
623
841
 
624
- msg!("Mail sent from {} to email {}: {} (effective fee: {})", sender.key, to_email, subject, effective_fee);
842
+ msg!(
843
+ "Mail sent from {} to email {}: {} (effective fee: {})",
844
+ sender.key,
845
+ to_email,
846
+ subject,
847
+ effective_fee
848
+ );
625
849
 
626
850
  Ok(())
627
851
  }
@@ -655,7 +879,8 @@ fn process_send_prepared_to_email(
655
879
  }
656
880
 
657
881
  // Calculate effective fee based on custom discount (if any)
658
- let effective_fee = calculate_fee_with_discount(_program_id, sender.key, accounts, mailer_state.send_fee)?;
882
+ let effective_fee =
883
+ calculate_fee_with_discount(_program_id, sender.key, accounts, mailer_state.send_fee)?;
659
884
 
660
885
  // Calculate 10% owner fee (no revenue share since no wallet address)
661
886
  let owner_fee = (effective_fee * 10) / 100;
@@ -688,7 +913,13 @@ fn process_send_prepared_to_email(
688
913
  mailer_state.owner_claimable += owner_fee;
689
914
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
690
915
 
691
- msg!("Prepared mail sent from {} to email {} (mailId: {}, effective fee: {})", sender.key, to_email, mail_id, effective_fee);
916
+ msg!(
917
+ "Prepared mail sent from {} to email {} (mailId: {}, effective fee: {})",
918
+ sender.key,
919
+ to_email,
920
+ mail_id,
921
+ effective_fee
922
+ );
692
923
 
693
924
  Ok(())
694
925
  }
@@ -871,10 +1102,8 @@ fn process_delegate_to(
871
1102
  }
872
1103
 
873
1104
  // Verify delegation account PDA
874
- let (delegation_pda, delegation_bump) = Pubkey::find_program_address(
875
- &[b"delegation", delegator.key.as_ref()],
876
- program_id
877
- );
1105
+ let (delegation_pda, delegation_bump) =
1106
+ Pubkey::find_program_address(&[b"delegation", delegator.key.as_ref()], program_id);
878
1107
 
879
1108
  if delegation_account.key != &delegation_pda {
880
1109
  return Err(MailerError::InvalidPDA.into());
@@ -904,7 +1133,8 @@ fn process_delegate_to(
904
1133
 
905
1134
  // Initialize delegation account
906
1135
  let mut delegation_data = delegation_account.try_borrow_mut_data()?;
907
- delegation_data[0..8].copy_from_slice(&hash_discriminator("account:Delegation").to_le_bytes());
1136
+ delegation_data[0..8]
1137
+ .copy_from_slice(&hash_discriminator("account:Delegation").to_le_bytes());
908
1138
 
909
1139
  let delegation_state = Delegation {
910
1140
  delegator: *delegator.key,
@@ -940,7 +1170,8 @@ fn process_delegate_to(
940
1170
 
941
1171
  // Update delegation
942
1172
  let mut delegation_data = delegation_account.try_borrow_mut_data()?;
943
- let mut delegation_state: Delegation = BorshDeserialize::deserialize(&mut &delegation_data[8..])?;
1173
+ let mut delegation_state: Delegation =
1174
+ BorshDeserialize::deserialize(&mut &delegation_data[8..])?;
944
1175
  delegation_state.delegate = delegate;
945
1176
  delegation_state.serialize(&mut &mut delegation_data[8..])?;
946
1177
 
@@ -949,18 +1180,34 @@ fn process_delegate_to(
949
1180
  }
950
1181
 
951
1182
  /// Reject delegation
952
- fn process_reject_delegation(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
1183
+ fn process_reject_delegation(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
953
1184
  let account_iter = &mut accounts.iter();
954
1185
  let rejector = next_account_info(account_iter)?;
955
1186
  let delegation_account = next_account_info(account_iter)?;
1187
+ let mailer_account = next_account_info(account_iter)?;
956
1188
 
957
1189
  if !rejector.is_signer {
958
1190
  return Err(ProgramError::MissingRequiredSignature);
959
1191
  }
960
1192
 
1193
+ // Verify mailer state PDA and ensure contract is not paused
1194
+ let (mailer_pda, _) = Pubkey::find_program_address(&[b"mailer"], program_id);
1195
+ if mailer_account.key != &mailer_pda {
1196
+ return Err(MailerError::InvalidPDA.into());
1197
+ }
1198
+
1199
+ let mailer_data = mailer_account.try_borrow_data()?;
1200
+ let mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
1201
+ drop(mailer_data);
1202
+
1203
+ if mailer_state.paused {
1204
+ return Err(MailerError::ContractPaused.into());
1205
+ }
1206
+
961
1207
  // Load and update delegation state
962
1208
  let mut delegation_data = delegation_account.try_borrow_mut_data()?;
963
- let mut delegation_state: Delegation = BorshDeserialize::deserialize(&mut &delegation_data[8..])?;
1209
+ let mut delegation_state: Delegation =
1210
+ BorshDeserialize::deserialize(&mut &delegation_data[8..])?;
964
1211
 
965
1212
  // Verify the rejector is the current delegate
966
1213
  if delegation_state.delegate != Some(*rejector.key) {
@@ -1048,10 +1295,8 @@ fn process_set_custom_fee_percentage(
1048
1295
  }
1049
1296
 
1050
1297
  // Verify fee discount account PDA
1051
- let (discount_pda, bump) = Pubkey::find_program_address(
1052
- &[b"discount", account.as_ref()],
1053
- program_id
1054
- );
1298
+ let (discount_pda, bump) =
1299
+ Pubkey::find_program_address(&[b"discount", account.as_ref()], program_id);
1055
1300
 
1056
1301
  if fee_discount_account.key != &discount_pda {
1057
1302
  return Err(MailerError::InvalidPDA.into());
@@ -1071,13 +1316,18 @@ fn process_set_custom_fee_percentage(
1071
1316
  space as u64,
1072
1317
  program_id,
1073
1318
  ),
1074
- &[payer.clone(), fee_discount_account.clone(), system_program.clone()],
1319
+ &[
1320
+ payer.clone(),
1321
+ fee_discount_account.clone(),
1322
+ system_program.clone(),
1323
+ ],
1075
1324
  &[&[b"discount", account.as_ref(), &[bump]]],
1076
1325
  )?;
1077
1326
 
1078
1327
  // Initialize discount account
1079
1328
  let mut discount_data = fee_discount_account.try_borrow_mut_data()?;
1080
- discount_data[0..8].copy_from_slice(&hash_discriminator("account:FeeDiscount").to_le_bytes());
1329
+ discount_data[0..8]
1330
+ .copy_from_slice(&hash_discriminator("account:FeeDiscount").to_le_bytes());
1081
1331
 
1082
1332
  let fee_discount = FeeDiscount {
1083
1333
  account,
@@ -1089,7 +1339,8 @@ fn process_set_custom_fee_percentage(
1089
1339
  } else {
1090
1340
  // Update existing discount account
1091
1341
  let mut discount_data = fee_discount_account.try_borrow_mut_data()?;
1092
- let mut fee_discount: FeeDiscount = BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1342
+ let mut fee_discount: FeeDiscount =
1343
+ BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1093
1344
  fee_discount.discount = 100 - percentage; // Store as discount
1094
1345
  fee_discount.serialize(&mut &mut discount_data[8..])?;
1095
1346
  }
@@ -1128,10 +1379,8 @@ fn process_clear_custom_fee_percentage(
1128
1379
  }
1129
1380
 
1130
1381
  // Verify fee discount account PDA
1131
- let (discount_pda, _) = Pubkey::find_program_address(
1132
- &[b"discount", account.as_ref()],
1133
- program_id
1134
- );
1382
+ let (discount_pda, _) =
1383
+ Pubkey::find_program_address(&[b"discount", account.as_ref()], program_id);
1135
1384
 
1136
1385
  if fee_discount_account.key != &discount_pda {
1137
1386
  return Err(MailerError::InvalidPDA.into());
@@ -1140,12 +1389,16 @@ fn process_clear_custom_fee_percentage(
1140
1389
  // Clear by setting discount to 0 (no discount = 100% fee = default behavior)
1141
1390
  if fee_discount_account.lamports() > 0 {
1142
1391
  let mut discount_data = fee_discount_account.try_borrow_mut_data()?;
1143
- let mut fee_discount: FeeDiscount = BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1392
+ let mut fee_discount: FeeDiscount =
1393
+ BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1144
1394
  fee_discount.discount = 0; // 0 discount = 100% fee = default
1145
1395
  fee_discount.serialize(&mut &mut discount_data[8..])?;
1146
1396
  }
1147
1397
 
1148
- msg!("Custom fee percentage cleared for {} (reset to 100%)", account);
1398
+ msg!(
1399
+ "Custom fee percentage cleared for {} (reset to 100%)",
1400
+ account
1401
+ );
1149
1402
  Ok(())
1150
1403
  }
1151
1404
 
@@ -1175,7 +1428,11 @@ fn record_shares(
1175
1428
  mailer_state.owner_claimable += owner_amount;
1176
1429
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
1177
1430
 
1178
- msg!("Shares recorded: recipient {}, owner {}", recipient_amount, owner_amount);
1431
+ msg!(
1432
+ "Shares recorded: recipient {}, owner {}",
1433
+ recipient_amount,
1434
+ owner_amount
1435
+ );
1179
1436
  Ok(())
1180
1437
  }
1181
1438
 
@@ -1189,10 +1446,8 @@ fn calculate_fee_with_discount(
1189
1446
  base_fee: u64,
1190
1447
  ) -> Result<u64, ProgramError> {
1191
1448
  // Try to find fee discount account
1192
- let (discount_pda, _) = Pubkey::find_program_address(
1193
- &[b"discount", account.as_ref()],
1194
- program_id
1195
- );
1449
+ let (discount_pda, _) =
1450
+ Pubkey::find_program_address(&[b"discount", account.as_ref()], program_id);
1196
1451
 
1197
1452
  // Check if any account in the accounts slice matches the discount PDA
1198
1453
  let discount_account = accounts.iter().find(|acc| acc.key == &discount_pda);
@@ -1202,7 +1457,8 @@ fn calculate_fee_with_discount(
1202
1457
  if discount_acc.lamports() > 0 {
1203
1458
  let discount_data = discount_acc.try_borrow_data()?;
1204
1459
  if discount_data.len() >= 8 + FeeDiscount::LEN {
1205
- let fee_discount: FeeDiscount = BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1460
+ let fee_discount: FeeDiscount =
1461
+ BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1206
1462
  let discount = fee_discount.discount as u64;
1207
1463
 
1208
1464
  // Apply discount: fee = base_fee * (100 - discount) / 100
@@ -1263,7 +1519,12 @@ fn process_pause(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResul
1263
1519
  &[],
1264
1520
  amount,
1265
1521
  )?,
1266
- &[mailer_usdc.clone(), owner_usdc.clone(), token_program.clone()],
1522
+ &[
1523
+ mailer_usdc.clone(),
1524
+ owner_usdc.clone(),
1525
+ mailer_account.clone(),
1526
+ token_program.clone(),
1527
+ ],
1267
1528
  &[&[b"mailer", &[bump]]],
1268
1529
  )?;
1269
1530
 
@@ -1272,7 +1533,7 @@ fn process_pause(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResul
1272
1533
 
1273
1534
  // Save updated state
1274
1535
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
1275
-
1536
+
1276
1537
  msg!("Contract paused by owner: {}", owner.key);
1277
1538
  Ok(())
1278
1539
  }
@@ -1304,40 +1565,8 @@ fn process_unpause(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramRes
1304
1565
  // Set unpaused state
1305
1566
  mailer_state.paused = false;
1306
1567
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
1307
-
1308
- msg!("Contract unpaused by owner: {}", owner.key);
1309
- Ok(())
1310
- }
1311
1568
 
1312
- /// Emergency unpause without fund distribution (owner only)
1313
- fn process_emergency_unpause(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
1314
- let account_iter = &mut accounts.iter();
1315
- let owner = next_account_info(account_iter)?;
1316
- let mailer_account = next_account_info(account_iter)?;
1317
-
1318
- if !owner.is_signer {
1319
- return Err(ProgramError::MissingRequiredSignature);
1320
- }
1321
-
1322
- // Load and update mailer state
1323
- let mut mailer_data = mailer_account.try_borrow_mut_data()?;
1324
- let mut mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
1325
-
1326
- // Verify owner
1327
- if mailer_state.owner != *owner.key {
1328
- return Err(MailerError::OnlyOwner.into());
1329
- }
1330
-
1331
- // Check if not paused
1332
- if !mailer_state.paused {
1333
- return Err(MailerError::ContractNotPaused.into());
1334
- }
1335
-
1336
- // Set unpaused state without fund distribution
1337
- mailer_state.paused = false;
1338
- mailer_state.serialize(&mut &mut mailer_data[8..])?;
1339
-
1340
- msg!("Contract emergency unpaused by owner: {} - funds can be claimed manually", owner.key);
1569
+ msg!("Contract unpaused by owner: {}", owner.key);
1341
1570
  Ok(())
1342
1571
  }
1343
1572
 
@@ -1394,16 +1623,114 @@ fn process_distribute_claimable_funds(
1394
1623
  &[],
1395
1624
  amount,
1396
1625
  )?,
1397
- &[mailer_usdc.clone(), recipient_usdc.clone(), token_program.clone()],
1626
+ &[
1627
+ mailer_usdc.clone(),
1628
+ recipient_usdc.clone(),
1629
+ mailer_account.clone(),
1630
+ token_program.clone(),
1631
+ ],
1398
1632
  &[&[b"mailer", &[bump]]],
1399
1633
  )?;
1400
1634
 
1401
1635
  claim_state.serialize(&mut &mut claim_data[8..])?;
1402
-
1636
+
1403
1637
  msg!("Distributed claimable funds to {}: {}", recipient, amount);
1404
1638
  Ok(())
1405
1639
  }
1406
1640
 
1641
+ /// Claim expired shares and move them under owner control (owner only)
1642
+ fn process_claim_expired_shares(
1643
+ program_id: &Pubkey,
1644
+ accounts: &[AccountInfo],
1645
+ recipient: Pubkey,
1646
+ ) -> ProgramResult {
1647
+ let account_iter = &mut accounts.iter();
1648
+ let owner = next_account_info(account_iter)?;
1649
+ let mailer_account = next_account_info(account_iter)?;
1650
+ let recipient_claim_account = next_account_info(account_iter)?;
1651
+
1652
+ if !owner.is_signer {
1653
+ return Err(ProgramError::MissingRequiredSignature);
1654
+ }
1655
+
1656
+ // Load and verify mailer state
1657
+ let mut mailer_data = mailer_account.try_borrow_mut_data()?;
1658
+ let mut mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
1659
+
1660
+ if mailer_state.owner != *owner.key {
1661
+ return Err(MailerError::OnlyOwner.into());
1662
+ }
1663
+
1664
+ // Verify recipient claim PDA
1665
+ let (claim_pda, _) = Pubkey::find_program_address(&[b"claim", recipient.as_ref()], program_id);
1666
+ if recipient_claim_account.key != &claim_pda {
1667
+ return Err(MailerError::InvalidPDA.into());
1668
+ }
1669
+
1670
+ // Load and validate claim state
1671
+ let mut claim_data = recipient_claim_account.try_borrow_mut_data()?;
1672
+ let mut claim_state: RecipientClaim = BorshDeserialize::deserialize(&mut &claim_data[8..])?;
1673
+
1674
+ if claim_state.recipient != recipient {
1675
+ return Err(MailerError::InvalidRecipient.into());
1676
+ }
1677
+ if claim_state.amount == 0 {
1678
+ return Err(MailerError::NoClaimableAmount.into());
1679
+ }
1680
+
1681
+ let current_time = Clock::get()?.unix_timestamp;
1682
+ if current_time <= claim_state.timestamp + CLAIM_PERIOD {
1683
+ return Err(MailerError::ClaimPeriodNotExpired.into());
1684
+ }
1685
+
1686
+ let amount = claim_state.amount;
1687
+ claim_state.amount = 0;
1688
+ claim_state.timestamp = 0;
1689
+ claim_state.serialize(&mut &mut claim_data[8..])?;
1690
+ drop(claim_data);
1691
+
1692
+ mailer_state.owner_claimable += amount;
1693
+ mailer_state.serialize(&mut &mut mailer_data[8..])?;
1694
+
1695
+ msg!("Expired shares claimed for {}: {}", recipient, amount);
1696
+ Ok(())
1697
+ }
1698
+
1699
+ /// Emergency unpause without fund distribution (owner only)
1700
+ fn process_emergency_unpause(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
1701
+ let account_iter = &mut accounts.iter();
1702
+ let owner = next_account_info(account_iter)?;
1703
+ let mailer_account = next_account_info(account_iter)?;
1704
+
1705
+ if !owner.is_signer {
1706
+ return Err(ProgramError::MissingRequiredSignature);
1707
+ }
1708
+
1709
+ // Load and update mailer state
1710
+ let mut mailer_data = mailer_account.try_borrow_mut_data()?;
1711
+ let mut mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
1712
+
1713
+ // Verify owner
1714
+ if mailer_state.owner != *owner.key {
1715
+ return Err(MailerError::OnlyOwner.into());
1716
+ }
1717
+
1718
+ // Check if not paused
1719
+ if !mailer_state.paused {
1720
+ return Err(MailerError::ContractNotPaused.into());
1721
+ }
1722
+
1723
+ // Set unpaused state without fund distribution
1724
+ mailer_state.paused = false;
1725
+ mailer_state.serialize(&mut &mut mailer_data[8..])?;
1726
+
1727
+ msg!(
1728
+ "Contract emergency unpaused by owner: {} - funds can be claimed manually",
1729
+ owner.key
1730
+ );
1731
+ Ok(())
1732
+ }
1733
+
1407
1734
  /// Simple hash function for account discriminators
1408
1735
  fn hash_discriminator(name: &str) -> u64 {
1409
1736
  use std::collections::hash_map::DefaultHasher;