miracle_api/sdk.rs
1use serde::{Deserialize, Serialize};
2use solana_program::hash::hashv;
3use steel::*;
4
5use crate::{
6 consts::*,
7 dmt::{DailyParticipantData, EpochClaimData, SocialClaimData},
8 instruction::*,
9 state::{config_pda, metrics_pda, proof_pda, snapshot_pda, treasury_pda},
10};
11
12/// Builds a claim instruction.
13///
14/// The reward amount is calculated on-chain using the transparent formula:
15/// reward = (activity_count * customer_reward_pool) / total_customer_activity
16///
17/// Where:
18/// - `activity_count` is extracted from the participant data
19/// - `customer_reward_pool` and `total_customer_activity` are read from epoch data
20///
21/// ## Parameters
22/// - `signer`: The wallet signing the transaction
23/// - `beneficiary`: The wallet receiving the rewards
24/// - `epoch`: The epoch number for the claim
25/// - `participant_data`: Participant data including activity count and timestamps
26/// - `payment_proof`: Merkle proof path for payment verification
27/// - `payment_indices`: Merkle proof path indices (true = right, false = left)
28/// - `seal_proof`: Merkle proof path for seal verification
29/// - `seal_indices`: Merkle proof path indices (true = right, false = left)
30/// - `payment_root`: Payment merkle root for verification
31/// - `epoch_data`: Epoch-specific data for accurate reward calculation
32/// - `participant_type`: 0 for customer, 1 for merchant
33/// - `social_data`: Optional social media engagement data
34///
35/// ## Returns
36/// - Instruction for claiming rewards
37///
38/// ## Note
39/// Users can estimate their rewards off-chain using the transparent calculation functions
40/// in `consts.rs`. The on-chain calculation will match the off-chain estimation exactly.
41pub fn claim(
42 signer: Pubkey,
43 beneficiary: Pubkey,
44 epoch: u64,
45 participant_data: DailyParticipantData,
46 payment_proof: Vec<[u8; 32]>,
47 payment_indices: Vec<bool>,
48 seal_proof: Vec<[u8; 32]>,
49 seal_indices: Vec<bool>,
50 payment_root: [u8; 32],
51 epoch_data: EpochClaimData,
52 participant_type: u8,
53 social_data: Option<SocialClaimData>,
54) -> Instruction {
55 let mut data = Vec::new();
56
57 // Use the Claim struct's to_bytes() method for consistent serialization
58 let claim_data = Claim {
59 epoch: epoch.to_le_bytes(),
60 payment_proof_length: payment_proof.len() as u8,
61 seal_proof_length: seal_proof.len() as u8,
62 participant_type,
63 has_social: if social_data.is_some() { 1u8 } else { 0u8 },
64 _padding: [0u8; 4],
65 };
66 data.extend_from_slice(&claim_data.to_bytes());
67
68 // Append participant data
69 data.extend_from_slice(bytemuck::bytes_of(&participant_data));
70
71 // Append payment proof data
72 for path_node in payment_proof {
73 data.extend_from_slice(&path_node);
74 }
75
76 // Append payment indices data
77 for &is_right in &payment_indices {
78 data.push(if is_right { 1u8 } else { 0u8 });
79 }
80
81 // Append seal proof data
82 for path_node in seal_proof {
83 data.extend_from_slice(&path_node);
84 }
85
86 // Append seal indices data
87 for &is_right in &seal_indices {
88 data.push(if is_right { 1u8 } else { 0u8 });
89 }
90
91 // Append payment root data
92 data.extend_from_slice(&payment_root);
93
94 // Append epoch data
95 data.extend_from_slice(bytemuck::bytes_of(&epoch_data));
96
97 // Append social data if provided
98 if let Some(social) = social_data {
99 data.extend_from_slice(bytemuck::bytes_of(&social));
100 }
101
102 let proof_pda = proof_pda(signer);
103 Instruction {
104 program_id: crate::ID,
105 accounts: vec![
106 AccountMeta::new(signer, true), // [0] signer
107 AccountMeta::new(beneficiary, false), // [1] beneficiary
108 AccountMeta::new(proof_pda.0, false), // [2] proof PDA
109 AccountMeta::new(TREASURY_ADDRESS, false), // [3] treasury
110 AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), // [4] treasury tokens
111 AccountMeta::new_readonly(spl_token::ID, false), // [5] token program
112 AccountMeta::new_readonly(SNAPSHOT_ADDRESS, false), // [6] snapshot
113 AccountMeta::new_readonly(CONFIG_ADDRESS, false), // [7] config
114 ],
115 data,
116 }
117}
118
119/// Create social claim data for social media engagement.
120///
121/// This function creates a `SocialClaimData` struct with the provided social media
122/// information. The oracle signature should be provided by the authorized oracle
123/// authority.
124///
125/// ## Parameters
126/// - `post_url`: Social media post URL (max 256 bytes)
127/// - `engagement_score`: Engagement score for the post (u32)
128/// - `post_timestamp`: Unix timestamp when post was made (i64)
129/// - `oracle_signature`: Ed25519 signature from authorized oracle
130///
131/// ## Oracle Signature Requirements
132/// The oracle must sign a message with the following format:
133///
134/// `epoch:user_pubkey:post_url:engagement_score:post_timestamp`
135///
136/// This format prevents replay attacks by including epoch and user context.
137///
138/// ## Returns
139/// - `SocialClaimData` struct ready for social claim
140///
141/// ## Example
142/// ```rust
143/// use miracle_api::sdk::create_social_claim_data;
144///
145/// let social_data = create_social_claim_data(
146/// "https://twitter.com/user/123",
147/// 500,
148/// 1640995200,
149/// &[0u8; 64], // Oracle signature
150/// );
151/// ```
152pub fn create_social_claim_data(
153 post_url: &str,
154 engagement_score: u32,
155 post_timestamp: i64,
156 oracle_signature: &[u8; 64],
157) -> SocialClaimData {
158 let mut post_url_bytes = [0u8; 256];
159 let url_bytes = post_url.as_bytes();
160 let copy_len = std::cmp::min(url_bytes.len(), 256);
161 post_url_bytes[..copy_len].copy_from_slice(&url_bytes[..copy_len]);
162
163 SocialClaimData {
164 post_url: post_url_bytes,
165 oracle_signature: *oracle_signature,
166 engagement_score: engagement_score.to_le_bytes(),
167 post_timestamp: post_timestamp.to_le_bytes(),
168 _padding: [0u8; 4],
169 }
170}
171
172/// Validate social media post URL for required content.
173///
174/// This function checks if the social media post URL contains the required
175/// hashtags and mentions for social marketing rewards.
176///
177/// ## Parameters
178/// - `post_url`: Social media post URL to validate
179///
180/// ## Returns
181/// - `true` if the URL contains required content, `false` otherwise
182///
183/// ## Required Content
184/// - Must contain `#MiracleRewards` hashtag
185/// - Must contain `@MiracleProtocol` mention
186/// - URL must be from supported platforms (Twitter, Instagram, etc.)
187pub fn validate_social_post_url(post_url: &str) -> bool {
188 let url_lower = post_url.to_lowercase();
189
190 // Check for required hashtag
191 if !url_lower.contains("#miraclerewards") {
192 return false;
193 }
194
195 // Check for required mention
196 if !url_lower.contains("@miracleprotocol") {
197 return false;
198 }
199
200 // Check for supported platforms
201 let supported_platforms = [
202 "twitter.com",
203 "x.com",
204 "instagram.com",
205 "facebook.com",
206 "linkedin.com",
207 "tiktok.com",
208 ];
209
210 supported_platforms
211 .iter()
212 .any(|platform| url_lower.contains(platform))
213}
214
215/// Calculate social reward amount based on engagement and base reward.
216///
217/// This function calculates the social reward amount based on the engagement
218/// score and the base reward per post. The calculation matches the on-chain
219/// logic used in the social claim instruction.
220///
221/// ## Parameters
222/// - `engagement_score`: Engagement metrics for the social post
223/// - `base_reward_per_post`: Base reward amount per post
224///
225/// ## Returns
226/// - Total reward amount (base reward + engagement bonus)
227///
228/// ## Formula
229/// - Base reward: `base_reward_per_post`
230/// - Engagement bonus: `(engagement_score - 1000) / 100` if score > 1000
231/// - Total reward: `base_reward + engagement_bonus`
232pub fn calculate_social_reward_amount(engagement_score: u32, base_reward_per_post: u32) -> u64 {
233 let base_reward = base_reward_per_post as u64;
234 let engagement_score_u64 = engagement_score as u64;
235
236 let engagement_bonus = if engagement_score_u64 > 1000 {
237 (engagement_score_u64 - 1000) / 100 // 1 MIRACLE per 100 engagement points above 1000
238 } else {
239 0
240 };
241
242 base_reward + engagement_bonus
243}
244
245/// Builds a social claim instruction.
246///
247/// This instruction allows users to claim social marketing rewards based on their
248/// social media engagement. Social rewards require payment activity in the same epoch
249/// and are verified through oracle signatures to prevent gaming.
250///
251/// ## Parameters
252/// - `signer`: The wallet signing the transaction
253/// - `beneficiary`: The wallet receiving the rewards
254/// - `epoch`: The epoch number for the claim (payment activity required)
255/// - `merkle_root`: Merkle root for payment verification
256/// - `merkle_path`: Merkle proof path for payment activity
257/// - `path_indices`: Merkle proof path indices (true = right, false = left)
258/// - `participant_type`: 0 for customer, 1 for merchant
259/// - `social_data`: Social media verification data
260///
261/// ## Returns
262/// - Instruction for claiming social rewards
263///
264/// ## Requirements
265
266/// Builds a close instruction.
267///
268/// This instruction allows a user to close their Proof account and recover the rent.
269/// The Proof account can only be closed if:
270/// 1. The signer is the Proof account owner (authority)
271/// 2. No rewards have been claimed (total_claimed_rewards == 0)
272///
273/// ## Accounts
274/// - `signer`: The Proof account owner (must be a signer)
275/// - `proof`: The Proof account to close (PDA derived from signer)
276/// - `system_program`: System program for rent recovery
277///
278/// ## Safety Features
279/// - **Owner-only**: Only the Proof account owner can close it
280/// - **No claims allowed**: Cannot close if rewards have been claimed
281/// - **Rent recovery**: All rent is returned to the signer
282///
283/// ## Use Cases
284/// - Recovering rent when no longer participating
285/// - Clean account management
286/// - Reopening with a fresh Proof account later
287///
288/// ## Returns
289/// - Instruction for closing the Proof account
290pub fn close(signer: Pubkey) -> Instruction {
291 let proof = proof_pda(signer).0;
292 Instruction {
293 program_id: crate::ID,
294 accounts: vec![
295 AccountMeta::new(signer, true),
296 AccountMeta::new(proof, false),
297 AccountMeta::new_readonly(solana_program::system_program::ID, false),
298 ],
299 data: Close {}.to_bytes(),
300 }
301}
302
303/// Builds an open instruction.
304pub fn open(signer: Pubkey, payer: Pubkey) -> Instruction {
305 let proof_pda = proof_pda(signer);
306 Instruction {
307 program_id: crate::ID,
308 accounts: vec![
309 AccountMeta::new(signer, true),
310 AccountMeta::new(payer, true),
311 AccountMeta::new(proof_pda.0, false),
312 AccountMeta::new_readonly(solana_program::system_program::ID, false),
313 ],
314 data: Open {}.to_bytes(),
315 }
316}
317
318/// Builds a reset instruction.
319///
320/// This instruction implements the Community-Driven Decay model by adjusting daily rewards
321/// based on community health metrics stored in the Metrics account.
322pub fn reset(signer: Pubkey) -> Instruction {
323 Instruction {
324 program_id: crate::ID,
325 accounts: vec![
326 AccountMeta::new(signer, true),
327 AccountMeta::new(CONFIG_ADDRESS, false),
328 AccountMeta::new_readonly(METRICS_ADDRESS, false),
329 AccountMeta::new(MINT_ADDRESS, false),
330 AccountMeta::new(TREASURY_ADDRESS, false),
331 AccountMeta::new(TREASURY_TOKENS_ADDRESS, false),
332 AccountMeta::new_readonly(spl_token::ID, false),
333 ],
334 data: Reset {}.to_bytes(),
335 }
336}
337
338// MI: abandoned for community trust/confidence
339// // Build an upgrade instruction.
340// pub fn upgrade(signer: Pubkey, beneficiary: Pubkey, sender: Pubkey, amount: u64) -> Instruction {
341// Instruction {
342// program_id: crate::ID,
343// accounts: vec![
344// AccountMeta::new(signer, true),
345// AccountMeta::new(beneficiary, false),
346// AccountMeta::new(MINT_ADDRESS, false),
347// AccountMeta::new(MINT_V1_ADDRESS, false),
348// AccountMeta::new(sender, false),
349// AccountMeta::new(TREASURY_ADDRESS, false),
350// AccountMeta::new_readonly(spl_token::ID, false),
351// AccountMeta::new_readonly(CONFIG_ADDRESS, false),
352// ],
353// data: Upgrade {
354// amount: amount.to_le_bytes(),
355// }
356// .to_bytes(),
357// }
358// }
359
360// /// Build an evolve instruction.
361// pub fn evolve(signer: Pubkey, evolvable: bool) -> Instruction {
362// Instruction {
363// program_id: crate::ID,
364// accounts: vec![
365// AccountMeta::new(signer, true),
366// AccountMeta::new(CONFIG_ADDRESS, false),
367// ],
368// data: Evolve {
369// evolvable: evolvable as u8,
370// }
371// .to_bytes(),
372// }
373// }
374
375/// Builds an initialize instruction.
376pub fn initialize(signer: Pubkey, project_wallet: Pubkey) -> Instruction {
377 let config_pda = config_pda();
378 let mint_pda = Pubkey::find_program_address(&[MINT, MINT_NOISE.as_slice()], &crate::ID);
379 let treasury_pda = treasury_pda();
380 let snapshot_pda = snapshot_pda();
381 let metrics_pda = metrics_pda();
382 let metadata_pda = Pubkey::find_program_address(
383 &[
384 METADATA,
385 mpl_token_metadata::ID.as_ref(),
386 mint_pda.0.as_ref(),
387 ],
388 &mpl_token_metadata::ID,
389 );
390 // let treasury_tokens_pda = Pubkey::find_program_address(
391 // &[
392 // treasury_pda.0.as_ref(),
393 // spl_token::id().as_ref(),
394 // MINT_ADDRESS.as_ref(),
395 // ],
396 // &spl_associated_token_account::id(),
397 // );
398 let project_tokens_pda = Pubkey::find_program_address(
399 &[
400 project_wallet.as_ref(),
401 spl_token::id().as_ref(),
402 MINT_ADDRESS.as_ref(),
403 ],
404 &spl_associated_token_account::id(),
405 );
406
407 Instruction {
408 program_id: crate::ID,
409 accounts: vec![
410 AccountMeta::new(signer, true),
411 AccountMeta::new(config_pda.0, false),
412 AccountMeta::new(snapshot_pda.0, false),
413 AccountMeta::new(metrics_pda.0, false),
414 AccountMeta::new(metadata_pda.0, false),
415 AccountMeta::new(mint_pda.0, false),
416 AccountMeta::new(treasury_pda.0, false),
417 AccountMeta::new(TREASURY_TOKENS_ADDRESS, false),
418 AccountMeta::new(project_wallet, false),
419 AccountMeta::new(project_tokens_pda.0, false),
420 AccountMeta::new_readonly(system_program::ID, false),
421 AccountMeta::new_readonly(spl_token::ID, false),
422 AccountMeta::new_readonly(spl_associated_token_account::ID, false),
423 AccountMeta::new_readonly(mpl_token_metadata::ID, false),
424 AccountMeta::new_readonly(sysvar::rent::ID, false),
425 ],
426 data: Initialize { project_wallet }.to_bytes(),
427 }
428}
429
430/// Builds an update oracle instruction.
431///
432/// This instruction allows the current oracle authority to update itself to a new oracle authority.
433/// Only the current oracle authority can execute this instruction.
434///
435/// ## Parameters
436/// - `oracle`: The current oracle authority public key (must be a signer)
437/// - `new_oracle_authority`: The new oracle authority public key
438///
439/// ## Returns
440/// An instruction that can be included in a transaction to update the oracle authority.
441///
442/// ## Security
443/// - Only the current oracle authority can execute this instruction
444/// - New oracle authority must be different from current and not zero
445/// - Both main and social oracle authorities are updated consistently
446///
447/// ## Example
448/// ```rust,no_run
449/// use steel::Pubkey;
450/// let current_oracle = Pubkey::new_unique();
451/// let new_oracle = Pubkey::new_unique();
452/// let instruction = miracle_api::sdk::update_oracle(current_oracle, new_oracle);
453/// ```
454pub fn update_oracle(oracle: Pubkey, new_oracle_authority: Pubkey) -> Instruction {
455 Instruction {
456 program_id: crate::ID,
457 accounts: vec![
458 AccountMeta::new(oracle, true),
459 AccountMeta::new(CONFIG_ADDRESS, false),
460 ],
461 data: UpdateOracle {
462 new_oracle_authority,
463 }
464 .to_bytes(),
465 }
466}
467
468/// Builds an update snapshot instruction.
469pub fn update_snapshot(
470 oracle: Pubkey,
471 epoch: u64,
472 seal_merkle_root: [u8; 32],
473 payment_merkle_root: [u8; 32],
474 epoch_hash: [u8; 32],
475 customer_reward_pool: u64,
476 merchant_reward_pool: u64,
477 customer_participants: u32,
478 merchant_participants: u32,
479 total_customer_payments: u32,
480 total_merchant_payments: u32,
481) -> Instruction {
482 Instruction {
483 program_id: crate::ID,
484 accounts: vec![
485 AccountMeta::new(oracle, true),
486 AccountMeta::new(SNAPSHOT_ADDRESS, false),
487 AccountMeta::new_readonly(CONFIG_ADDRESS, false),
488 AccountMeta::new(TREASURY_ADDRESS, false),
489 ],
490 data: UpdateSnapshot {
491 epoch: epoch.to_le_bytes(),
492 seal_merkle_root,
493 payment_merkle_root,
494 epoch_hash,
495 customer_reward_pool: customer_reward_pool.to_le_bytes(),
496 merchant_reward_pool: merchant_reward_pool.to_le_bytes(),
497 customer_participants: customer_participants.to_le_bytes(),
498 merchant_participants: merchant_participants.to_le_bytes(),
499 total_customer_payments: total_customer_payments.to_le_bytes(),
500 total_merchant_payments: total_merchant_payments.to_le_bytes(),
501 }
502 .to_bytes(),
503 }
504}
505
506/// Builds an update community metrics instruction.
507///
508/// This function creates an instruction to update community health metrics used for
509/// the Enhanced Community-Driven Decay model. Only the oracle authority can execute this instruction.
510///
511/// ## Parameters
512/// - `oracle`: The oracle authority public key (must be a signer)
513/// - `weekly_active_users`: Number of active users in the past week
514/// - `weekly_retention_rate`: Retention rate in basis points (0-10000, 0-100%)
515/// - `user_weight`: User weight in basis points (0-10000) for weighted geometric mean
516/// - `volume_weight`: Volume weight in basis points (0-10000) for weighted geometric mean
517/// - `retention_weight`: Retention weight in basis points (0-10000) for weighted geometric mean
518///
519/// ## Returns
520/// An instruction that can be included in a transaction to update community metrics.
521///
522/// ## Security
523/// - The oracle must be a signer of the transaction
524/// - Input validation occurs on-chain
525/// - Only the authorized oracle can update metrics
526///
527/// ## Example
528/// ```rust,no_run
529/// use steel::Pubkey;
530/// let oracle_pubkey = Pubkey::new_unique();
531/// let instruction = miracle_api::sdk::update_metrics(
532/// oracle_pubkey,
533/// 5000, // 5,000 active users
534/// 7500, // 75% retention rate
535/// 4000, // 40% user weight
536/// 3500, // 35% activity weight
537/// 2500, // 25% retention weight
538/// 50_000, // 50,000 transactions
539/// 7000, // 70% customer reward share
540/// 3000, // 30% merchant reward share
541/// );
542/// ```
543pub fn update_metrics(
544 oracle: Pubkey,
545 weekly_active_users: u32,
546 weekly_retention_rate: u16,
547 user_weight: u16,
548 activity_weight: u16,
549 retention_weight: u16,
550 weekly_activity_count: u32,
551 customer_reward_share: u16,
552 merchant_reward_share: u16,
553) -> Instruction {
554 Instruction {
555 program_id: crate::ID,
556 accounts: vec![
557 AccountMeta::new(oracle, true),
558 AccountMeta::new(METRICS_ADDRESS, false),
559 AccountMeta::new_readonly(CONFIG_ADDRESS, false),
560 ],
561 data: UpdateMetrics {
562 weekly_active_users: weekly_active_users.to_le_bytes(),
563 weekly_retention_rate: weekly_retention_rate.to_le_bytes(),
564 user_weight: user_weight.to_le_bytes(),
565 activity_weight: activity_weight.to_le_bytes(),
566 retention_weight: retention_weight.to_le_bytes(),
567 weekly_activity_count: weekly_activity_count.to_le_bytes(),
568 customer_reward_share: customer_reward_share.to_le_bytes(),
569 merchant_reward_share: merchant_reward_share.to_le_bytes(),
570 _padding: [0u8; 4], // 4 bytes padding for 8-byte alignment
571 }
572 .to_bytes(),
573 }
574}
575
576/// Validates community metrics parameters before creating an instruction.
577///
578/// This function helps oracles validate their inputs before submitting transactions,
579/// providing better error handling and user experience.
580///
581/// ## Parameters
582/// - `weekly_active_users`: Number of active users in the past week
583/// - `weekly_retention_rate`: Retention rate in basis points (0-10000, 0-100%)
584/// - `user_weight`: User weight in basis points (0-10000) for weighted geometric mean
585/// - `activity_weight`: Activity weight in basis points (0-10000) for weighted geometric mean
586/// - `retention_weight`: Retention weight in basis points (0-10000) for weighted geometric mean
587/// - `weekly_activity_count`: Number of transactions in the past week
588/// - `customer_reward_share`: Customer reward percentage in basis points (0-10000)
589/// - `merchant_reward_share`: Merchant reward percentage in basis points (0-10000)
590///
591/// ## Returns
592/// - `Ok(())` if all parameters are valid
593/// - `Err(MiracleError)` with specific error if validation fails
594///
595/// ## Validation Rules
596/// - Weights must sum to 10000 (100%)
597/// - Reward shares must sum to 10000 (100%)
598/// - Retention rate must be 0-10000 (0-100%)
599
600/// - Activity count must be reasonable (0-1M transactions per week)
601/// - All numeric values must be reasonable
602pub fn validate_community_metrics(
603 weekly_active_users: u32,
604 weekly_retention_rate: u16,
605 user_weight: u16,
606 activity_weight: u16,
607 retention_weight: u16,
608 weekly_activity_count: u32,
609 customer_reward_share: u16,
610 merchant_reward_share: u16,
611) -> Result<(), crate::error::MiracleError> {
612 // Validate weights sum to 10000 (100%)
613 let total_weight = user_weight
614 .saturating_add(activity_weight)
615 .saturating_add(retention_weight);
616 if total_weight != 10000 {
617 return Err(crate::error::MiracleError::InvalidWeights);
618 }
619
620 // Validate reward shares sum to 10000 (100%)
621 let total_reward_share = customer_reward_share.saturating_add(merchant_reward_share);
622 if total_reward_share != 10000 {
623 return Err(crate::error::MiracleError::InvalidRewardSplit);
624 }
625
626 // Validate retention rate bounds (0-10000)
627 if weekly_retention_rate > 10000 {
628 return Err(crate::error::MiracleError::InvalidRetentionRate);
629 }
630
631 // Validate activity count bounds (reasonable bounds: 0-1M transactions per week)
632 if weekly_activity_count > 1_000_000 {
633 return Err(crate::error::MiracleError::InvalidActivityCount);
634 }
635
636 // Validate that weights are reasonable (not all zero)
637 if user_weight == 0 && activity_weight == 0 && retention_weight == 0 {
638 return Err(crate::error::MiracleError::InvalidWeights);
639 }
640
641 // Validate that weekly metrics are reasonable (not unreasonably large)
642 if weekly_active_users > 1_000_000 {
643 return Err(crate::error::MiracleError::InvalidInput);
644 }
645
646 Ok(())
647}
648
649/// Complete transparent calculation for customer rewards.
650/// This function combines all steps of the reward calculation for transparency.
651///
652/// ## Formula Chain
653/// 1. daily_rewards = calculate_daily_rewards(community_score, years_since_launch)
654/// 2. customer_pool = daily_rewards * customer_reward_share / 10000
655/// 3. customer_reward = (activity_count * customer_pool) / total_customer_activity
656///
657/// ## Parameters
658/// - `activity_count`: Number of activities performed by the customer
659/// - `community_score`: Community health score (0-10000 basis points)
660/// - `years_since_launch`: Number of years since project launch (for time decay)
661/// - `customer_reward_share`: Customer reward percentage in basis points (0-10000)
662/// - `total_customer_activity`: Total activity count across all customers
663///
664/// ## Returns
665/// - Individual customer reward amount in smallest token units
666///
667/// ## Usage
668/// ```rust
669/// use miracle_api::sdk::calculate_customer_reward_transparent;
670///
671/// let reward = calculate_customer_reward_transparent(
672/// 5, // activity_count
673/// 8000, // community_score
674/// 2, // years_since_launch
675/// 7000, // customer_reward_share
676/// 1000, // total_customer_activity
677/// );
678/// println!("Reward: {} MIRACLE", reward);
679/// ```
680///
681/// ## Note
682/// This function provides complete transparency from community metrics to individual rewards.
683/// It can be used both on-chain and off-chain for verification.
684pub fn calculate_customer_reward_transparent(
685 activity_count: u32,
686 community_score: u16,
687 years_since_launch: u32,
688 customer_reward_share: u16,
689 total_customer_activity: u32,
690) -> u64 {
691 crate::consts::calculate_customer_reward_transparent(
692 activity_count,
693 community_score,
694 years_since_launch,
695 customer_reward_share,
696 total_customer_activity,
697 )
698}
699
700/// Complete transparent calculation for customer rewards WITH activity capping.
701/// This function matches the on-chain calculation behavior exactly.
702///
703/// ## Formula Chain
704/// 1. daily_rewards = calculate_daily_rewards(community_score, years_since_launch)
705/// 2. customer_pool = daily_rewards * customer_reward_share / 10000
706/// 3. capped_activity = min(activity_count, max_customer_activity_per_epoch)
707/// 4. customer_reward = (capped_activity * customer_pool) / total_customer_activity
708///
709/// ## Parameters
710/// - `activity_count`: Number of activities performed by the customer
711/// - `community_score`: Community health score (0-10000 basis points)
712/// - `years_since_launch`: Number of years since project launch (for time decay)
713/// - `customer_reward_share`: Customer reward percentage in basis points (0-10000)
714/// - `total_customer_activity`: Total activity count across all customers
715/// - `max_customer_activity_per_epoch`: Maximum allowed customer activity per epoch
716///
717/// ## Returns
718/// - Individual customer reward amount in smallest token units (with activity capping)
719///
720/// ## Benefits
721/// - Matches on-chain calculation exactly
722/// - Includes activity capping to prevent gaming
723/// - Complete transparency for off-chain estimation
724/// - Deterministic calculation for verification
725///
726/// ## Usage
727/// ```rust
728/// use miracle_api::sdk::calculate_customer_reward_transparent_with_cap;
729///
730/// let reward = calculate_customer_reward_transparent_with_cap(
731/// 15, // activity_count (will be capped)
732/// 8000, // community_score
733/// 2, // years_since_launch
734/// 7000, // customer_reward_share
735/// 1000, // total_customer_activity
736/// 10, // max_customer_activity_per_epoch (caps activity at 10)
737/// );
738/// println!("Reward: {} MIRACLE", reward);
739/// ```
740///
741/// ## Note
742/// This function provides complete transparency from community metrics to individual rewards
743/// and matches the on-chain calculation behavior exactly.
744pub fn calculate_customer_reward_transparent_with_cap(
745 activity_count: u32,
746 community_score: u16,
747 years_since_launch: u32,
748 customer_reward_share: u16,
749 total_customer_activity: u32,
750 max_customer_activity_per_epoch: u32,
751) -> u64 {
752 crate::consts::calculate_customer_reward_transparent_with_cap(
753 activity_count,
754 community_score,
755 years_since_launch,
756 customer_reward_share,
757 total_customer_activity,
758 max_customer_activity_per_epoch,
759 )
760}
761
762/// Complete transparent calculation for merchant rewards.
763/// This function combines all steps of the reward calculation for transparency.
764///
765/// ## Formula Chain
766/// 1. daily_rewards = calculate_daily_rewards(community_score, years_since_launch)
767/// 2. merchant_pool = daily_rewards * merchant_reward_share / 10000
768/// 3. merchant_reward = (activity_count * merchant_pool) / total_merchant_activity
769///
770/// ## Parameters
771/// - `activity_count`: Number of activities performed by the merchant
772/// - `community_score`: Community health score (0-10000 basis points)
773/// - `years_since_launch`: Number of years since project launch (for time decay)
774/// - `merchant_reward_share`: Merchant reward percentage in basis points (0-10000)
775/// - `total_merchant_activity`: Total activity count across all merchants
776///
777/// ## Returns
778/// - Individual merchant reward amount in smallest token units
779///
780/// ## Usage
781/// ```rust
782/// use miracle_api::sdk::calculate_merchant_reward_transparent;
783///
784/// let reward = calculate_merchant_reward_transparent(
785/// 3, // activity_count
786/// 8000, // community_score
787/// 2, // years_since_launch
788/// 3000, // merchant_reward_share
789/// 500, // total_merchant_activity
790/// );
791/// println!("Reward: {} MIRACLE", reward);
792/// ```
793///
794/// ## Note
795/// This function provides complete transparency from community metrics to individual rewards.
796/// It can be used both on-chain and off-chain for verification.
797pub fn calculate_merchant_reward_transparent(
798 activity_count: u32,
799 community_score: u16,
800 years_since_launch: u32,
801 merchant_reward_share: u16,
802 total_merchant_activity: u32,
803) -> u64 {
804 crate::consts::calculate_merchant_reward_transparent(
805 activity_count,
806 community_score,
807 years_since_launch,
808 merchant_reward_share,
809 total_merchant_activity,
810 )
811}
812
813/// Complete transparent calculation for merchant rewards WITH activity capping.
814/// This function matches the on-chain calculation behavior exactly.
815///
816/// ## Formula Chain
817/// 1. daily_rewards = calculate_daily_rewards(community_score, years_since_launch)
818/// 2. merchant_pool = daily_rewards * merchant_reward_share / 10000
819/// 3. capped_activity = min(activity_count, max_merchant_activity_per_epoch)
820/// 4. merchant_reward = (capped_activity * merchant_pool) / total_merchant_activity
821///
822/// ## Parameters
823/// - `activity_count`: Number of activities performed by the merchant
824/// - `community_score`: Community health score (0-10000 basis points)
825/// - `years_since_launch`: Number of years since project launch (for time decay)
826/// - `merchant_reward_share`: Merchant reward percentage in basis points (0-10000)
827/// - `total_merchant_activity`: Total activity count across all merchants
828/// - `max_merchant_activity_per_epoch`: Maximum allowed merchant activity per epoch
829///
830/// ## Returns
831/// - Individual merchant reward amount in smallest token units (with activity capping)
832///
833/// ## Benefits
834/// - Matches on-chain calculation exactly
835/// - Includes activity capping to prevent gaming
836/// - Complete transparency for off-chain estimation
837/// - Deterministic calculation for verification
838///
839/// ## Usage
840/// ```rust
841/// use miracle_api::sdk::calculate_merchant_reward_transparent_with_cap;
842///
843/// let reward = calculate_merchant_reward_transparent_with_cap(
844/// 25, // activity_count (will be capped)
845/// 8000, // community_score
846/// 2, // years_since_launch
847/// 3000, // merchant_reward_share
848/// 500, // total_merchant_activity
849/// 20, // max_merchant_activity_per_epoch (caps activity at 20)
850/// );
851/// println!("Reward: {} MIRACLE", reward);
852/// ```
853///
854/// ## Note
855/// This function provides complete transparency from community metrics to individual rewards
856/// and matches the on-chain calculation behavior exactly.
857pub fn calculate_merchant_reward_transparent_with_cap(
858 activity_count: u32,
859 community_score: u16,
860 years_since_launch: u32,
861 merchant_reward_share: u16,
862 total_merchant_activity: u32,
863 max_merchant_activity_per_epoch: u32,
864) -> u64 {
865 crate::consts::calculate_merchant_reward_transparent_with_cap(
866 activity_count,
867 community_score,
868 years_since_launch,
869 merchant_reward_share,
870 total_merchant_activity,
871 max_merchant_activity_per_epoch,
872 )
873}
874
875/// Convert Unix timestamp to epoch number (day sequence since launch).
876///
877/// ## Formula
878/// epoch = (timestamp - START_AT) / EPOCH_DURATION
879///
880/// ## Parameters
881/// - `timestamp`: Unix timestamp in seconds
882///
883/// ## Returns
884/// - Epoch number (0-based day sequence since launch)
885///
886/// ## Epoch Alignment
887/// - Epoch 0: Sept 1, 2025 00:00:00 UTC to Sept 1, 2025 23:59:59 UTC
888/// - Epoch 1: Sept 2, 2025 00:00:00 UTC to Sept 2, 2025 23:59:59 UTC
889/// - Each epoch aligns with a natural UTC date (midnight to midnight)
890/// - Project launches at START_AT (10:00 AM UTC on Sept 1, 2025) within epoch 0
891///
892/// ## Example
893/// ```rust
894/// use miracle_api::sdk;
895/// let epoch = sdk::timestamp_to_epoch(1756756800); // Returns 0 (Sept 1, 2025 00:00:00 UTC)
896/// let epoch = sdk::timestamp_to_epoch(1756843200); // Returns 1 (Sept 2, 2025 00:00:00 UTC)
897/// ```
898pub fn timestamp_to_epoch(timestamp: i64) -> u64 {
899 // Align epochs with natural UTC dates (midnight to midnight)
900 // Formula: Find the start of the UTC day containing START_AT
901 // Then calculate epochs from that day boundary
902 let start_day_beginning = (START_AT / EPOCH_DURATION) * EPOCH_DURATION; // Start of UTC day containing START_AT
903
904 if timestamp < start_day_beginning {
905 return 0; // Return epoch 0 for timestamps before the start of epoch 0
906 }
907
908 let adjusted_timestamp = timestamp - start_day_beginning;
909 (adjusted_timestamp / EPOCH_DURATION) as u64
910}
911
912/// Calculate Unix timestamp from epoch number (day sequence).
913///
914/// ## Formula
915/// timestamp = START_AT + (epoch * EPOCH_DURATION)
916///
917/// ## Parameters
918/// - `epoch`: Epoch number (0-based day sequence since launch)
919///
920/// ## Returns
921/// - Unix timestamp in seconds (start of the epoch day)
922///
923/// ## Epoch Alignment
924/// - Epoch 0: Returns Sept 1, 2025 00:00:00 UTC
925/// - Epoch 1: Returns Sept 2, 2025 00:00:00 UTC
926/// - Each epoch starts at 00:00:00 UTC of the corresponding date
927/// - Project launches at START_AT (10:00 AM UTC on Sept 1, 2025) within epoch 0
928///
929/// ## Example
930/// ```rust
931/// use miracle_api::sdk;
932/// let timestamp = sdk::epoch_to_timestamp(0); // Returns 1756756800 (Sept 1, 2025 00:00:00 UTC)
933/// let timestamp = sdk::epoch_to_timestamp(1); // Returns 1756843200 (Sept 2, 2025 00:00:00 UTC)
934/// ```
935pub fn epoch_to_timestamp(epoch: u64) -> i64 {
936 // Align epochs with natural UTC dates (midnight to midnight)
937 // Formula: Find the start of the UTC day containing START_AT
938 // Then calculate timestamp from that day boundary
939 let start_day_beginning = (START_AT / EPOCH_DURATION) * EPOCH_DURATION; // Start of UTC day containing START_AT
940 start_day_beginning + (epoch as i64 * EPOCH_DURATION)
941}
942
943/// Convert a date range to a list of epochs.
944///
945/// ## Parameters
946/// - `start_date`: Start date as Unix timestamp
947/// - `end_date`: End date as Unix timestamp (inclusive)
948///
949/// ## Returns
950/// - Vector of epoch numbers from start_date to end_date (inclusive)
951///
952/// ## Example
953/// ```rust
954/// use miracle_api::sdk;
955/// let epochs = sdk::date_range_to_epochs(1756792800, 1756879199);
956/// // Returns [0] for Sept 1, 2025 (launch day)
957/// let epochs = sdk::date_range_to_epochs(1756792800, 1756965599);
958/// // Returns [0, 1] for Sept 1-2, 2025
959/// ```
960pub fn date_range_to_epochs(start_date: i64, end_date: i64) -> Vec<u64> {
961 if start_date > end_date {
962 return Vec::new(); // Return empty vector for invalid range
963 }
964
965 let start_epoch = timestamp_to_epoch(start_date);
966 let end_epoch = timestamp_to_epoch(end_date);
967
968 (start_epoch..=end_epoch).collect()
969}
970
971/// Builds a batch claim instruction for a date range.
972///
973/// This function converts the date range to epochs and creates a vector claim.
974/// It provides a user-friendly interface for claiming rewards across multiple days.
975///
976/// ## Parameters
977/// - `signer`: The wallet signing the transaction
978/// - `beneficiary`: The wallet receiving the rewards
979/// - `start_date`: Start date as Unix timestamp
980/// - `end_date`: End date as Unix timestamp (inclusive)
981/// - `participant_type`: 0 = customer, 1 = merchant
982/// - `max_batch_size`: Maximum epochs per batch (1-10)
983/// - `merkle_proofs`: Vector of Merkle proof data for each epoch
984///
985/// ## Returns
986/// - Instruction for batch claiming rewards across the date range
987///
988/// ## Note
989/// - The date range is converted to epochs using `date_range_to_epochs`
990/// - Each epoch requires its own Merkle proof in the `merkle_proofs` vector
991/// - The number of proofs must match the number of epochs in the range
992/// - If the date range is invalid or too large, returns an error
993///
994/// ## Example
995/// ```rust
996/// use miracle_api::sdk;
997/// use solana_program::pubkey::Pubkey;
998///
999/// let signer = Pubkey::new_unique();
1000/// let beneficiary = Pubkey::new_unique();
1001/// let merkle_proofs = vec![vec![1, 2, 3], vec![4, 5, 6]]; // Example proofs
1002///
1003/// let instruction = sdk::batch_claim_date_range(
1004/// signer,
1005/// beneficiary,
1006/// 1756792800, // Sept 1, 2025 00:00:00 UTC
1007/// 1756879199, // Sept 1, 2025 23:59:59 UTC
1008/// 0, // customer
1009/// 10, // max batch size
1010/// merkle_proofs, // proofs for epoch 0
1011/// None, // social_data (no social claims)
1012/// );
1013/// ```
1014///
1015/// Batch claim instruction for date range - uses correct 8-account structure
1016pub fn batch_claim_date_range(
1017 signer: Pubkey,
1018 beneficiary: Pubkey,
1019 start_date: i64,
1020 end_date: i64,
1021 participant_type: u8,
1022 max_batch_size: u8,
1023 merkle_proofs: Vec<Vec<u8>>,
1024 social_data: Option<Vec<Option<SocialClaimData>>>,
1025) -> Result<Instruction, crate::error::MiracleError> {
1026 // Convert date range to epochs
1027 let epochs = date_range_to_epochs(start_date, end_date);
1028
1029 if epochs.is_empty() {
1030 return Err(crate::error::MiracleError::InvalidInput);
1031 }
1032
1033 if epochs.len() > max_batch_size as usize {
1034 return Err(crate::error::MiracleError::InvalidInput);
1035 }
1036
1037 if merkle_proofs.len() != epochs.len() {
1038 return Err(crate::error::MiracleError::InvalidInput);
1039 }
1040
1041 // Validate social_data if provided
1042 if let Some(ref social_data_vec) = social_data {
1043 if social_data_vec.len() != epochs.len() {
1044 return Err(crate::error::MiracleError::InvalidInput);
1045 }
1046 }
1047
1048 // Create batch claim instruction using vector claim type
1049 let mut data = Vec::new();
1050
1051 // Add instruction discriminator for MiracleInstruction::BatchClaim (value = 0)
1052 data.push(MiracleInstruction::BatchClaim as u8);
1053
1054 // Manually serialize the BatchClaim struct fields in the exact order the program expects
1055 data.extend_from_slice(&[0u8; 8]); // start_epoch: 8 bytes (not used for date range)
1056 data.extend_from_slice(&[0u8; 8]); // end_epoch: 8 bytes (not used for date range)
1057 data.push(participant_type); // participant_type: 1 byte
1058 data.push(2u8); // claim_type: 1 byte (Date range claim = 2)
1059 data.push(epochs.len() as u8); // epoch_count: 1 byte
1060 data.push(max_batch_size); // max_batch_size: 1 byte
1061 data.extend_from_slice(&[0u8; 4]); // _padding: 4 bytes
1062
1063 // Append epoch list
1064 for &epoch in &epochs {
1065 data.extend_from_slice(&epoch.to_le_bytes());
1066 }
1067
1068 // Append Merkle proof data (consistent with single Claim)
1069 // For batch claims, we use the same merkle path and path indices for all epochs
1070 // since all epochs in a batch must use the same merkle root and structure
1071 let first_proof = &merkle_proofs[0];
1072 if first_proof.len() >= 37 {
1073 // Skip the first 33 bytes (merkle_root + merkle_path_length) and append the rest
1074 // This gives us: merkle_path + path_indices + activity_count
1075 data.extend_from_slice(&first_proof[33..]);
1076 } else {
1077 return Err(crate::error::MiracleError::InvalidInput);
1078 }
1079
1080 // Append social flags (1 byte per epoch indicating if social claim exists)
1081 let social_flags = social_data
1082 .as_ref()
1083 .map(|social_vec| {
1084 social_vec
1085 .iter()
1086 .map(|opt| if opt.is_some() { 1u8 } else { 0u8 })
1087 .collect::<Vec<u8>>()
1088 })
1089 .unwrap_or_else(|| vec![0u8; epochs.len()]);
1090
1091 data.extend_from_slice(&social_flags);
1092
1093 // Append social data for epochs that have social claims
1094 if let Some(social_data_vec) = social_data {
1095 for social_opt in social_data_vec {
1096 if let Some(social) = social_opt {
1097 // Serialize SocialClaimData using bytemuck::bytes_of for consistent formatting
1098 data.extend_from_slice(bytemuck::bytes_of(&social));
1099 }
1100 }
1101 }
1102
1103 // Create instruction with correct account structure matching program
1104 let proof_pda = proof_pda(signer);
1105 Ok(Instruction {
1106 program_id: crate::ID,
1107 accounts: vec![
1108 AccountMeta::new(signer, true), // [0] signer
1109 AccountMeta::new(beneficiary, false), // [1] beneficiary
1110 AccountMeta::new(proof_pda.0, false), // [2] proof PDA
1111 AccountMeta::new(TREASURY_ADDRESS, false), // [3] treasury
1112 AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), // [4] treasury tokens
1113 AccountMeta::new_readonly(spl_token::ID, false), // [5] token program
1114 AccountMeta::new_readonly(SNAPSHOT_ADDRESS, false), // [6] snapshot
1115 AccountMeta::new_readonly(CONFIG_ADDRESS, false), // [7] config
1116 ],
1117 data,
1118 })
1119}
1120
1121/// Builds a batch claim instruction for a vector of epochs.
1122///
1123/// This is the underlying function used by `batch_claim_date_range`.
1124/// Uses correct 8-account structure matching program.
1125/// It allows claiming rewards for specific epochs in a single transaction.
1126/// Supports both payment rewards and optional social rewards per epoch.
1127///
1128/// ## Parameters
1129/// - `signer`: The wallet signing the transaction
1130/// - `beneficiary`: The wallet receiving the rewards
1131/// - `epochs`: Vector of epoch numbers to claim
1132/// - `participant_type`: 0 for customer, 1 for merchant
1133/// - `max_batch_size`: Maximum epochs per batch (1-10)
1134/// - `epoch_proofs`: Vector of proof data for each epoch
1135/// - `social_data`: Optional vector of social data for each epoch (None = no social claim)
1136///
1137/// ## Returns
1138/// - Instruction for batch claiming rewards for specific epochs
1139///
1140/// ## Note
1141/// - Each epoch requires its own proof data
1142/// - The number of proofs must match the number of epochs
1143/// - Social data is optional per epoch (None = payment only, Some = payment + social)
1144/// - Epochs must be in ascending order for optimal gas efficiency
1145///
1146/// ## Data Structure
1147/// Each epoch proof contains:
1148/// - `DailyParticipantData`: Participant-specific information
1149/// - `Vec<[u8; 32]>`: Payment proof path
1150/// - `Vec<bool>`: Payment proof indices
1151/// - `Vec<[u8; 32]>`: Seal proof path
1152/// - `Vec<bool>`: Seal proof indices
1153/// - `[u8; 32]`: Payment root
1154/// - `EpochClaimData`: Epoch-specific reward and activity data
1155/// - `DailyParticipantData`: Participant data for each epoch
1156pub fn batch_claim_vector(
1157 signer: Pubkey,
1158 beneficiary: Pubkey,
1159 epochs: Vec<u64>,
1160 participant_type: u8,
1161 max_batch_size: u8,
1162 epoch_proofs: Vec<(
1163 Vec<[u8; 32]>, // payment_proof
1164 Vec<bool>, // payment_indices
1165 Vec<[u8; 32]>, // seal_proof
1166 Vec<bool>, // seal_indices
1167 [u8; 32], // payment_root
1168 EpochClaimData,
1169 DailyParticipantData, // participant data for each epoch
1170 )>,
1171 social_data: Option<Vec<Option<SocialClaimData>>>,
1172) -> Result<Instruction, crate::error::MiracleError> {
1173 if epochs.is_empty() {
1174 return Err(crate::error::MiracleError::InvalidInput);
1175 }
1176
1177 if epochs.len() > max_batch_size as usize {
1178 return Err(crate::error::MiracleError::InvalidInput);
1179 }
1180
1181 if epoch_proofs.len() != epochs.len() {
1182 return Err(crate::error::MiracleError::InvalidInput);
1183 }
1184
1185 // Validate social_data if provided
1186 if let Some(ref social_data_vec) = social_data {
1187 if social_data_vec.len() != epochs.len() {
1188 return Err(crate::error::MiracleError::InvalidInput);
1189 }
1190 }
1191
1192 // Create batch claim instruction
1193 let mut data = Vec::new();
1194
1195 // Use the BatchClaim struct's proper serialization like single claim does
1196 let batch_claim_data = BatchClaim {
1197 start_epoch: [0u8; 8], // Not used for vector claims
1198 end_epoch: [0u8; 8], // Not used for vector claims
1199 participant_type,
1200 claim_type: 1u8, // Vector claim = 1
1201 epoch_count: epochs.len() as u8,
1202 max_batch_size,
1203 _padding: [0u8; 4],
1204 };
1205 data.extend_from_slice(&batch_claim_data.to_bytes());
1206
1207 // Add epoch list with proper endianness like single claim
1208 for &epoch in &epochs {
1209 data.extend_from_slice(&epoch.to_le_bytes());
1210 }
1211
1212 // Add proof data for each epoch
1213 for epoch_proof in &epoch_proofs {
1214 let (
1215 payment_proof,
1216 payment_indices,
1217 seal_proof,
1218 seal_indices,
1219 payment_root,
1220 epoch_data,
1221 participant_data,
1222 ) = epoch_proof;
1223
1224 // Add payment proof length
1225 data.push(payment_proof.len() as u8);
1226
1227 // Add payment proof path
1228 for path_node in payment_proof {
1229 data.extend_from_slice(path_node);
1230 }
1231
1232 // Add payment indices
1233 for &is_right in payment_indices {
1234 data.push(if is_right { 1u8 } else { 0u8 });
1235 }
1236
1237 // Add seal proof length
1238 data.push(seal_proof.len() as u8);
1239
1240 // Add seal proof path
1241 for path_node in seal_proof {
1242 data.extend_from_slice(path_node);
1243 }
1244
1245 // Add seal indices
1246 for &is_right in seal_indices {
1247 data.push(if is_right { 1u8 } else { 0u8 });
1248 }
1249
1250 // Add payment root
1251 data.extend_from_slice(payment_root);
1252
1253 // Add epoch data
1254 data.extend_from_slice(bytemuck::bytes_of(epoch_data));
1255
1256 // Add participant data for this epoch
1257 data.extend_from_slice(bytemuck::bytes_of(participant_data));
1258 }
1259
1260 // Add social flags (1 byte per epoch indicating if social claim exists)
1261 let social_flags = social_data
1262 .as_ref()
1263 .map(|social_vec| {
1264 social_vec
1265 .iter()
1266 .map(|opt| if opt.is_some() { 1u8 } else { 0u8 })
1267 .collect::<Vec<u8>>()
1268 })
1269 .unwrap_or_else(|| vec![0u8; epochs.len()]);
1270
1271 data.extend_from_slice(&social_flags);
1272
1273 // Add social data for epochs that have social claims
1274 if let Some(social_data_vec) = social_data {
1275 for social_opt in social_data_vec {
1276 if let Some(social) = social_opt {
1277 data.extend_from_slice(bytemuck::bytes_of(&social));
1278 }
1279 }
1280 }
1281
1282 // Create instruction with correct account structure
1283 let proof_pda = proof_pda(signer);
1284 Ok(Instruction {
1285 program_id: crate::ID,
1286 accounts: vec![
1287 AccountMeta::new(signer, true), // [0] signer
1288 AccountMeta::new(beneficiary, false), // [1] beneficiary
1289 AccountMeta::new(proof_pda.0, false), // [2] proof PDA
1290 AccountMeta::new(TREASURY_ADDRESS, false), // [3] treasury
1291 AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), // [4] treasury tokens
1292 AccountMeta::new_readonly(spl_token::ID, false), // [5] token program
1293 AccountMeta::new_readonly(SNAPSHOT_ADDRESS, false), // [6] snapshot
1294 AccountMeta::new_readonly(CONFIG_ADDRESS, false), // [7] config
1295 ],
1296 data,
1297 })
1298}
1299
1300/// Build an UpdateTargets instruction to update community targets and activity limits.
1301/// This function allows the oracle to update configurable parameters based on community performance.
1302///
1303/// ## Parameters
1304/// - `oracle_authority`: The oracle authority that can update these parameters
1305/// - `target_weekly_users`: Target weekly active users (default: 500 for launch)
1306/// - `target_weekly_activity`: Target weekly activity count (default: 5,000 for launch)
1307/// - `target_retention_rate`: Target retention rate in basis points (default: 5,000 = 50%)
1308/// - `max_customer_activity_per_epoch`: Max customer activities per epoch (default: 5)
1309/// - `max_merchant_activity_per_epoch`: Max merchant activities per epoch (default: 50)
1310/// - `activity_cap_enabled`: Whether activity capping is enabled (default: 1 = enabled)
1311/// - `claim_cap_enabled`: Whether claim capping is enabled (default: 1 = enabled)
1312///
1313/// ## Returns
1314/// - Instruction to update community targets and activity limits
1315///
1316/// ## Authority
1317/// - Only the oracle authority can execute this instruction
1318/// - Oracle has the data to make informed decisions about community health
1319///
1320/// ## Benefits
1321/// - **Operational Flexibility**: Adjust parameters based on real community performance
1322/// - **Anti-Gaming**: Update activity limits to prevent new gaming strategies
1323/// - **Community Adaptation**: Adjust targets as community grows and evolves
1324/// - **Economic Balance**: Fine-tune parameters for sustainable tokenomics
1325/// - **Risk Management**: Adjust claim rewards threshold based on market conditions
1326///
1327/// ## Example
1328/// ```rust
1329/// use miracle_api::sdk;
1330/// use solana_program::pubkey::Pubkey;
1331///
1332/// let oracle_authority = Pubkey::new_unique();
1333/// let update_targets_ix = sdk::update_targets(
1334/// oracle_authority,
1335/// 1000, // target_weekly_users
1336/// 10000, // target_weekly_activity
1337/// 6000, // target_retention_rate (60%)
1338/// 10, // max_customer_activity_per_epoch
1339/// 100, // max_merchant_activity_per_epoch
1340/// 1, // activity_cap_enabled
1341/// 1, // claim_cap_enabled
1342/// 1000000, // claim_rewards_threshold
1343/// );
1344/// ```
1345pub fn update_targets(
1346 oracle_authority: Pubkey,
1347 target_weekly_users: u32,
1348 target_weekly_activity: u32,
1349 target_retention_rate: u16,
1350 max_customer_activity_per_epoch: u32,
1351 max_merchant_activity_per_epoch: u32,
1352 activity_cap_enabled: u8,
1353 claim_cap_enabled: u8,
1354 claim_rewards_threshold: u64,
1355) -> Instruction {
1356 let data = UpdateTargets {
1357 target_weekly_users: target_weekly_users.to_le_bytes(),
1358 target_weekly_activity: target_weekly_activity.to_le_bytes(),
1359 target_retention_rate: target_retention_rate.to_le_bytes(),
1360 max_customer_activity_per_epoch: max_customer_activity_per_epoch.to_le_bytes(),
1361 max_merchant_activity_per_epoch: max_merchant_activity_per_epoch.to_le_bytes(),
1362 activity_cap_enabled,
1363 claim_cap_enabled,
1364 claim_rewards_threshold: claim_rewards_threshold.to_le_bytes(),
1365 _padding: [0; 4],
1366 }
1367 .to_bytes();
1368
1369 Instruction {
1370 program_id: crate::ID,
1371 accounts: vec![
1372 AccountMeta::new(oracle_authority, true),
1373 AccountMeta::new(CONFIG_ADDRESS, false),
1374 ],
1375 data,
1376 }
1377}
1378
1379// ===== MERKLE TREE CREATION MODULE =====
1380
1381/// Merkle tree creation result containing the root and proof generation capabilities.
1382///
1383/// This structure provides everything needed for off-chain processors to:
1384/// 1. Generate the merkle root for snapshot updates
1385/// 2. Generate merkle proofs for individual claims
1386/// 3. Verify merkle proofs for validation
1387///
1388/// ## Security
1389/// - **Deterministic**: Same input data always produces the same merkle root
1390/// - **Cryptographic**: Uses Solana hashv for collision resistance
1391/// - **Efficient**: O(n) construction time, O(log n) proof generation
1392/// - **Verifiable**: All proofs can be verified on-chain
1393#[repr(C)]
1394#[derive(Clone, Debug, Serialize, Deserialize)]
1395pub struct MerkleTree {
1396 /// The merkle root hash (32 bytes) - used for snapshot updates
1397 pub root: [u8; 32],
1398 /// Internal tree structure for proof generation
1399 tree: Vec<Vec<[u8; 32]>>,
1400 /// Original participant data for proof generation
1401 participants: Vec<DailyParticipantData>,
1402}
1403
1404impl MerkleTree {
1405 /// Create a new merkle tree from participant data.
1406 ///
1407 /// ## Parameters
1408 /// - `participants`: Vector of daily participant data
1409 ///
1410 /// ## Returns
1411 /// - `MerkleTree` instance with root and proof generation capabilities
1412 ///
1413 /// ## Panics
1414 /// - If participants vector is empty
1415 ///
1416 /// ## Example
1417 /// ```rust
1418 /// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree}};
1419 ///
1420 /// let participants = vec![
1421 /// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1422 /// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
1423 /// ];
1424 ///
1425 /// let merkle_tree = MerkleTree::new(participants);
1426 /// println!("Merkle root: {:?}", merkle_tree.root());
1427 /// ```
1428 pub fn new(mut participants: Vec<DailyParticipantData>) -> Self {
1429 if participants.is_empty() {
1430 panic!("Cannot create merkle tree from empty participants list");
1431 }
1432
1433 // Validate all participants
1434 for participant in &participants {
1435 participant.validate().expect("Invalid participant data");
1436 }
1437
1438 // Sort participants deterministically by participant_id for consistent merkle tree construction
1439 // This ensures that the same participants always produce the same merkle root regardless of input order
1440 sort_participants_by_id(&mut participants);
1441
1442 // Generate leaf hashes from sorted participants
1443 let mut leaves: Vec<[u8; 32]> =
1444 participants.iter().map(|p| p.compute_leaf_hash()).collect();
1445
1446 // CRITICAL: Handle single leaf case - duplicate the leaf to force one hash operation
1447 // This matches Oracle's logic exactly for consistency
1448 if leaves.len() == 1 {
1449 leaves.push(leaves[0]); // Duplicate the single leaf
1450 }
1451
1452 // Build merkle tree bottom-up
1453 let mut tree = vec![leaves.clone()];
1454
1455 while leaves.len() > 1 {
1456 let mut new_level = Vec::new();
1457 for chunk in leaves.chunks(2) {
1458 if chunk.len() == 2 {
1459 let combined = hashv(&[&chunk[0], &chunk[1]]).to_bytes();
1460 new_level.push(combined);
1461 } else {
1462 // Single element case - hash with itself for consistency
1463 let combined = hashv(&[&chunk[0], &chunk[0]]).to_bytes();
1464 new_level.push(combined);
1465 }
1466 }
1467 leaves = new_level;
1468 tree.push(leaves.clone());
1469 }
1470
1471 println!("🔍 Debug: Built tree with {} levels", tree.len());
1472 for (i, level) in tree.iter().enumerate() {
1473 println!("🔍 Debug: Level {} has {} nodes", i, level.len());
1474 }
1475
1476 let root = tree.last().unwrap()[0];
1477 println!("🔍 Debug: Root from tree: {:?}", root);
1478 println!("🔍 Debug: Root from leaves: {:?}", leaves[0]);
1479
1480 Self {
1481 root,
1482 tree,
1483 participants,
1484 }
1485 }
1486
1487 /// Get the merkle root hash.
1488 ///
1489 /// ## Returns
1490 /// - 32-byte merkle root hash for snapshot updates
1491 pub fn root(&self) -> [u8; 32] {
1492 self.root
1493 }
1494
1495 /// Generate a merkle proof for a specific participant.
1496 ///
1497 /// ## Parameters
1498 /// - `participant_index`: Index of the participant in the original data
1499 ///
1500 /// ## Returns
1501 /// - `MerkleProof` containing path and indices for verification
1502 ///
1503 /// ## Panics
1504 /// - If participant_index is out of bounds
1505 ///
1506 /// ## Example
1507 /// ```rust
1508 /// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree, EpochClaimData}};
1509 /// use solana_program::pubkey::Pubkey;
1510 ///
1511 /// let participants = vec![
1512 /// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1513 /// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
1514 /// ];
1515 ///
1516 /// let merkle_tree = MerkleTree::new(participants);
1517 /// let payment_proof = merkle_tree.generate_proof(0);
1518 /// let seal_proof = sdk::SealProof::new_for_verification(vec![[2u8; 32]], vec![true], merkle_tree.root());
1519 ///
1520 /// // Use proof for claim instruction using dual merkle tree
1521 /// let customer_wallet = Pubkey::new_unique();
1522 /// let beneficiary = Pubkey::new_unique();
1523 /// let epoch = 0u64;
1524 /// let participant_type = 0u8;
1525 /// let claim_ix = sdk::claim(
1526 /// customer_wallet,
1527 /// beneficiary,
1528 /// epoch,
1529 /// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1530 /// payment_proof.path,
1531 /// payment_proof.indices,
1532 /// seal_proof.path,
1533 /// seal_proof.indices,
1534 /// seal_proof.payment_root,
1535 /// EpochClaimData {
1536 /// customer_reward_pool: 1000000u64.to_le_bytes(),
1537 /// merchant_reward_pool: 500000u64.to_le_bytes(),
1538 /// total_customer_activity: 0u64.to_le_bytes(),
1539 /// total_merchant_activity: 0u64.to_le_bytes(),
1540 /// },
1541 /// participant_type,
1542 /// None, // social_data (no social claim)
1543 /// );
1544 /// ```
1545 pub fn generate_proof(&self, participant_index: usize) -> MerkleProof {
1546 if participant_index >= self.participants.len() {
1547 panic!("Participant index out of bounds");
1548 }
1549
1550 let mut path = Vec::new();
1551 let mut indices = Vec::new();
1552 let mut current_index = participant_index;
1553
1554 println!(
1555 "🔍 Debug: Generating proof for participant index {}",
1556 participant_index
1557 );
1558 println!("🔍 Debug: Tree has {} levels", self.tree.len());
1559
1560 // Traverse up the tree to build the proof
1561 for level in 0..self.tree.len() - 1 {
1562 let level_size = self.tree[level].len();
1563 println!(
1564 "🔍 Debug: Level {} has {} nodes, current_index = {}",
1565 level, level_size, current_index
1566 );
1567
1568 if current_index % 2 == 0 {
1569 // Left child - need right sibling
1570 if current_index + 1 < level_size {
1571 let sibling = self.tree[level][current_index + 1];
1572 path.push(sibling);
1573 indices.push(true); // true = sibling is on the right
1574 println!(
1575 "🔍 Debug: Level {}: Left child, adding right sibling {:?}",
1576 level, sibling
1577 );
1578 }
1579 } else {
1580 // Right child - need left sibling
1581 let sibling = self.tree[level][current_index - 1];
1582 path.push(sibling);
1583 indices.push(false); // false = sibling is on the left
1584 println!(
1585 "🔍 Debug: Level {}: Right child, adding left sibling {:?}",
1586 level, sibling
1587 );
1588 }
1589
1590 current_index /= 2;
1591 }
1592
1593 println!(
1594 "🔍 Debug: Generated proof with {} path nodes and indices {:?}",
1595 path.len(),
1596 indices
1597 );
1598 MerkleProof { path, indices }
1599 }
1600
1601 /// Find participant by ID and generate proof.
1602 ///
1603 /// ## Parameters
1604 /// - `participant_id`: The participant ID to find
1605 ///
1606 /// ## Returns
1607 /// - `Some(MerkleProof)` if participant found
1608 /// - `None` if participant not found
1609 ///
1610 /// ## Example
1611 /// ```rust
1612 /// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree}};
1613 ///
1614 /// let participants = vec![
1615 /// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1616 /// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
1617 /// ];
1618 ///
1619 /// let merkle_tree = MerkleTree::new(participants);
1620 ///
1621 /// if let Some(payment_proof) = merkle_tree.find_and_generate_proof([1u8; 32]) {
1622 /// // Use payment proof for dual merkle tree claim
1623 /// }
1624 /// ```
1625 pub fn find_and_generate_proof(&self, participant_id: [u8; 32]) -> Option<MerkleProof> {
1626 self.participants
1627 .iter()
1628 .position(|p| p.participant_id == participant_id)
1629 .map(|index| self.generate_proof(index))
1630 }
1631
1632 /// Get the number of participants in the tree.
1633 ///
1634 /// ## Returns
1635 /// - Number of participants
1636 pub fn participant_count(&self) -> usize {
1637 self.participants.len()
1638 }
1639
1640 /// Get participant data by index.
1641 ///
1642 /// ## Parameters
1643 /// - `index`: Participant index
1644 ///
1645 /// ## Returns
1646 /// - `Some(&DailyParticipantData)` if index is valid
1647 /// - `None` if index is out of bounds
1648 pub fn get_participant(&self, index: usize) -> Option<&DailyParticipantData> {
1649 self.participants.get(index)
1650 }
1651
1652 /// Get all participant data.
1653 ///
1654 /// ## Returns
1655 /// - Reference to all participant data
1656 pub fn participants(&self) -> &[DailyParticipantData] {
1657 &self.participants
1658 }
1659}
1660
1661/// Merkle proof for a specific participant.
1662///
1663/// This structure contains the path and direction indices needed to verify
1664/// that a participant's data is included in the merkle tree.
1665///
1666/// ## Security
1667/// - **Cryptographic**: Uses Solana hashv for collision resistance
1668/// - **Deterministic**: Same participant always generates same proof
1669/// - **Verifiable**: Can be verified on-chain against merkle root
1670#[repr(C)]
1671#[derive(Clone, Debug, Serialize, Deserialize)]
1672pub struct MerkleProof {
1673 /// Merkle path nodes (sibling hashes)
1674 pub path: Vec<[u8; 32]>,
1675 /// Direction indices (false = left, true = right)
1676 pub indices: Vec<bool>,
1677}
1678
1679/// Seal proof structure for dual merkle tree verification.
1680/// Contains both the merkle proof path and the payment root being proven.
1681/// Enhanced with epoch-specific data for accurate historical reward calculations.
1682#[repr(C)]
1683#[derive(Clone, Debug, Serialize, Deserialize)]
1684pub struct SealProof {
1685 /// Seal merkle path nodes (sibling hashes)
1686 pub path: Vec<[u8; 32]>,
1687 /// Seal direction indices (false = left, true = right)
1688 pub indices: Vec<bool>,
1689 /// Payment root being proven (embedded in seal proof)
1690 pub payment_root: [u8; 32],
1691 /// Epoch-specific reward calculation data for accurate historical calculations
1692 pub customer_reward_pool: u64,
1693 pub merchant_reward_pool: u64,
1694 pub total_customer_activity: u64,
1695 pub total_merchant_activity: u64,
1696 pub community_score: u16,
1697 pub customer_reward_share: u16,
1698 pub merchant_reward_share: u16,
1699 pub weekly_active_users: u32,
1700 pub weekly_retention_rate: u16,
1701 pub weekly_activity_count: u32,
1702}
1703
1704impl MerkleProof {
1705 /// Create a new merkle proof.
1706 ///
1707 /// ## Parameters
1708 /// - `path`: Vector of merkle path nodes
1709 /// - `indices`: Vector of direction indices
1710 ///
1711 /// ## Returns
1712 /// - New MerkleProof instance
1713 pub fn new(path: Vec<[u8; 32]>, indices: Vec<bool>) -> Self {
1714 Self { path, indices }
1715 }
1716
1717 /// Verify this proof against a merkle root and participant data.
1718 ///
1719 /// ## Parameters
1720 /// - `participant`: The participant data to verify
1721 /// - `merkle_root`: The expected merkle root
1722 ///
1723 /// ## Returns
1724 /// - `true` if proof is valid
1725 /// - `false` if proof is invalid
1726 ///
1727 /// ## Example
1728 /// ```rust
1729 /// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree}};
1730 ///
1731 /// let participant = DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0);
1732 /// let participants = vec![participant.clone()];
1733 /// let merkle_tree = MerkleTree::new(participants);
1734 /// let proof = merkle_tree.generate_proof(0);
1735 ///
1736 /// assert!(proof.verify(&participant, merkle_tree.root()));
1737 /// ```
1738 pub fn verify(&self, participant: &DailyParticipantData, merkle_root: [u8; 32]) -> bool {
1739 if self.path.len() != self.indices.len() {
1740 return false;
1741 }
1742
1743 let mut current_hash = participant.compute_leaf_hash();
1744 println!(
1745 "🔍 Debug: Starting verification with leaf hash: {:?}",
1746 current_hash
1747 );
1748
1749 // Traverse the proof path
1750 for (i, &path_node) in self.path.iter().enumerate() {
1751 let is_right = self.indices[i];
1752 println!(
1753 "🔍 Debug: Step {}: is_right={}, path_node={:?}",
1754 i, is_right, path_node
1755 );
1756
1757 if is_right {
1758 // When is_right is true, the sibling is on the right
1759 // So current_hash should be on the left, path_node on the right
1760 // Use hashv(&[&left, &right]) to match tree construction
1761 current_hash = hashv(&[¤t_hash, &path_node]).to_bytes();
1762 } else {
1763 // When is_right is false, the sibling is on the left
1764 // So path_node should be on the left, current_hash on the right
1765 // Use hashv(&[&left, &right]) to match tree construction
1766 current_hash = hashv(&[&path_node, ¤t_hash]).to_bytes();
1767 }
1768 println!("🔍 Debug: Step {}: current hash = {:?}", i, current_hash);
1769 }
1770
1771 println!(
1772 "🔍 Debug: Final hash: {:?}, Expected root: {:?}",
1773 current_hash, merkle_root
1774 );
1775 current_hash == merkle_root
1776 }
1777
1778 /// Get the proof length (number of path nodes).
1779 ///
1780 /// ## Returns
1781 /// - Number of nodes in the proof path
1782 pub fn length(&self) -> usize {
1783 self.path.len()
1784 }
1785
1786 /// Check if this is an empty proof (single leaf tree).
1787 ///
1788 /// ## Returns
1789 /// - `true` if proof is empty
1790 /// - `false` if proof has path nodes
1791 pub fn is_empty(&self) -> bool {
1792 self.path.is_empty()
1793 }
1794
1795 /// Serialize the MerkleProof to bytes (for use in transactions, batch claims, etc.)
1796 pub fn serialize(&self) -> Vec<u8> {
1797 bincode::serialize(self).expect("MerkleProof serialization failed")
1798 }
1799
1800 /// Deserialize a MerkleProof from bytes.
1801 pub fn deserialize(data: &[u8]) -> Self {
1802 bincode::deserialize(data).expect("MerkleProof deserialization failed")
1803 }
1804}
1805
1806/// Create a merkle tree from participant data.
1807///
1808/// This is a convenience function that creates a merkle tree from a vector
1809/// of participant data. It's the primary entry point for off-chain processors.
1810///
1811/// ## Parameters
1812/// - `participants`: Vector of daily participant data
1813///
1814/// ## Returns
1815/// - `MerkleTree` instance with root and proof generation capabilities
1816///
1817/// ## Panics
1818/// - If participants vector is empty
1819///
1820/// ## Example
1821/// ```rust
1822/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree}};
1823///
1824/// // Off-chain processor aggregates daily payments
1825/// let participants = vec![
1826/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1827/// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
1828/// ];
1829///
1830/// // Create merkle tree
1831/// let merkle_tree = sdk::create_merkle_tree(participants);
1832/// println!("Merkle root: {:?}", merkle_tree.root());
1833/// ```
1834pub fn create_merkle_tree(participants: Vec<DailyParticipantData>) -> MerkleTree {
1835 MerkleTree::new(participants)
1836}
1837
1838/// Verify a merkle proof against a merkle root and participant data.
1839///
1840/// This is a convenience function for proof verification that can be used
1841/// both off-chain (for testing) and on-chain (in claim instructions).
1842///
1843/// ## Parameters
1844/// - `participant`: The participant data to verify
1845/// - `proof`: The merkle proof
1846/// - `merkle_root`: The expected merkle root
1847///
1848/// ## Returns
1849/// - `true` if proof is valid
1850/// - `false` if proof is invalid
1851///
1852/// ## Example
1853/// ```rust
1854/// use miracle_api::{sdk, prelude::DailyParticipantData};
1855///
1856/// let participant = DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0);
1857/// let participants = vec![participant.clone()];
1858/// let merkle_tree = sdk::create_merkle_tree(participants);
1859/// let proof = merkle_tree.generate_proof(0);
1860///
1861/// assert!(sdk::verify_merkle_proof(&participant, &proof, merkle_tree.root()));
1862/// ```
1863pub fn verify_merkle_proof(
1864 participant: &DailyParticipantData,
1865 proof: &MerkleProof,
1866 merkle_root: [u8; 32],
1867) -> bool {
1868 proof.verify(participant, merkle_root)
1869}
1870
1871/// Find the index of a participant in the participant list.
1872///
1873/// This helper function allows users to find their index in the Merkle tree
1874/// without needing to know the internal ordering. This is useful for:
1875/// - Generating proofs for claims
1876/// - Verifying participant inclusion
1877/// - Building user-friendly interfaces
1878///
1879/// ## Parameters
1880/// - `participants`: Vector of daily participant data
1881/// - `participant_id`: The participant ID to find
1882///
1883/// ## Returns
1884/// - `Some(index)` if participant found
1885/// - `None` if participant not found
1886///
1887/// ## Example
1888/// ```rust
1889/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree, EpochClaimData}};
1890/// use solana_program::pubkey::Pubkey;
1891///
1892/// let participants = vec![
1893/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1894/// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
1895/// ];
1896///
1897/// let my_id = [1u8; 32];
1898/// if let Some(my_index) = sdk::find_participant_index(&participants, my_id) {
1899/// println!("My index in the Merkle tree: {}", my_index);
1900///
1901/// // Generate proof using the index
1902/// let merkle_tree = sdk::create_merkle_tree(participants);
1903/// let proof = merkle_tree.generate_proof(my_index);
1904///
1905/// // Use proof for claim
1906/// let customer_wallet = Pubkey::new_unique();
1907/// let beneficiary = Pubkey::new_unique();
1908/// let epoch = 0u64;
1909/// let activity_count = 5u32;
1910/// let participant_type = 0u8;
1911/// let payment_proof = merkle_tree.generate_proof(my_index);
1912/// let seal_proof = sdk::SealProof::new_for_verification(vec![[2u8; 32]], vec![true], merkle_tree.root());
1913/// let claim_ix = sdk::claim(
1914/// customer_wallet,
1915/// beneficiary,
1916/// epoch,
1917/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1918/// payment_proof.path,
1919/// payment_proof.indices,
1920/// seal_proof.path,
1921/// seal_proof.indices,
1922/// seal_proof.payment_root,
1923/// EpochClaimData {
1924/// customer_reward_pool: 1000000u64.to_le_bytes(),
1925/// merchant_reward_pool: 500000u64.to_le_bytes(),
1926/// total_customer_activity: 0u64.to_le_bytes(),
1927/// total_merchant_activity: 0u64.to_le_bytes(),
1928/// },
1929/// participant_type,
1930/// None, // social_data (no social claim)
1931/// );
1932/// }
1933/// ```
1934///
1935/// ## Alternative Approaches
1936/// Instead of finding the index, you can also use:
1937/// ```rust
1938/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree}};
1939///
1940/// let participants = vec![
1941/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
1942/// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
1943/// ];
1944///
1945/// let my_id = [1u8; 32];
1946/// // Direct proof generation by participant ID
1947/// let merkle_tree = sdk::create_merkle_tree(participants);
1948/// if let Some(proof) = merkle_tree.find_and_generate_proof(my_id) {
1949/// // Use proof directly
1950/// }
1951/// ```
1952pub fn find_participant_index(
1953 participants: &[DailyParticipantData],
1954 participant_id: [u8; 32],
1955) -> Option<usize> {
1956 participants
1957 .iter()
1958 .position(|p| p.participant_id == participant_id)
1959}
1960
1961/// Helper to sort participants deterministically by participant_id (lexicographically).
1962/// This is recommended for trustless Merkle tree construction.
1963pub fn sort_participants_by_id(participants: &mut [DailyParticipantData]) {
1964 participants.sort_by(|a, b| a.participant_id.cmp(&b.participant_id));
1965}
1966
1967/// Convert wallet address to participant ID (32-byte hash).
1968///
1969/// This function creates a deterministic participant ID from a wallet address
1970/// by hashing the address bytes. This ensures consistent participant identification
1971/// across different systems while maintaining privacy.
1972///
1973/// ## Parameters
1974/// - `wallet_address`: Base58 encoded Solana wallet address
1975///
1976/// ## Returns
1977/// - 32-byte participant ID hash
1978///
1979/// ## Example
1980/// ```rust
1981/// use miracle_api::sdk;
1982///
1983/// let wallet = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
1984/// let participant_id = sdk::wallet_to_participant_id(wallet);
1985/// println!("Participant ID: {:?}", participant_id);
1986/// ```
1987///
1988/// ## Note
1989/// - Uses Solana's hashv function for deterministic results
1990/// - Same wallet address always produces same participant ID
1991/// - 32-byte output matches DailyParticipantData::participant_id type
1992/// - Matches on-chain participant ID computation exactly
1993pub fn wallet_to_participant_id(wallet_address: &str) -> [u8; 32] {
1994 use solana_program::pubkey::Pubkey;
1995 use std::str::FromStr;
1996
1997 // Parse wallet address to Pubkey first, then hash the bytes (same as on-chain)
1998 match Pubkey::from_str(wallet_address) {
1999 Ok(pubkey) => {
2000 let wallet_bytes = pubkey.to_bytes();
2001 hashv(&[&wallet_bytes]).to_bytes()
2002 }
2003 Err(_) => {
2004 // Fallback to hash of string bytes if Pubkey parsing fails
2005 hashv(&[wallet_address.as_bytes()]).to_bytes()
2006 }
2007 }
2008}
2009
2010/// Find participant by wallet address in a participant list.
2011///
2012/// This function converts the wallet address to a participant ID and then
2013/// searches for it in the participant list. Useful for user-friendly lookups
2014/// when you have the wallet address but need to find the participant data.
2015///
2016/// ## Parameters
2017/// - `participants`: Vector of daily participant data
2018/// - `wallet_address`: Base58 encoded Solana wallet address
2019///
2020/// ## Returns
2021/// - `Some(index)` if participant found
2022/// - `None` if participant not found
2023///
2024/// ## Example
2025/// ```rust
2026/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree, EpochClaimData}};
2027/// use solana_program::pubkey::Pubkey;
2028///
2029/// let participants = vec![
2030/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
2031/// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
2032/// ];
2033///
2034/// let wallet = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
2035/// if let Some(index) = sdk::find_participant_by_wallet(&participants, wallet) {
2036/// println!("Found participant at index: {}", index);
2037///
2038/// // Generate proof using the index
2039/// let merkle_tree = sdk::create_merkle_tree(participants);
2040/// let proof = merkle_tree.generate_proof(index);
2041///
2042/// // Use proof for claim
2043/// let customer_wallet = Pubkey::new_unique();
2044/// let beneficiary = Pubkey::new_unique();
2045/// let epoch = 0u64;
2046/// let activity_count = 5u32;
2047/// let participant_type = 0u8;
2048/// let payment_proof = merkle_tree.generate_proof(index);
2049/// let seal_proof = sdk::SealProof::new_for_verification(vec![[2u8; 32]], vec![true], merkle_tree.root());
2050/// let claim_ix = sdk::claim(
2051/// customer_wallet,
2052/// beneficiary,
2053/// epoch,
2054/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
2055/// payment_proof.path,
2056/// payment_proof.indices,
2057/// seal_proof.path,
2058/// seal_proof.indices,
2059/// seal_proof.payment_root,
2060/// EpochClaimData {
2061/// customer_reward_pool: 1000000u64.to_le_bytes(),
2062/// merchant_reward_pool: 500000u64.to_le_bytes(),
2063/// total_customer_activity: 0u64.to_le_bytes(),
2064/// total_merchant_activity: 0u64.to_le_bytes(),
2065/// },
2066/// participant_type,
2067/// None, // social_data (no social claim)
2068/// );
2069/// }
2070/// ```
2071pub fn find_participant_by_wallet(
2072 participants: &[DailyParticipantData],
2073 wallet_address: &str,
2074) -> Option<usize> {
2075 let participant_id = wallet_to_participant_id(wallet_address);
2076 participants
2077 .iter()
2078 .position(|p| p.participant_id == participant_id)
2079}
2080
2081/// Create DailyParticipantData from wallet address and payment information.
2082///
2083/// This convenience function creates a DailyParticipantData instance from
2084/// a wallet address, automatically converting it to the required participant ID.
2085/// Useful for off-chain processors when aggregating payment data.
2086///
2087/// ## Parameters
2088/// - `wallet_address`: Base58 encoded Solana wallet address
2089/// - `first_payment_timestamp`: Unix timestamp of first payment
2090/// - `last_payment_timestamp`: Unix timestamp of last payment
2091/// - `first_payment_tx_sig_short`: First payment transaction signature (short)
2092/// - `last_payment_tx_sig_short`: Last payment transaction signature (short)
2093/// - `payment_count`: Number of transactions for this participant
2094/// - `participant_type`: 0 for customer, 1 for merchant
2095///
2096/// ## Returns
2097/// - New DailyParticipantData instance
2098///
2099/// ## Example
2100/// ```rust
2101/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree}};
2102///
2103/// // Off-chain processor aggregating daily payments
2104/// let customer_wallet = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
2105/// let customer_data = sdk::create_participant_from_wallet(
2106/// customer_wallet,
2107/// 1723680000, // first payment timestamp
2108/// 1723766400, // last payment timestamp
2109/// [1u8; 8], // first payment tx sig short
2110/// [2u8; 8], // last payment tx sig short
2111/// 5, // 5 transactions
2112/// 0, // customer type
2113/// );
2114///
2115/// let merchant_wallet = "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU";
2116/// let merchant_data = sdk::create_participant_from_wallet(
2117/// merchant_wallet,
2118/// 1723680000, // first payment timestamp
2119/// 1723766400, // last payment timestamp
2120/// [3u8; 8], // first payment tx sig short
2121/// [4u8; 8], // last payment tx sig short
2122/// 10, // 10 transactions
2123/// 1, // merchant type
2124/// );
2125///
2126/// // Create merkle tree from aggregated data
2127/// let participants = vec![customer_data, merchant_data];
2128/// let merkle_tree = sdk::create_merkle_tree(participants);
2129/// println!("Merkle root: {:?}", merkle_tree.root());
2130/// ```
2131pub fn create_participant_from_wallet(
2132 wallet_address: &str,
2133 first_payment_timestamp: i64,
2134 last_payment_timestamp: i64,
2135 first_payment_tx_sig_short: [u8; 8],
2136 last_payment_tx_sig_short: [u8; 8],
2137 payment_count: u32,
2138 participant_type: u8,
2139) -> DailyParticipantData {
2140 DailyParticipantData::new(
2141 wallet_to_participant_id(wallet_address),
2142 first_payment_timestamp,
2143 last_payment_timestamp,
2144 first_payment_tx_sig_short,
2145 last_payment_tx_sig_short,
2146 payment_count,
2147 participant_type,
2148 )
2149}
2150
2151/// Generate merkle proof for a wallet address.
2152///
2153/// This convenience function combines wallet address lookup and proof generation.
2154/// It finds the participant by wallet address and generates their merkle proof
2155/// in one step. Useful for user-facing applications where you have the wallet
2156/// address but need the proof for claiming.
2157///
2158/// ## Parameters
2159/// - `participants`: Vector of daily participant data
2160/// - `wallet_address`: Base58 encoded Solana wallet address
2161///
2162/// ## Returns
2163/// - `Some(MerkleProof)` if participant found
2164/// - `None` if participant not found
2165///
2166/// ## Example
2167/// ```rust
2168/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleTree, EpochClaimData}};
2169/// use solana_program::pubkey::Pubkey;
2170///
2171/// let participants = vec![
2172/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
2173/// DailyParticipantData::new([2u8; 32], 1723680000, 1723766400, [3u8; 8], [4u8; 8], 10, 1),
2174/// ];
2175///
2176/// let wallet = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
2177/// if let Some(proof) = sdk::generate_proof_for_wallet(&participants, wallet) {
2178/// println!("Generated proof with {} path nodes", proof.length());
2179///
2180/// // Use proof for claim
2181/// let merkle_tree = sdk::create_merkle_tree(participants);
2182/// let customer_wallet = Pubkey::new_unique();
2183/// let beneficiary = Pubkey::new_unique();
2184/// let epoch = 0u64;
2185/// let activity_count = 5u32;
2186/// let participant_type = 0u8;
2187/// let payment_proof = proof;
2188/// let seal_proof = sdk::SealProof::new_for_verification(vec![[2u8; 32]], vec![true], merkle_tree.root());
2189/// let claim_ix = sdk::claim(
2190/// customer_wallet,
2191/// beneficiary,
2192/// epoch,
2193/// DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0),
2194/// payment_proof.path,
2195/// payment_proof.indices,
2196/// seal_proof.path,
2197/// seal_proof.indices,
2198/// seal_proof.payment_root,
2199/// EpochClaimData {
2200/// customer_reward_pool: 1000000u64.to_le_bytes(),
2201/// merchant_reward_pool: 500000u64.to_le_bytes(),
2202/// total_customer_activity: 0u64.to_le_bytes(),
2203/// total_merchant_activity: 0u64.to_le_bytes(),
2204/// },
2205/// participant_type,
2206/// None, // social_data (no social claim)
2207/// );
2208/// }
2209/// ```
2210pub fn generate_proof_for_wallet(
2211 participants: &[DailyParticipantData],
2212 wallet_address: &str,
2213) -> Option<MerkleProof> {
2214 let merkle_tree = create_merkle_tree(participants.to_vec());
2215 merkle_tree.find_and_generate_proof(wallet_to_participant_id(wallet_address))
2216}
2217
2218// ===== CLAIMS TRACKING MODULE =====
2219
2220/// Generate a unique claim key for tracking claims.
2221///
2222/// The claim key is a hash of participant ID, epoch, participant type, and claim type.
2223/// This ensures each claim is uniquely identifiable and prevents double-spending.
2224///
2225/// ## Parameters
2226/// - `participant_id`: 32-byte participant ID (wallet hash)
2227/// - `epoch`: Epoch number
2228/// - `participant_type`: 0 = customer, 1 = merchant
2229/// - `claim_type`: 1 = payment only, 3 = payment + social
2230///
2231/// ## Returns
2232/// - 32-byte claim key hash
2233///
2234/// ## Security
2235/// - **Unique**: Each (participant, epoch, type) combination has unique key
2236/// - **Collision Resistant**: Uses SHA-256 for cryptographic security
2237/// - **Deterministic**: Same inputs always produce same key
2238pub fn generate_claim_key(
2239 participant_id: [u8; 32],
2240 epoch: u64,
2241 participant_type: u8,
2242 claim_type: u8,
2243) -> [u8; 32] {
2244 use solana_program::hash::hashv;
2245
2246 hashv(&[
2247 &participant_id,
2248 &epoch.to_le_bytes(),
2249 &participant_type.to_le_bytes(),
2250 &claim_type.to_le_bytes(),
2251 ])
2252 .to_bytes()
2253}
2254
2255// ===== DUAL MERKLE TREE VERIFICATION FUNCTIONS =====
2256
2257/// Verify a dual merkle claim using both payment and seal proofs.
2258///
2259/// This function implements the core security verification for the Dual Merkle Tree system.
2260/// It verifies both the payment merkle proof (user activity within epoch) and the seal
2261/// merkle proof (payment root authenticity from oracle), with the payment root embedded
2262/// in the seal proof data.
2263///
2264/// ## Parameters
2265/// - `payment_proof`: Merkle proof for user's activity within the epoch
2266/// - `seal_proof`: Merkle proof that payment root is in the seal tree (contains payment root)
2267/// - `on_chain_seal_root`: Current seal merkle root stored on-chain
2268/// - `participant_data`: User's participant data to verify
2269///
2270/// ## Returns
2271/// - `Ok(())` if both proofs are valid
2272/// - `Err(InvalidPaymentProof)` if payment proof is invalid
2273/// - `Err(InvalidSealProof)` if seal proof is invalid
2274///
2275/// ## Security Features
2276/// - **Payment Proof**: Ensures claim data integrity within an epoch
2277/// - **Seal Proof**: Ensures payment merkle root is authentic (from oracle)
2278/// - **Payment Root**: Embedded in seal proof for explicit verification
2279/// - **Dual Verification**: Both proofs must pass for a valid claim
2280///
2281/// ## Example
2282/// ```rust
2283/// use miracle_api::{sdk, prelude::{DailyParticipantData, MerkleProof, SealProof}};
2284///
2285/// let participant = DailyParticipantData::new([1u8; 32], 1723680000, 1723766400, [1u8; 8], [2u8; 8], 5, 0);
2286/// let payment_proof = MerkleProof::new(vec![[2u8; 32]], vec![true]);
2287/// let seal_proof = SealProof::new_for_verification(vec![[4u8; 32]], vec![false], [3u8; 32]);
2288/// let on_chain_seal_root = [5u8; 32];
2289///
2290/// let result = sdk::verify_dual_merkle_claim(
2291/// &payment_proof,
2292/// &seal_proof,
2293/// on_chain_seal_root,
2294/// &participant,
2295/// );
2296///
2297/// match result {
2298/// Ok(()) => println!("Dual merkle verification successful"),
2299/// Err(e) => println!("Verification failed: {:?}", e),
2300/// }
2301/// ```
2302pub fn verify_dual_merkle_claim(
2303 payment_proof: &MerkleProof,
2304 seal_proof: &SealProof,
2305 on_chain_seal_root: [u8; 32],
2306 participant_data: &DailyParticipantData,
2307) -> Result<(), crate::error::MiracleError> {
2308 // Rule 1: Verify seal proof - payment root must be in seal merkle tree
2309 if !verify_seal_proof(
2310 &seal_proof.path,
2311 &seal_proof.indices,
2312 seal_proof.payment_root,
2313 on_chain_seal_root,
2314 ) {
2315 return Err(crate::error::MiracleError::InvalidSealProof);
2316 }
2317
2318 // Rule 2: Verify payment proof against payment root
2319 if !payment_proof.verify(participant_data, seal_proof.payment_root) {
2320 return Err(crate::error::MiracleError::InvalidPaymentProof);
2321 }
2322
2323 Ok(())
2324}
2325
2326impl SealProof {
2327 /// Create a new seal proof with path, indices, payment root, and epoch-specific data.
2328 ///
2329 /// This constructor is required for:
2330 /// - Historical reward calculations
2331 /// - Production claim verification
2332 /// - Accurate epoch-specific data
2333 ///
2334 /// Use this for all production claims that require historical accuracy.
2335 ///
2336 /// ## Parameters
2337 /// - `path`: Seal merkle path nodes (sibling hashes)
2338 /// - `indices`: Seal direction indices (false = left, true = right)
2339 /// - `payment_root`: Payment root being proven (embedded in seal proof)
2340 /// - `customer_reward_pool`: Epoch-specific customer reward pool
2341 /// - `merchant_reward_pool`: Epoch-specific merchant reward pool
2342 /// - `total_customer_activity`: Epoch-specific total customer activity
2343 /// - `total_merchant_activity`: Epoch-specific total merchant activity
2344 /// - `community_score`: Epoch-specific community score
2345 /// - `customer_reward_share`: Epoch-specific customer reward share
2346 /// - `merchant_reward_share`: Epoch-specific merchant reward share
2347 /// - `weekly_active_users`: Epoch-specific weekly active users
2348 /// - `weekly_retention_rate`: Epoch-specific weekly retention rate
2349 /// - `weekly_activity_count`: Epoch-specific weekly activity count
2350 pub fn new(
2351 path: Vec<[u8; 32]>,
2352 indices: Vec<bool>,
2353 payment_root: [u8; 32],
2354 customer_reward_pool: u64,
2355 merchant_reward_pool: u64,
2356 total_customer_activity: u64,
2357 total_merchant_activity: u64,
2358 community_score: u16,
2359 customer_reward_share: u16,
2360 merchant_reward_share: u16,
2361 weekly_active_users: u32,
2362 weekly_retention_rate: u16,
2363 weekly_activity_count: u32,
2364 ) -> Self {
2365 Self {
2366 path,
2367 indices,
2368 payment_root,
2369 customer_reward_pool,
2370 merchant_reward_pool,
2371 total_customer_activity,
2372 total_merchant_activity,
2373 community_score,
2374 customer_reward_share,
2375 merchant_reward_share,
2376 weekly_active_users,
2377 weekly_retention_rate,
2378 weekly_activity_count,
2379 }
2380 }
2381
2382 /// Create a new seal proof for verification purposes only.
2383 ///
2384 /// This constructor is suitable for:
2385 /// - Proof verification (merkle path validation)
2386 /// - Testing and development
2387 /// - Backward compatibility with existing proofs
2388 /// - Client-side utility functions
2389 ///
2390 /// ⚠️ WARNING: This proof will have zero epoch data.
2391 /// For historical reward calculations, use oracle-generated proofs instead.
2392 ///
2393 /// ## Parameters
2394 /// - `path`: Seal merkle path nodes (sibling hashes)
2395 /// - `indices`: Seal direction indices (false = left, true = right)
2396 /// - `payment_root`: Payment root being proven (embedded in seal proof)
2397 ///
2398 /// ## Usage
2399 /// ```rust
2400 /// use miracle_api::prelude::SealProof;
2401 /// let path = vec![[1u8; 32], [2u8; 32]];
2402 /// let indices = vec![true, false];
2403 /// let payment_root = [3u8; 32];
2404 /// let proof = SealProof::new_for_verification(path, indices, payment_root);
2405 /// // Use for proof verification only
2406 /// ```
2407 pub fn new_for_verification(
2408 path: Vec<[u8; 32]>,
2409 indices: Vec<bool>,
2410 payment_root: [u8; 32],
2411 ) -> Self {
2412 Self {
2413 path,
2414 indices,
2415 payment_root,
2416 customer_reward_pool: 0,
2417 merchant_reward_pool: 0,
2418 total_customer_activity: 0,
2419 total_merchant_activity: 0,
2420 community_score: 0,
2421 customer_reward_share: 0,
2422 merchant_reward_share: 0,
2423 weekly_active_users: 0,
2424 weekly_retention_rate: 0,
2425 weekly_activity_count: 0,
2426 }
2427 }
2428}
2429
2430/// Verify a seal merkle proof.
2431///
2432/// This function verifies that a payment merkle root is included in the seal merkle tree.
2433/// The seal proof demonstrates that the payment root is authentic and was provided by the oracle.
2434///
2435/// ## Parameters
2436/// - `seal_path`: Seal merkle path nodes (Vec<[u8; 32]>)
2437/// - `seal_indices`: Seal direction indices (Vec<bool>)
2438/// - `payment_root`: Payment merkle root to verify
2439/// - `seal_root`: Root of the seal merkle tree
2440///
2441/// ## Returns
2442/// - `true` if seal proof is valid
2443/// - `false` if seal proof is invalid
2444///
2445/// ## Security
2446/// - Verifies payment root is authentic (from oracle)
2447/// - Prevents forged payment roots
2448/// - Ensures payment root is in the historical seal tree
2449fn verify_seal_proof(
2450 seal_path: &Vec<[u8; 32]>,
2451 seal_indices: &Vec<bool>,
2452 payment_root: [u8; 32],
2453 seal_root: [u8; 32],
2454) -> bool {
2455 // REJECT empty seal proof paths - they can't be verified
2456 if seal_path.is_empty() {
2457 return false;
2458 }
2459
2460 if seal_path.len() != seal_indices.len() {
2461 return false;
2462 }
2463
2464 let mut current_hash = payment_root;
2465
2466 // Traverse the seal proof path
2467 for (i, &path_node) in seal_path.iter().enumerate() {
2468 let is_right = seal_indices[i];
2469
2470 let mut combined_data = Vec::new();
2471 if is_right {
2472 // When is_right is true, the sibling is on the right
2473 // So current_hash should be on the left, path_node on the right
2474 combined_data.extend_from_slice(¤t_hash);
2475 combined_data.extend_from_slice(&path_node);
2476 } else {
2477 // When is_right is false, the sibling is on the left
2478 // So path_node should be on the left, current_hash on the right
2479 combined_data.extend_from_slice(&path_node);
2480 combined_data.extend_from_slice(¤t_hash);
2481 }
2482
2483 current_hash = hashv(&[&combined_data]).to_bytes();
2484 }
2485
2486 current_hash == seal_root
2487}
2488
2489/// Seal Merkle Tree for incremental construction of historical payment roots.
2490///
2491/// This tree grows incrementally, adding each new payment merkle root as a leaf.
2492/// It enables unlimited claim history while maintaining perfect security.
2493///
2494/// ## Growth Pattern
2495///
2496/// The seal tree grows incrementally:
2497/// - Epoch 0: [Payment Root 0]
2498/// - Epoch 1: [Payment Root 0] + [Payment Root 1] -> Seal Root 1
2499/// - Epoch 2: [Payment Root 0] + [Payment Root 1] + [Payment Root 2] -> Seal Root 2
2500/// - ...
2501/// - Epoch N: [Payment Root 0] + ... + [Payment Root N] -> Seal Root N
2502///
2503/// ## Security
2504/// - Each payment root is embedded in the seal tree
2505/// - Seal proofs demonstrate payment root authenticity
2506/// - Dual verification prevents all attack vectors
2507///
2508/// ## 🚨 CRITICAL: Chronological Ordering Requirement
2509///
2510/// **PAYMENT ROOTS MUST BE PROVIDED IN CHRONOLOGICAL ORDER (EPOCH 0, 1, 2, 3...)**
2511///
2512/// This is essential for:
2513/// - Deterministic seal merkle root generation
2514/// - Consistent verification between Oracle and on-chain program
2515/// - Preventing `0x10` (InvalidPaymentProof) errors
2516///
2517/// The Oracle team MUST ensure:
2518/// 1. Database queries return epochs in chronological order
2519/// 2. Payment roots are collected in epoch sequence
2520/// 3. `SealMerkleTree::from_payment_roots()` receives ordered data
2521///
2522/// Failure to maintain chronological ordering will result in:
2523/// - Different seal merkle roots between Oracle and on-chain
2524/// - Proof verification failures
2525/// - Inability to process claims
2526#[repr(C)]
2527#[derive(Clone, Debug, Serialize, Deserialize)]
2528pub struct SealMerkleTree {
2529 /// Current seal merkle root (32 bytes)
2530 pub root: [u8; 32],
2531 /// Internal tree structure for proof generation
2532 tree: Vec<Vec<[u8; 32]>>,
2533 /// Historical payment roots (leaves of the seal tree)
2534 payment_roots: Vec<[u8; 32]>,
2535}
2536
2537impl SealMerkleTree {
2538 /// Create a new empty seal merkle tree.
2539 ///
2540 /// ## Returns
2541 /// - Empty seal merkle tree with zero root
2542 pub fn new() -> Self {
2543 Self {
2544 root: [0u8; 32],
2545 tree: vec![vec![]],
2546 payment_roots: vec![],
2547 }
2548 }
2549
2550 /// Create a seal merkle tree from existing payment roots.
2551 ///
2552 /// ## Parameters
2553 /// - `payment_roots`: Vector of historical payment merkle roots
2554 ///
2555 /// ## Returns
2556 /// - Seal merkle tree with all payment roots included
2557 ///
2558 /// ## Important: Chronological Ordering
2559 /// The payment roots MUST be provided in chronological order (epoch 0, 1, 2, 3...)
2560 /// to ensure deterministic and verifiable seal merkle roots across all systems.
2561 /// This ordering rule is enforced to maintain consistency between Oracle and on-chain verification.
2562 pub fn from_payment_roots(payment_roots: Vec<[u8; 32]>) -> Self {
2563 if payment_roots.is_empty() {
2564 return Self::new();
2565 }
2566
2567 // CRITICAL: Enforce chronological ordering for deterministic seal merkle roots
2568 // The Oracle team must provide payment roots in epoch order (0, 1, 2, 3...)
2569 // This ensures the on-chain program can verify proofs against the same tree structure
2570 if payment_roots.len() > 1 {
2571 // Note: We cannot directly sort by epoch number from payment root hashes
2572 // The Oracle team MUST provide payment roots in chronological order (0, 1, 2, 3...)
2573 // This is a critical architectural requirement for deterministic tree construction
2574
2575 // Log warning about chronological ordering requirement
2576 // This helps debug ordering issues during development
2577 println!(
2578 "⚠️ WARNING: SealMerkleTree::from_payment_roots called with {} payment roots",
2579 payment_roots.len()
2580 );
2581 println!(" CRITICAL: Oracle team MUST provide payment roots in chronological order (epoch 0, 1, 2, 3...)");
2582 println!(" This ensures deterministic seal merkle roots for on-chain verification");
2583 println!(" Current implementation assumes correct ordering from Oracle team");
2584 }
2585
2586 // Build merkle tree from payment roots
2587 let mut tree = Vec::new();
2588 tree.push(payment_roots.clone());
2589
2590 while tree.last().unwrap().len() > 1 {
2591 let current_level = tree.last().unwrap();
2592 let mut next_level = Vec::new();
2593
2594 for chunk in current_level.chunks(2) {
2595 if chunk.len() == 2 {
2596 // Correct hash ordering: left child first, then right child
2597 let combined = hashv(&[&chunk[0], &chunk[1]]).to_bytes();
2598 next_level.push(combined);
2599 } else {
2600 // Single element case
2601 let combined = hashv(&[&chunk[0], &chunk[0]]).to_bytes();
2602 next_level.push(combined);
2603 }
2604 }
2605
2606 tree.push(next_level);
2607 }
2608
2609 let root = tree.last().unwrap()[0];
2610
2611 Self {
2612 root,
2613 tree,
2614 payment_roots,
2615 }
2616 }
2617
2618 /// Add a new payment root to the seal merkle tree.
2619 ///
2620 /// This function incrementally grows the seal tree by adding the new payment root
2621 /// and rebuilding the tree structure.
2622 ///
2623 /// ## Parameters
2624 /// - `payment_root`: New payment merkle root to add
2625 ///
2626 /// ## Returns
2627 /// - Updated seal merkle tree with new payment root included
2628 ///
2629 /// ## Important: Chronological Ordering
2630 /// Payment roots MUST be added in chronological order (epoch 0, 1, 2, 3...)
2631 /// to maintain deterministic tree structure. This method assumes the new payment root
2632 /// is for the next sequential epoch.
2633 pub fn add_payment_root(&mut self, payment_root: [u8; 32]) {
2634 // CRITICAL: Ensure payment roots are added in chronological order
2635 // This method assumes the new payment root is for the next sequential epoch
2636 // The Oracle team must call this method in epoch order to maintain consistency
2637 println!(
2638 "🔒 Adding payment root to seal merkle tree (epoch {})",
2639 self.payment_roots.len()
2640 );
2641 println!(" CRITICAL: Payment roots must be added in chronological order for deterministic verification");
2642
2643 self.payment_roots.push(payment_root);
2644
2645 // Rebuild the entire tree with the new payment root
2646 let mut tree = Vec::new();
2647 tree.push(self.payment_roots.clone());
2648
2649 while tree.last().unwrap().len() > 1 {
2650 let current_level = tree.last().unwrap();
2651 let mut next_level = Vec::new();
2652
2653 for chunk in current_level.chunks(2) {
2654 if chunk.len() == 2 {
2655 // Correct hash ordering: left child first, then right child
2656 let combined = hashv(&[&chunk[0], &chunk[1]]).to_bytes();
2657 next_level.push(combined);
2658 } else {
2659 // Single element case
2660 let combined = hashv(&[&chunk[0], &chunk[0]]).to_bytes();
2661 next_level.push(combined);
2662 }
2663 }
2664
2665 tree.push(next_level);
2666 }
2667
2668 self.tree = tree;
2669 self.root = self.tree.last().unwrap()[0];
2670 }
2671
2672 /// Get the current seal merkle root.
2673 ///
2674 /// ## Returns
2675 /// - Current seal merkle root (32 bytes)
2676 pub fn root(&self) -> [u8; 32] {
2677 self.root
2678 }
2679
2680 /// Get the number of payment roots in the seal tree.
2681 ///
2682 /// ## Returns
2683 /// - Number of historical payment roots
2684 pub fn payment_root_count(&self) -> usize {
2685 self.payment_roots.len()
2686 }
2687
2688 /// Get all payment roots in the seal tree.
2689 ///
2690 /// ## Returns
2691 /// - Vector of all historical payment roots
2692 pub fn payment_roots(&self) -> &[[u8; 32]] {
2693 &self.payment_roots
2694 }
2695
2696 /// Validate that payment roots are in chronological order.
2697 ///
2698 /// This method helps ensure the Oracle team is providing payment roots
2699 /// in the correct epoch sequence for deterministic tree construction.
2700 ///
2701 /// ## Returns
2702 /// - `true` if payment roots appear to be in chronological order
2703 /// - `false` if there are potential ordering issues
2704 ///
2705 /// ## Note
2706 /// This is a best-effort validation since we can't directly determine
2707 /// epoch numbers from payment root hashes. The Oracle team must ensure
2708 /// chronological ordering when calling this method.
2709 pub fn validate_chronological_order(&self) -> bool {
2710 if self.payment_roots.len() <= 1 {
2711 return true; // Single or no payment roots are always "ordered"
2712 }
2713
2714 // Log the current payment root count for debugging
2715 println!(
2716 "🔍 Validating chronological order of {} payment roots",
2717 self.payment_roots.len()
2718 );
2719 println!(" CRITICAL: Oracle team must ensure payment roots are provided in epoch order (0, 1, 2, 3...)");
2720
2721 // Since we can't directly validate epoch numbers from hashes,
2722 // we rely on the Oracle team's commitment to chronological ordering
2723 // This method serves as a reminder and debugging aid
2724 true
2725 }
2726
2727 /// Generate a seal proof for a specific payment root.
2728 ///
2729 /// This function creates a proof that demonstrates a payment root is included
2730 /// in the current seal merkle tree.
2731 ///
2732 /// ## Parameters
2733 /// - `payment_root`: Payment root to generate proof for
2734 ///
2735 /// ## Returns
2736 /// - `Some(SealProof)` if payment root is found
2737 /// - `None` if payment root is not in the tree
2738 pub fn generate_seal_proof(&self, payment_root: [u8; 32]) -> Option<SealProof> {
2739 // Find the index of the payment root
2740 let payment_root_index = self
2741 .payment_roots
2742 .iter()
2743 .position(|&root| root == payment_root)?;
2744
2745 let mut path = Vec::new();
2746 let mut indices = Vec::new();
2747
2748 let mut current_index = payment_root_index;
2749 for level in &self.tree[..self.tree.len() - 1] {
2750 let sibling_index = if current_index % 2 == 0 {
2751 current_index + 1
2752 } else {
2753 current_index - 1
2754 };
2755
2756 if sibling_index < level.len() {
2757 path.push(level[sibling_index]);
2758 indices.push(current_index % 2 == 1); // true if right sibling
2759 }
2760
2761 current_index /= 2;
2762 }
2763
2764 Some(SealProof::new_for_verification(path, indices, payment_root))
2765 }
2766
2767 /// Find and generate a seal proof for a payment root by epoch.
2768 ///
2769 /// This is a convenience function that finds a payment root by its epoch index
2770 /// and generates a seal proof for it.
2771 ///
2772 /// ## Parameters
2773 /// - `epoch_index`: Index of the epoch (0-based)
2774 ///
2775 /// ## Returns
2776 /// - `Some(SealProof)` if epoch exists
2777 /// - `None` if epoch index is out of bounds
2778 pub fn generate_seal_proof_for_epoch(&self, epoch_index: usize) -> Option<SealProof> {
2779 let payment_root = self.payment_roots.get(epoch_index)?;
2780 self.generate_seal_proof(*payment_root)
2781 }
2782
2783 /// Verify that a payment root is included in the seal tree.
2784 ///
2785 /// ## Parameters
2786 /// - `payment_root`: Payment root to verify
2787 ///
2788 /// ## Returns
2789 /// - `true` if payment root is in the seal tree
2790 /// - `false` if payment root is not in the seal tree
2791 pub fn contains_payment_root(&self, payment_root: [u8; 32]) -> bool {
2792 self.payment_roots.contains(&payment_root)
2793 }
2794}
2795
2796/// Create a new seal merkle tree.
2797///
2798/// This is a convenience function that creates an empty seal merkle tree.
2799/// Use `SealMerkleTree::from_payment_roots()` to create a tree with existing payment roots.
2800///
2801/// ## Returns
2802/// - Empty seal merkle tree
2803///
2804/// ## Example
2805/// ```rust
2806/// use miracle_api::sdk::create_seal_merkle_tree;
2807///
2808/// let mut seal_tree = create_seal_merkle_tree();
2809/// seal_tree.add_payment_root([1u8; 32]);
2810/// println!("Seal root: {:?}", seal_tree.root());
2811/// ```
2812pub fn create_seal_merkle_tree() -> SealMerkleTree {
2813 SealMerkleTree::new()
2814}
2815
2816/// Create a seal merkle tree from payment roots.
2817///
2818/// This is a convenience function that creates a seal merkle tree from existing payment roots.
2819///
2820/// ## Parameters
2821/// - `payment_roots`: Vector of historical payment merkle roots
2822///
2823/// ## Returns
2824/// - Seal merkle tree with all payment roots included
2825///
2826/// ## 🚨 CRITICAL: Chronological Ordering Requirement
2827///
2828/// **PAYMENT ROOTS MUST BE PROVIDED IN CHRONOLOGICAL ORDER (EPOCH 0, 1, 2, 3...)**
2829///
2830/// This is essential for deterministic seal merkle root generation and consistent
2831/// verification between Oracle and on-chain program. Failure to maintain chronological
2832/// ordering will result in proof verification failures.
2833///
2834/// ## Example
2835/// ```rust
2836/// use miracle_api::sdk::create_seal_merkle_tree_from_roots;
2837///
2838/// let payment_roots = vec![[1u8; 32], [2u8; 32], [3u8; 32]];
2839/// let seal_tree = create_seal_merkle_tree_from_roots(payment_roots);
2840/// println!("Seal root: {:?}", seal_tree.root());
2841/// ```
2842pub fn create_seal_merkle_tree_from_roots(payment_roots: Vec<[u8; 32]>) -> SealMerkleTree {
2843 SealMerkleTree::from_payment_roots(payment_roots)
2844}
2845
2846/// Generate a seal proof for a payment root.
2847///
2848/// This is a convenience function that generates a seal proof for a payment root
2849/// from a seal merkle tree.
2850///
2851/// ## Parameters
2852/// - `seal_tree`: Seal merkle tree
2853/// - `payment_root`: Payment root to generate proof for
2854///
2855/// ## Returns
2856/// - `Some(SealProof)` if payment root is found
2857/// - `None` if payment root is not in the tree
2858///
2859/// ## Example
2860/// ```rust
2861/// use miracle_api::sdk::{create_seal_merkle_tree_from_roots, generate_seal_proof};
2862///
2863/// let payment_roots = vec![[1u8; 32], [2u8; 32]];
2864/// let seal_tree = create_seal_merkle_tree_from_roots(payment_roots);
2865/// let seal_proof = generate_seal_proof(&seal_tree, [1u8; 32]);
2866/// assert!(seal_proof.is_some());
2867/// ```
2868pub fn generate_seal_proof(
2869 seal_tree: &SealMerkleTree,
2870 payment_root: [u8; 32],
2871) -> Option<SealProof> {
2872 seal_tree.generate_seal_proof(payment_root)
2873}
2874
2875/// Computes the epoch hash for the Epoch Hash Chain security solution.
2876///
2877/// This function implements the same logic as the on-chain program:
2878/// - Epoch 0: Uses the predefined genesis hash constant
2879/// - Subsequent epochs: Hash(previous_epoch_hash + epoch_number + current_payment_merkle_root)
2880///
2881/// ## Parameters
2882/// - `epoch`: The epoch number
2883/// - `previous_epoch_hash`: The hash from the previous epoch (use genesis hash for epoch 0)
2884/// - `payment_merkle_root`: The current epoch's payment merkle root
2885///
2886/// ## Returns
2887/// The computed epoch hash for tamper detection
2888///
2889/// ## Security
2890/// This function must be used consistently between off-chain oracle processing
2891/// and on-chain verification to maintain the hash chain integrity.
2892/// The epoch number inclusion ensures uniqueness even with identical payment roots.
2893///
2894/// ## Implementation Details
2895/// - **Epoch 0**: Uses the predefined genesis hash constant (same as on-chain program)
2896/// - **Subsequent epochs**: Build upon the previous epoch hash + epoch number + payment root
2897/// - **No fallback logic**: Since program now initializes with genesis hash, fallback is not needed
2898/// - **Uniqueness guarantee**: Epoch number ensures uniqueness even with identical payment roots
2899pub fn compute_epoch_hash(
2900 epoch: u64,
2901 previous_epoch_hash: [u8; 32],
2902 payment_merkle_root: [u8; 32],
2903) -> [u8; 32] {
2904 if epoch == 0 {
2905 // For epoch 0, use the predefined genesis hash constant
2906 // This maintains security while allowing epoch 0 to be properly initialized
2907 crate::consts::miracle_epoch_0_genesis()
2908 } else {
2909 // For subsequent epochs: Previous hash + EPOCH_NUMBER + current payment merkle root
2910 // This creates an unbreakable chain: each epoch depends on all previous epochs
2911 // The epoch number ensures uniqueness even when payment roots are identical (e.g., empty epochs)
2912
2913 // Handle fallback for backward compatibility (external callers might still pass [0; 32])
2914 let previous_epoch_hash = if previous_epoch_hash == [0u8; 32] {
2915 // If previous epoch hash is zero, use genesis as fallback (same as program)
2916 // This handles cases where the previous epoch hash hasn't been set yet
2917 crate::consts::miracle_epoch_0_genesis()
2918 } else {
2919 previous_epoch_hash
2920 };
2921
2922 // Include epoch number to ensure uniqueness even with empty payment roots
2923 let epoch_bytes = epoch.to_le_bytes();
2924 hashv(&[&previous_epoch_hash, &epoch_bytes, &payment_merkle_root]).to_bytes()
2925 }
2926}