@sudobility/contracts 1.9.0 → 1.10.0

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 (32) hide show
  1. package/artifacts/contracts/Mailer.sol/Mailer.d.ts +963 -0
  2. package/artifacts/contracts/Mailer.sol/Mailer.dbg.json +1 -1
  3. package/artifacts/contracts/Mailer.sol/Mailer.json +95 -2
  4. package/artifacts/contracts/Mailer.sol/artifacts.d.ts +21 -0
  5. package/artifacts/contracts/MockUSDC.sol/MockUSDC.d.ts +284 -0
  6. package/artifacts/contracts/MockUSDC.sol/MockUSDC.dbg.json +1 -1
  7. package/artifacts/contracts/MockUSDC.sol/artifacts.d.ts +21 -0
  8. package/artifacts/contracts/interfaces/IERC20.sol/IERC20.d.ts +157 -0
  9. package/artifacts/contracts/interfaces/IERC20.sol/IERC20.dbg.json +1 -1
  10. package/artifacts/contracts/interfaces/IERC20.sol/artifacts.d.ts +21 -0
  11. package/dist/evm/typechain-types/Mailer.d.ts +51 -2
  12. package/dist/evm/typechain-types/Mailer.d.ts.map +1 -1
  13. package/dist/evm/typechain-types/factories/Mailer__factory.d.ts +72 -1
  14. package/dist/evm/typechain-types/factories/Mailer__factory.d.ts.map +1 -1
  15. package/dist/evm/typechain-types/factories/Mailer__factory.js +94 -1
  16. package/dist/evm/typechain-types/factories/Mailer__factory.js.map +1 -1
  17. package/dist/unified/typechain-types/Mailer.d.ts +51 -2
  18. package/dist/unified/typechain-types/Mailer.d.ts.map +1 -1
  19. package/dist/unified/typechain-types/factories/Mailer__factory.d.ts +72 -1
  20. package/dist/unified/typechain-types/factories/Mailer__factory.d.ts.map +1 -1
  21. package/dist/unified/typechain-types/factories/Mailer__factory.js +94 -1
  22. package/dist/unified/typechain-types/factories/Mailer__factory.js.map +1 -1
  23. package/dist/unified-esm/typechain-types/Mailer.d.ts +51 -2
  24. package/dist/unified-esm/typechain-types/Mailer.d.ts.map +1 -1
  25. package/dist/unified-esm/typechain-types/factories/Mailer__factory.d.ts +72 -1
  26. package/dist/unified-esm/typechain-types/factories/Mailer__factory.d.ts.map +1 -1
  27. package/dist/unified-esm/typechain-types/factories/Mailer__factory.js +94 -1
  28. package/dist/unified-esm/typechain-types/factories/Mailer__factory.js.map +1 -1
  29. package/package.json +7 -3
  30. package/programs/mailer/src/lib.rs +314 -78
  31. package/typechain-types/Mailer.ts +104 -0
  32. package/typechain-types/factories/Mailer__factory.ts +94 -1
@@ -97,6 +97,20 @@ impl Delegation {
97
97
  pub const LEN: usize = 32 + 1 + 32 + 1; // 66 bytes (max with Some(Pubkey))
98
98
  }
99
99
 
100
+ /// Fee discount account for custom fee percentages
101
+ /// Stores discount (0-100) instead of percentage for cleaner default behavior
102
+ /// 0 = no discount (100% fee), 100 = full discount (0% fee, free)
103
+ #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
104
+ pub struct FeeDiscount {
105
+ pub account: Pubkey,
106
+ pub discount: u8, // 0-100: 0 = no discount (full fee), 100 = full discount (free)
107
+ pub bump: u8,
108
+ }
109
+
110
+ impl FeeDiscount {
111
+ pub const LEN: usize = 32 + 1 + 1; // 34 bytes
112
+ }
113
+
100
114
  /// Instructions
101
115
  #[derive(BorshSerialize, BorshDeserialize, Debug)]
102
116
  pub enum MailerInstruction {
@@ -198,7 +212,27 @@ pub enum MailerInstruction {
198
212
  /// 0. `[signer]` Owner
199
213
  /// 1. `[writable]` Mailer state account (PDA)
200
214
  SetDelegationFee { new_fee: u64 },
201
-
215
+
216
+ /// Set custom fee percentage for a specific address (owner only)
217
+ /// Accounts:
218
+ /// 0. `[signer]` Owner
219
+ /// 1. `[]` Mailer state account (PDA)
220
+ /// 2. `[writable]` Fee discount account (PDA)
221
+ /// 3. `[]` Account to set custom fee for
222
+ /// 4. `[signer]` Payer for account creation
223
+ /// 5. `[]` System program
224
+ SetCustomFeePercentage {
225
+ account: Pubkey,
226
+ percentage: u8, // 0-100: 0 = free, 100 = full fee
227
+ },
228
+
229
+ /// Clear custom fee percentage for a specific address (owner only)
230
+ /// Accounts:
231
+ /// 0. `[signer]` Owner
232
+ /// 1. `[]` Mailer state account (PDA)
233
+ /// 2. `[writable]` Fee discount account (PDA)
234
+ ClearCustomFeePercentage { account: Pubkey },
235
+
202
236
  /// Pause the contract (owner only)
203
237
  /// Accounts:
204
238
  /// 0. `[signer]` Owner
@@ -260,6 +294,8 @@ pub enum MailerError {
260
294
  ContractPaused,
261
295
  #[error("Contract is not paused")]
262
296
  ContractNotPaused,
297
+ #[error("Invalid percentage (must be 0-100)")]
298
+ InvalidPercentage,
263
299
  }
264
300
 
265
301
  impl From<MailerError> for ProgramError {
@@ -301,6 +337,12 @@ pub fn process_instruction(
301
337
  MailerInstruction::SetDelegationFee { new_fee } => {
302
338
  process_set_delegation_fee(program_id, accounts, new_fee)
303
339
  }
340
+ MailerInstruction::SetCustomFeePercentage { account, percentage } => {
341
+ process_set_custom_fee_percentage(program_id, accounts, account, percentage)
342
+ }
343
+ MailerInstruction::ClearCustomFeePercentage { account } => {
344
+ process_clear_custom_fee_percentage(program_id, accounts, account)
345
+ }
304
346
  MailerInstruction::Pause => {
305
347
  process_pause(program_id, accounts)
306
348
  }
@@ -407,6 +449,9 @@ fn process_send(
407
449
  return Err(MailerError::ContractPaused.into());
408
450
  }
409
451
 
452
+ // 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)?;
454
+
410
455
  if revenue_share_to_receiver {
411
456
  // Priority mode: full fee with revenue sharing
412
457
 
@@ -453,50 +498,53 @@ fn process_send(
453
498
  drop(claim_data);
454
499
  }
455
500
 
456
- // Transfer full send fee
457
- invoke(
458
- &spl_token::instruction::transfer(
459
- token_program.key,
460
- sender_usdc.key,
461
- mailer_usdc.key,
462
- sender.key,
463
- &[],
464
- mailer_state.send_fee,
465
- )?,
466
- &[
467
- sender_usdc.clone(),
468
- mailer_usdc.clone(),
469
- sender.clone(),
470
- token_program.clone(),
471
- ],
472
- )?;
501
+ // Transfer effective fee (may be discounted)
502
+ if effective_fee > 0 {
503
+ invoke(
504
+ &spl_token::instruction::transfer(
505
+ token_program.key,
506
+ sender_usdc.key,
507
+ mailer_usdc.key,
508
+ sender.key,
509
+ &[],
510
+ effective_fee,
511
+ )?,
512
+ &[
513
+ sender_usdc.clone(),
514
+ mailer_usdc.clone(),
515
+ sender.clone(),
516
+ token_program.clone(),
517
+ ],
518
+ )?;
473
519
 
474
- // Record revenue shares
475
- record_shares(recipient_claim, mailer_account, to, mailer_state.send_fee)?;
520
+ // Record revenue shares (only if fee > 0)
521
+ record_shares(recipient_claim, mailer_account, to, effective_fee)?;
522
+ }
476
523
 
477
- msg!("Priority mail sent from {} to {}: {} (revenue share enabled, resolve sender: {})", sender.key, to, subject, _resolve_sender_to_name);
524
+ msg!("Priority mail sent from {} to {}: {} (revenue share enabled, resolve sender: {}, effective fee: {})", sender.key, to, subject, _resolve_sender_to_name, effective_fee);
478
525
  } else {
479
526
  // Standard mode: 10% fee only, no revenue sharing
480
-
481
- let owner_fee = mailer_state.send_fee / 10; // 10% of send_fee
527
+ let owner_fee = (effective_fee * 10) / 100; // 10% of effective fee
482
528
 
483
529
  // Transfer only owner fee (10%)
484
- invoke(
485
- &spl_token::instruction::transfer(
486
- token_program.key,
487
- sender_usdc.key,
488
- mailer_usdc.key,
489
- sender.key,
490
- &[],
491
- owner_fee,
492
- )?,
493
- &[
494
- sender_usdc.clone(),
495
- mailer_usdc.clone(),
496
- sender.clone(),
497
- token_program.clone(),
498
- ],
499
- )?;
530
+ if owner_fee > 0 {
531
+ invoke(
532
+ &spl_token::instruction::transfer(
533
+ token_program.key,
534
+ sender_usdc.key,
535
+ mailer_usdc.key,
536
+ sender.key,
537
+ &[],
538
+ owner_fee,
539
+ )?,
540
+ &[
541
+ sender_usdc.clone(),
542
+ mailer_usdc.clone(),
543
+ sender.clone(),
544
+ token_program.clone(),
545
+ ],
546
+ )?;
547
+ }
500
548
 
501
549
  // Update owner claimable
502
550
  let mut mailer_data = mailer_account.try_borrow_mut_data()?;
@@ -504,7 +552,7 @@ fn process_send(
504
552
  mailer_state.owner_claimable += owner_fee;
505
553
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
506
554
 
507
- msg!("Standard mail sent from {} to {}: {} (resolve sender: {})", sender.key, to, subject, _resolve_sender_to_name);
555
+ msg!("Standard mail sent from {} to {}: {} (resolve sender: {}, effective fee: {})", sender.key, to, subject, _resolve_sender_to_name, effective_fee);
508
556
  }
509
557
 
510
558
  Ok(())
@@ -539,28 +587,33 @@ fn process_send_to_email(
539
587
  return Err(MailerError::ContractPaused.into());
540
588
  }
541
589
 
590
+ // 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)?;
592
+
542
593
  // Calculate 10% owner fee (no revenue share since no wallet address)
543
- let owner_fee = (mailer_state.send_fee * 10) / 100;
594
+ let owner_fee = (effective_fee * 10) / 100;
544
595
 
545
596
  // Transfer fee from sender to mailer
546
- let transfer_ix = spl_token::instruction::transfer(
547
- token_program.key,
548
- sender_usdc.key,
549
- mailer_usdc.key,
550
- sender.key,
551
- &[],
552
- owner_fee,
553
- )?;
597
+ if owner_fee > 0 {
598
+ let transfer_ix = spl_token::instruction::transfer(
599
+ token_program.key,
600
+ sender_usdc.key,
601
+ mailer_usdc.key,
602
+ sender.key,
603
+ &[],
604
+ owner_fee,
605
+ )?;
554
606
 
555
- invoke(
556
- &transfer_ix,
557
- &[
558
- sender_usdc.clone(),
559
- mailer_usdc.clone(),
560
- sender.clone(),
561
- token_program.clone(),
562
- ],
563
- )?;
607
+ invoke(
608
+ &transfer_ix,
609
+ &[
610
+ sender_usdc.clone(),
611
+ mailer_usdc.clone(),
612
+ sender.clone(),
613
+ token_program.clone(),
614
+ ],
615
+ )?;
616
+ }
564
617
 
565
618
  // Update owner claimable
566
619
  let mut mailer_data = mailer_account.try_borrow_mut_data()?;
@@ -568,7 +621,7 @@ fn process_send_to_email(
568
621
  mailer_state.owner_claimable += owner_fee;
569
622
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
570
623
 
571
- msg!("Mail sent from {} to email {}: {}", sender.key, to_email, subject);
624
+ msg!("Mail sent from {} to email {}: {} (effective fee: {})", sender.key, to_email, subject, effective_fee);
572
625
 
573
626
  Ok(())
574
627
  }
@@ -601,28 +654,33 @@ fn process_send_prepared_to_email(
601
654
  return Err(MailerError::ContractPaused.into());
602
655
  }
603
656
 
657
+ // 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)?;
659
+
604
660
  // Calculate 10% owner fee (no revenue share since no wallet address)
605
- let owner_fee = (mailer_state.send_fee * 10) / 100;
661
+ let owner_fee = (effective_fee * 10) / 100;
606
662
 
607
663
  // Transfer fee from sender to mailer
608
- let transfer_ix = spl_token::instruction::transfer(
609
- token_program.key,
610
- sender_usdc.key,
611
- mailer_usdc.key,
612
- sender.key,
613
- &[],
614
- owner_fee,
615
- )?;
664
+ if owner_fee > 0 {
665
+ let transfer_ix = spl_token::instruction::transfer(
666
+ token_program.key,
667
+ sender_usdc.key,
668
+ mailer_usdc.key,
669
+ sender.key,
670
+ &[],
671
+ owner_fee,
672
+ )?;
616
673
 
617
- invoke(
618
- &transfer_ix,
619
- &[
620
- sender_usdc.clone(),
621
- mailer_usdc.clone(),
622
- sender.clone(),
623
- token_program.clone(),
624
- ],
625
- )?;
674
+ invoke(
675
+ &transfer_ix,
676
+ &[
677
+ sender_usdc.clone(),
678
+ mailer_usdc.clone(),
679
+ sender.clone(),
680
+ token_program.clone(),
681
+ ],
682
+ )?;
683
+ }
626
684
 
627
685
  // Update owner claimable
628
686
  let mut mailer_data = mailer_account.try_borrow_mut_data()?;
@@ -630,7 +688,7 @@ fn process_send_prepared_to_email(
630
688
  mailer_state.owner_claimable += owner_fee;
631
689
  mailer_state.serialize(&mut &mut mailer_data[8..])?;
632
690
 
633
- msg!("Prepared mail sent from {} to email {} (mailId: {})", sender.key, to_email, mail_id);
691
+ msg!("Prepared mail sent from {} to email {} (mailId: {}, effective fee: {})", sender.key, to_email, mail_id, effective_fee);
634
692
 
635
693
  Ok(())
636
694
  }
@@ -951,6 +1009,146 @@ fn process_set_delegation_fee(
951
1009
  Ok(())
952
1010
  }
953
1011
 
1012
+ /// Set custom fee percentage for a specific address (owner only)
1013
+ fn process_set_custom_fee_percentage(
1014
+ program_id: &Pubkey,
1015
+ accounts: &[AccountInfo],
1016
+ account: Pubkey,
1017
+ percentage: u8,
1018
+ ) -> ProgramResult {
1019
+ let account_iter = &mut accounts.iter();
1020
+ let owner = next_account_info(account_iter)?;
1021
+ let mailer_account = next_account_info(account_iter)?;
1022
+ let fee_discount_account = next_account_info(account_iter)?;
1023
+ let _target_account = next_account_info(account_iter)?;
1024
+ let payer = next_account_info(account_iter)?;
1025
+ let system_program = next_account_info(account_iter)?;
1026
+
1027
+ if !owner.is_signer {
1028
+ return Err(ProgramError::MissingRequiredSignature);
1029
+ }
1030
+
1031
+ // Load mailer state and verify owner
1032
+ let mailer_data = mailer_account.try_borrow_data()?;
1033
+ let mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
1034
+ drop(mailer_data);
1035
+
1036
+ if mailer_state.owner != *owner.key {
1037
+ return Err(MailerError::OnlyOwner.into());
1038
+ }
1039
+
1040
+ // Check if contract is paused
1041
+ if mailer_state.paused {
1042
+ return Err(MailerError::ContractPaused.into());
1043
+ }
1044
+
1045
+ // Validate percentage
1046
+ if percentage > 100 {
1047
+ return Err(MailerError::InvalidPercentage.into());
1048
+ }
1049
+
1050
+ // Verify fee discount account PDA
1051
+ let (discount_pda, bump) = Pubkey::find_program_address(
1052
+ &[b"discount", account.as_ref()],
1053
+ program_id
1054
+ );
1055
+
1056
+ if fee_discount_account.key != &discount_pda {
1057
+ return Err(MailerError::InvalidPDA.into());
1058
+ }
1059
+
1060
+ // Create or update fee discount account
1061
+ if fee_discount_account.lamports() == 0 {
1062
+ let rent = Rent::get()?;
1063
+ let space = 8 + FeeDiscount::LEN;
1064
+ let lamports = rent.minimum_balance(space);
1065
+
1066
+ invoke_signed(
1067
+ &system_instruction::create_account(
1068
+ payer.key,
1069
+ fee_discount_account.key,
1070
+ lamports,
1071
+ space as u64,
1072
+ program_id,
1073
+ ),
1074
+ &[payer.clone(), fee_discount_account.clone(), system_program.clone()],
1075
+ &[&[b"discount", account.as_ref(), &[bump]]],
1076
+ )?;
1077
+
1078
+ // Initialize discount account
1079
+ 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());
1081
+
1082
+ let fee_discount = FeeDiscount {
1083
+ account,
1084
+ discount: 100 - percentage, // Store as discount: 0% fee = 100 discount, 100% fee = 0 discount
1085
+ bump,
1086
+ };
1087
+
1088
+ fee_discount.serialize(&mut &mut discount_data[8..])?;
1089
+ } else {
1090
+ // Update existing discount account
1091
+ let mut discount_data = fee_discount_account.try_borrow_mut_data()?;
1092
+ let mut fee_discount: FeeDiscount = BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1093
+ fee_discount.discount = 100 - percentage; // Store as discount
1094
+ fee_discount.serialize(&mut &mut discount_data[8..])?;
1095
+ }
1096
+
1097
+ msg!("Custom fee percentage set for {}: {}%", account, percentage);
1098
+ Ok(())
1099
+ }
1100
+
1101
+ /// Clear custom fee percentage for a specific address (owner only)
1102
+ fn process_clear_custom_fee_percentage(
1103
+ program_id: &Pubkey,
1104
+ accounts: &[AccountInfo],
1105
+ account: Pubkey,
1106
+ ) -> ProgramResult {
1107
+ let account_iter = &mut accounts.iter();
1108
+ let owner = next_account_info(account_iter)?;
1109
+ let mailer_account = next_account_info(account_iter)?;
1110
+ let fee_discount_account = next_account_info(account_iter)?;
1111
+
1112
+ if !owner.is_signer {
1113
+ return Err(ProgramError::MissingRequiredSignature);
1114
+ }
1115
+
1116
+ // Load mailer state and verify owner
1117
+ let mailer_data = mailer_account.try_borrow_data()?;
1118
+ let mailer_state: MailerState = BorshDeserialize::deserialize(&mut &mailer_data[8..])?;
1119
+ drop(mailer_data);
1120
+
1121
+ if mailer_state.owner != *owner.key {
1122
+ return Err(MailerError::OnlyOwner.into());
1123
+ }
1124
+
1125
+ // Check if contract is paused
1126
+ if mailer_state.paused {
1127
+ return Err(MailerError::ContractPaused.into());
1128
+ }
1129
+
1130
+ // Verify fee discount account PDA
1131
+ let (discount_pda, _) = Pubkey::find_program_address(
1132
+ &[b"discount", account.as_ref()],
1133
+ program_id
1134
+ );
1135
+
1136
+ if fee_discount_account.key != &discount_pda {
1137
+ return Err(MailerError::InvalidPDA.into());
1138
+ }
1139
+
1140
+ // Clear by setting discount to 0 (no discount = 100% fee = default behavior)
1141
+ if fee_discount_account.lamports() > 0 {
1142
+ let mut discount_data = fee_discount_account.try_borrow_mut_data()?;
1143
+ let mut fee_discount: FeeDiscount = BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1144
+ fee_discount.discount = 0; // 0 discount = 100% fee = default
1145
+ fee_discount.serialize(&mut &mut discount_data[8..])?;
1146
+ }
1147
+
1148
+ msg!("Custom fee percentage cleared for {} (reset to 100%)", account);
1149
+ Ok(())
1150
+ }
1151
+
954
1152
  /// Record revenue shares for priority messages
955
1153
  fn record_shares(
956
1154
  recipient_claim: &AccountInfo,
@@ -981,6 +1179,44 @@ fn record_shares(
981
1179
  Ok(())
982
1180
  }
983
1181
 
1182
+ /// Calculate the effective fee for an account based on custom discount
1183
+ /// If no discount account exists, returns full base_fee (default behavior)
1184
+ /// Otherwise applies discount: fee = base_fee * (100 - discount) / 100
1185
+ fn calculate_fee_with_discount(
1186
+ program_id: &Pubkey,
1187
+ account: &Pubkey,
1188
+ accounts: &[AccountInfo],
1189
+ base_fee: u64,
1190
+ ) -> Result<u64, ProgramError> {
1191
+ // Try to find fee discount account
1192
+ let (discount_pda, _) = Pubkey::find_program_address(
1193
+ &[b"discount", account.as_ref()],
1194
+ program_id
1195
+ );
1196
+
1197
+ // Check if any account in the accounts slice matches the discount PDA
1198
+ let discount_account = accounts.iter().find(|acc| acc.key == &discount_pda);
1199
+
1200
+ if let Some(discount_acc) = discount_account {
1201
+ // Account exists and has lamports - load the discount
1202
+ if discount_acc.lamports() > 0 {
1203
+ let discount_data = discount_acc.try_borrow_data()?;
1204
+ if discount_data.len() >= 8 + FeeDiscount::LEN {
1205
+ let fee_discount: FeeDiscount = BorshDeserialize::deserialize(&mut &discount_data[8..])?;
1206
+ let discount = fee_discount.discount as u64;
1207
+
1208
+ // Apply discount: fee = base_fee * (100 - discount) / 100
1209
+ // Examples: discount=0 → 100% fee, discount=50 → 50% fee, discount=100 → 0% fee (free)
1210
+ let effective_fee = (base_fee * (100 - discount)) / 100;
1211
+ return Ok(effective_fee);
1212
+ }
1213
+ }
1214
+ }
1215
+
1216
+ // No discount account or uninitialized - use full fee (default behavior)
1217
+ Ok(base_fee)
1218
+ }
1219
+
984
1220
  /// Pause the contract and distribute owner claimable funds
985
1221
  fn process_pause(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
986
1222
  let account_iter = &mut accounts.iter();