|
1 | 1 | #![no_std] |
2 | 2 | use soroban_sdk::{ |
3 | 3 | contract, contracterror, contractimpl, contracttype, log, symbol_short, token, vec, Address, |
4 | | - Bytes, BytesN, Env, IntoVal, InvokeError, String, Symbol, Val, Vec, |
| 4 | + Bytes, BytesN, Env, FromVal, IntoVal, InvokeError, String, Symbol, Val, Vec, |
5 | 5 | }; |
6 | 6 |
|
7 | 7 | /// Current contract version - bump this on each upgrade |
@@ -142,6 +142,9 @@ pub enum DataKey { |
142 | 142 | ActiveWillVersion(u64), // plan_id -> u32 (active version number) |
143 | 143 | WillSignature(u64), // plan_id -> WillSignatureProof |
144 | 144 | SignatureUsed(BytesN<32>), // sig_hash -> bool (replay protection) |
| 145 | + NextMessageId, // Global next message ID counter |
| 146 | + LegacyMessage(u64), // message_id -> LegacyMessageMetadata |
| 147 | + VaultMessages(u64), // vault_id -> Vec<u64> (message IDs) |
145 | 148 | } |
146 | 149 |
|
147 | 150 | #[contracttype] |
@@ -337,6 +340,48 @@ pub struct EmergencyContactAddedEvent { |
337 | 340 | pub contact: Address, |
338 | 341 | } |
339 | 342 |
|
| 343 | +/// Legacy message metadata stored on-chain |
| 344 | +#[contracttype] |
| 345 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 346 | +pub struct LegacyMessageMetadata { |
| 347 | + pub vault_id: u64, // Associated vault/plan ID |
| 348 | + pub message_id: u64, // Unique message identifier |
| 349 | + pub message_hash: BytesN<32>, // Cryptographic hash of message content (off-chain) |
| 350 | + pub creator: Address, // Message creator (vault owner) |
| 351 | + pub unlock_timestamp: u64, // Timestamp when message becomes accessible |
| 352 | + pub is_unlocked: bool, // Whether message has been unlocked |
| 353 | + pub created_at: u64, // Message creation timestamp |
| 354 | +} |
| 355 | + |
| 356 | +/// Parameters for creating a legacy message |
| 357 | +#[contracttype] |
| 358 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 359 | +pub struct CreateLegacyMessageParams { |
| 360 | + pub vault_id: u64, |
| 361 | + pub message_hash: BytesN<32>, |
| 362 | + pub unlock_timestamp: u64, |
| 363 | +} |
| 364 | + |
| 365 | +/// Event emitted when a legacy message is created |
| 366 | +#[contracttype] |
| 367 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 368 | +pub struct MessageCreatedEvent { |
| 369 | + pub vault_id: u64, |
| 370 | + pub message_id: u64, |
| 371 | + pub creator: Address, |
| 372 | + pub unlock_timestamp: u64, |
| 373 | +} |
| 374 | + |
| 375 | +/// Event emitted when a message is unlocked |
| 376 | +#[contracttype] |
| 377 | +#[derive(Clone, Debug, Eq, PartialEq)] |
| 378 | +pub struct MessageUnlockedEvent { |
| 379 | + pub vault_id: u64, |
| 380 | + pub message_id: u64, |
| 381 | + pub unlocked_at: u64, |
| 382 | + pub unlock_reason: Symbol, // "timestamp" or "inheritance" |
| 383 | +} |
| 384 | + |
340 | 385 | #[contracttype] |
341 | 386 | #[derive(Clone, Debug, Eq, PartialEq)] |
342 | 387 | pub struct EmergencyContactRemovedEvent { |
@@ -2758,6 +2803,269 @@ impl InheritanceContract { |
2758 | 2803 | .persistent() |
2759 | 2804 | .get(&DataKey::WillSignature(vault_id)) |
2760 | 2805 | } |
| 2806 | + |
| 2807 | + /// Create a new legacy message with metadata stored on-chain |
| 2808 | + /// |
| 2809 | + /// # Arguments |
| 2810 | + /// * `env` - The Soroban environment |
| 2811 | + /// * `creator` - The address of the message creator (must be vault owner) |
| 2812 | + /// * `params` - Message creation parameters including hash and unlock timestamp |
| 2813 | + /// |
| 2814 | + /// # Requirements |
| 2815 | + /// - Creator must be the vault owner |
| 2816 | + /// - Unlock timestamp must be in the future |
| 2817 | + /// - Vault/plan must exist |
| 2818 | + pub fn create_legacy_message( |
| 2819 | + env: Env, |
| 2820 | + creator: Address, |
| 2821 | + params: CreateLegacyMessageParams, |
| 2822 | + ) -> Result<u64, InheritanceError> { |
| 2823 | + // Verify vault/plan exists and creator is the owner |
| 2824 | + let plan = Self::get_plan(&env, params.vault_id).ok_or(InheritanceError::PlanNotFound)?; |
| 2825 | + if plan.owner != creator { |
| 2826 | + return Err(InheritanceError::Unauthorized); |
| 2827 | + } |
| 2828 | + |
| 2829 | + // Validate unlock timestamp is in the future |
| 2830 | + let current_timestamp = env.ledger().timestamp(); |
| 2831 | + if params.unlock_timestamp <= current_timestamp { |
| 2832 | + return Err(InheritanceError::InvalidClaimCode); // Reuse for invalid timestamp |
| 2833 | + } |
| 2834 | + |
| 2835 | + // Generate unique message ID |
| 2836 | + let message_id = env |
| 2837 | + .storage() |
| 2838 | + .persistent() |
| 2839 | + .get(&DataKey::NextMessageId) |
| 2840 | + .unwrap_or(0u64); |
| 2841 | + |
| 2842 | + // Create message metadata |
| 2843 | + let message = LegacyMessageMetadata { |
| 2844 | + vault_id: params.vault_id, |
| 2845 | + message_id, |
| 2846 | + message_hash: params.message_hash, |
| 2847 | + creator: creator.clone(), |
| 2848 | + unlock_timestamp: params.unlock_timestamp, |
| 2849 | + is_unlocked: false, |
| 2850 | + created_at: current_timestamp, |
| 2851 | + }; |
| 2852 | + |
| 2853 | + // Store message metadata |
| 2854 | + env.storage() |
| 2855 | + .persistent() |
| 2856 | + .set(&DataKey::LegacyMessage(message_id), &message); |
| 2857 | + |
| 2858 | + // Add message to vault's message list |
| 2859 | + let mut vault_messages: Vec<u64> = env |
| 2860 | + .storage() |
| 2861 | + .persistent() |
| 2862 | + .get(&DataKey::VaultMessages(params.vault_id)) |
| 2863 | + .unwrap_or_else(|| vec![&env]); |
| 2864 | + vault_messages.push_back(message_id); |
| 2865 | + env.storage() |
| 2866 | + .persistent() |
| 2867 | + .set(&DataKey::VaultMessages(params.vault_id), &vault_messages); |
| 2868 | + |
| 2869 | + // Increment next message ID |
| 2870 | + env.storage() |
| 2871 | + .persistent() |
| 2872 | + .set(&DataKey::NextMessageId, &(message_id + 1)); |
| 2873 | + |
| 2874 | + // Emit event |
| 2875 | + env.events().publish( |
| 2876 | + (Symbol::new(&env, "message_created"),), |
| 2877 | + MessageCreatedEvent { |
| 2878 | + vault_id: params.vault_id, |
| 2879 | + message_id, |
| 2880 | + creator, |
| 2881 | + unlock_timestamp: params.unlock_timestamp, |
| 2882 | + }, |
| 2883 | + ); |
| 2884 | + |
| 2885 | + Ok(message_id) |
| 2886 | + } |
| 2887 | + |
| 2888 | + /// Get metadata for a specific legacy message |
| 2889 | + /// |
| 2890 | + /// # Arguments |
| 2891 | + /// * `env` - The Soroban environment |
| 2892 | + /// * `message_id` - The unique message identifier |
| 2893 | + pub fn get_legacy_message(env: Env, message_id: u64) -> Option<LegacyMessageMetadata> { |
| 2894 | + env.storage() |
| 2895 | + .persistent() |
| 2896 | + .get(&DataKey::LegacyMessage(message_id)) |
| 2897 | + } |
| 2898 | + |
| 2899 | + /// Get all message IDs for a specific vault |
| 2900 | + /// |
| 2901 | + /// # Arguments |
| 2902 | + /// * `env` - The Soroban environment |
| 2903 | + /// * `vault_id` - The vault/plan ID |
| 2904 | + pub fn get_vault_messages(env: Env, vault_id: u64) -> Vec<u64> { |
| 2905 | + env.storage() |
| 2906 | + .persistent() |
| 2907 | + .get(&DataKey::VaultMessages(vault_id)) |
| 2908 | + .unwrap_or_else(|| vec![&env]) |
| 2909 | + } |
| 2910 | + |
| 2911 | + /// Access a legacy message (returns metadata if accessible) |
| 2912 | + /// |
| 2913 | + /// # Arguments |
| 2914 | + /// * `env` - The Soroban environment |
| 2915 | + /// * `caller` - The address requesting access |
| 2916 | + /// * `message_id` - The message ID to access |
| 2917 | + /// |
| 2918 | + /// # Requirements |
| 2919 | + /// - Caller must be a verified beneficiary of the vault |
| 2920 | + /// - Message must be unlocked (either by timestamp or inheritance trigger) |
| 2921 | + pub fn access_legacy_message( |
| 2922 | + env: Env, |
| 2923 | + caller: Address, |
| 2924 | + message_id: u64, |
| 2925 | + ) -> Result<LegacyMessageMetadata, InheritanceError> { |
| 2926 | + // Get message metadata |
| 2927 | + let mut message: LegacyMessageMetadata = env |
| 2928 | + .storage() |
| 2929 | + .persistent() |
| 2930 | + .get(&DataKey::LegacyMessage(message_id)) |
| 2931 | + .ok_or(InheritanceError::PlanNotFound)?; // Reuse PlanNotFound for MessageNotFound |
| 2932 | + |
| 2933 | + // Check if already unlocked |
| 2934 | + if !message.is_unlocked { |
| 2935 | + let current_timestamp = env.ledger().timestamp(); |
| 2936 | + |
| 2937 | + // Check if unlock timestamp has been reached |
| 2938 | + if current_timestamp >= message.unlock_timestamp { |
| 2939 | + // Unlock by timestamp |
| 2940 | + message.is_unlocked = true; |
| 2941 | + env.storage() |
| 2942 | + .persistent() |
| 2943 | + .set(&DataKey::LegacyMessage(message_id), &message); |
| 2944 | + |
| 2945 | + // Emit unlock event |
| 2946 | + env.events().publish( |
| 2947 | + (Symbol::new(&env, "message_unlocked"),), |
| 2948 | + MessageUnlockedEvent { |
| 2949 | + vault_id: message.vault_id, |
| 2950 | + message_id, |
| 2951 | + unlocked_at: current_timestamp, |
| 2952 | + unlock_reason: symbol_short!("time"), |
| 2953 | + }, |
| 2954 | + ); |
| 2955 | + } else { |
| 2956 | + // Check if inheritance has been triggered |
| 2957 | + let inheritance_triggered: bool = env |
| 2958 | + .storage() |
| 2959 | + .persistent() |
| 2960 | + .get(&DataKey::InheritanceTrigger(message.vault_id)) |
| 2961 | + .map(|info: InheritanceTriggerInfo| info.triggered_at > 0) |
| 2962 | + .unwrap_or(false); |
| 2963 | + |
| 2964 | + if inheritance_triggered { |
| 2965 | + // Unlock by inheritance trigger |
| 2966 | + message.is_unlocked = true; |
| 2967 | + env.storage() |
| 2968 | + .persistent() |
| 2969 | + .set(&DataKey::LegacyMessage(message_id), &message); |
| 2970 | + |
| 2971 | + // Emit unlock event |
| 2972 | + env.events().publish( |
| 2973 | + (Symbol::new(&env, "message_unlocked"),), |
| 2974 | + MessageUnlockedEvent { |
| 2975 | + vault_id: message.vault_id, |
| 2976 | + message_id, |
| 2977 | + unlocked_at: current_timestamp, |
| 2978 | + unlock_reason: symbol_short!("inherit"), |
| 2979 | + }, |
| 2980 | + ); |
| 2981 | + } else { |
| 2982 | + // Message still locked |
| 2983 | + return Err(InheritanceError::ClaimNotAllowedYet); // Reuse for locked message |
| 2984 | + } |
| 2985 | + } |
| 2986 | + } |
| 2987 | + |
| 2988 | + // Verify caller is a beneficiary of this vault |
| 2989 | + let plan = Self::get_plan(&env, message.vault_id).ok_or(InheritanceError::PlanNotFound)?; |
| 2990 | + |
| 2991 | + // Hash the caller's address to check against beneficiaries |
| 2992 | + let caller_bytes = Bytes::from_val(&env, &caller.to_val()); |
| 2993 | + let caller_hash: BytesN<32> = env.crypto().sha256(&caller_bytes).into(); |
| 2994 | + let mut is_beneficiary = false; |
| 2995 | + |
| 2996 | + for i in 0..plan.beneficiaries.len() { |
| 2997 | + let beneficiary = plan |
| 2998 | + .beneficiaries |
| 2999 | + .get(i) |
| 3000 | + .ok_or(InheritanceError::BeneficiaryNotFound)?; |
| 3001 | + // Check if caller matches any beneficiary hashed email |
| 3002 | + if beneficiary.hashed_email == caller_hash { |
| 3003 | + is_beneficiary = true; |
| 3004 | + break; |
| 3005 | + } |
| 3006 | + } |
| 3007 | + |
| 3008 | + if !is_beneficiary { |
| 3009 | + return Err(InheritanceError::Unauthorized); // Reuse for not beneficiary |
| 3010 | + } |
| 3011 | + |
| 3012 | + Ok(message) |
| 3013 | + } |
| 3014 | + |
| 3015 | + /// Manually unlock a message when inheritance is triggered |
| 3016 | + /// This can be called during the inheritance trigger process |
| 3017 | + /// |
| 3018 | + /// # Arguments |
| 3019 | + /// * `env` - The Soroban environment |
| 3020 | + /// * `vault_id` - The vault/plan ID for which inheritance was triggered |
| 3021 | + pub fn unlock_messages_on_inheritance(env: Env, vault_id: u64) -> Result<(), InheritanceError> { |
| 3022 | + // Verify inheritance was triggered |
| 3023 | + let trigger_info: InheritanceTriggerInfo = env |
| 3024 | + .storage() |
| 3025 | + .persistent() |
| 3026 | + .get(&DataKey::InheritanceTrigger(vault_id)) |
| 3027 | + .ok_or(InheritanceError::InheritanceNotTriggered)?; |
| 3028 | + |
| 3029 | + if trigger_info.triggered_at == 0 { |
| 3030 | + return Err(InheritanceError::InheritanceNotTriggered); |
| 3031 | + } |
| 3032 | + |
| 3033 | + // Get all messages for this vault |
| 3034 | + let messages = Self::get_vault_messages(env.clone(), vault_id); |
| 3035 | + let current_timestamp = env.ledger().timestamp(); |
| 3036 | + |
| 3037 | + // Unlock each message |
| 3038 | + for message_id in messages.iter() { |
| 3039 | + let mut message: LegacyMessageMetadata = match env |
| 3040 | + .storage() |
| 3041 | + .persistent() |
| 3042 | + .get(&DataKey::LegacyMessage(message_id)) |
| 3043 | + { |
| 3044 | + Some(m) => m, |
| 3045 | + None => continue, // Skip if message doesn't exist |
| 3046 | + }; |
| 3047 | + |
| 3048 | + if !message.is_unlocked { |
| 3049 | + message.is_unlocked = true; |
| 3050 | + env.storage() |
| 3051 | + .persistent() |
| 3052 | + .set(&DataKey::LegacyMessage(message_id), &message); |
| 3053 | + |
| 3054 | + // Emit unlock event |
| 3055 | + env.events().publish( |
| 3056 | + (Symbol::new(&env, "message_unlocked"),), |
| 3057 | + MessageUnlockedEvent { |
| 3058 | + vault_id, |
| 3059 | + message_id, |
| 3060 | + unlocked_at: current_timestamp, |
| 3061 | + unlock_reason: symbol_short!("inherit"), |
| 3062 | + }, |
| 3063 | + ); |
| 3064 | + } |
| 3065 | + } |
| 3066 | + |
| 3067 | + Ok(()) |
| 3068 | + } |
2761 | 3069 | } |
2762 | 3070 |
|
2763 | 3071 | mod test; |
0 commit comments