@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.
- package/dist/solana/solana/mailer-client.d.ts +7 -0
- package/dist/solana/solana/mailer-client.d.ts.map +1 -1
- package/dist/solana/solana/mailer-client.js +30 -1
- package/dist/solana/solana/mailer-client.js.map +1 -1
- package/dist/unified/src/react/hooks/useMailerMutations.d.ts +1 -1
- package/dist/unified/src/react/hooks/useMailerMutations.js +1 -1
- package/dist/unified/src/solana/mailer-client.d.ts +7 -0
- package/dist/unified/src/solana/mailer-client.d.ts.map +1 -1
- package/dist/unified/src/solana/mailer-client.js +30 -1
- package/dist/unified/src/solana/mailer-client.js.map +1 -1
- package/dist/unified/src/unified/onchain-mailer-client.d.ts +2 -1
- package/dist/unified/src/unified/onchain-mailer-client.d.ts.map +1 -1
- package/dist/unified/src/unified/onchain-mailer-client.js +23 -2
- package/dist/unified/src/unified/onchain-mailer-client.js.map +1 -1
- package/dist/unified-esm/src/react/hooks/useMailerMutations.d.ts +1 -1
- package/dist/unified-esm/src/react/hooks/useMailerMutations.js +1 -1
- package/dist/unified-esm/src/solana/mailer-client.d.ts +7 -0
- package/dist/unified-esm/src/solana/mailer-client.d.ts.map +1 -1
- package/dist/unified-esm/src/solana/mailer-client.js +30 -1
- package/dist/unified-esm/src/solana/mailer-client.js.map +1 -1
- package/dist/unified-esm/src/unified/onchain-mailer-client.d.ts +2 -1
- package/dist/unified-esm/src/unified/onchain-mailer-client.d.ts.map +1 -1
- package/dist/unified-esm/src/unified/onchain-mailer-client.js +23 -2
- package/dist/unified-esm/src/unified/onchain-mailer-client.js.map +1 -1
- package/package.json +6 -1
- package/programs/mailer/src/lib.rs +429 -102
- 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 {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 {
|
|
341
|
-
|
|
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
|
-
|
|
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::
|
|
356
|
-
|
|
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
|
-
&[
|
|
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 =
|
|
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) =
|
|
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
|
-
&[
|
|
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]
|
|
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!(
|
|
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 =
|
|
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!(
|
|
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 =
|
|
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!(
|
|
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) =
|
|
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]
|
|
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 =
|
|
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(
|
|
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 =
|
|
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) =
|
|
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
|
-
&[
|
|
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]
|
|
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 =
|
|
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, _) =
|
|
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 =
|
|
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!(
|
|
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!(
|
|
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, _) =
|
|
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 =
|
|
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
|
-
&[
|
|
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
|
-
|
|
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
|
-
&[
|
|
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;
|