Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions contracts/inheritance-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,15 @@ pub struct MessageFinalizedEvent {
pub timestamp: u64,
}

/// Event emitted when a legacy message is deleted
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MessageDeletedEvent {
pub vault_id: u64,
pub message_id: u64,
pub timestamp: u64,
}

/// Event emitted when a message is unlocked
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -3095,6 +3104,71 @@ impl InheritanceContract {
.unwrap_or_else(|| vec![&env])
}

/// Delete a legacy message before it has been finalized.
///
/// # Arguments
/// * `env` - The Soroban environment
/// * `owner` - The vault owner requesting deletion
/// * `message_id` - The message to delete
///
/// # Errors
/// - `PlanNotFound` if the message does not exist
/// - `Unauthorized` if caller is not the message creator
/// - `WillAlreadyFinalized` if the message has been finalized
pub fn delete_legacy_message(
env: Env,
owner: Address,
message_id: u64,
) -> Result<(), InheritanceError> {
owner.require_auth();

let message: LegacyMessageMetadata = env
.storage()
.persistent()
.get(&DataKey::LegacyMessage(message_id))
.ok_or(InheritanceError::PlanNotFound)?;

if message.creator != owner {
return Err(InheritanceError::Unauthorized);
}

if message.is_finalized {
return Err(InheritanceError::WillAlreadyFinalized);
}

// Remove message metadata
env.storage()
.persistent()
.remove(&DataKey::LegacyMessage(message_id));

// Remove from vault's message list
let vault_messages: Vec<u64> = env
.storage()
.persistent()
.get(&DataKey::VaultMessages(message.vault_id))
.unwrap_or_else(|| vec![&env]);
let mut updated: Vec<u64> = vec![&env];
for id in vault_messages.iter() {
if id != message_id {
updated.push_back(id);
}
}
env.storage()
.persistent()
.set(&DataKey::VaultMessages(message.vault_id), &updated);

env.events().publish(
(Symbol::new(&env, "message_deleted"), message.vault_id),
MessageDeletedEvent {
vault_id: message.vault_id,
message_id,
timestamp: env.ledger().timestamp(),
},
);

Ok(())
}

/// Access a legacy message (returns metadata if accessible)
///
/// # Arguments
Expand Down Expand Up @@ -3478,5 +3552,6 @@ impl InheritanceContract {
}

#[cfg(test)]
#[allow(clippy::duplicated_attributes)]
mod message_test;
mod test;
85 changes: 85 additions & 0 deletions contracts/inheritance-contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4582,3 +4582,88 @@ fn test_claim_plan_with_approved_kyc_succeeds() {

// Verify claim was recorded (no error means success)
}

// --- Message Deletion Option ---

fn make_message(
env: &Env,
client: &InheritanceContractClient<'_>,
owner: &Address,
vault_id: u64,
) -> u64 {
client.create_legacy_message(
owner,
&CreateLegacyMessageParams {
vault_id,
message_hash: BytesN::from_array(env, &[1u8; 32]),
unlock_timestamp: env.ledger().timestamp() + 10_000,
key_reference: soroban_sdk::String::from_str(env, "ref_1"),
},
)
}

#[test]
fn test_delete_legacy_message_before_lock() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);
let message_id = make_message(&env, &client, &owner, plan_id);

client.delete_legacy_message(&owner, &message_id);

// Message is gone
assert!(client.get_legacy_message(&message_id).is_none());
// Removed from vault list
let vault_messages = client.get_vault_messages(&plan_id);
assert!(!vault_messages.contains(message_id));
}

#[test]
fn test_delete_legacy_message_removes_from_vault_list() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);

let id_a = make_message(&env, &client, &owner, plan_id);
let id_b = make_message(&env, &client, &owner, plan_id);

assert_eq!(client.get_vault_messages(&plan_id).len(), 2);

client.delete_legacy_message(&owner, &id_a);

let remaining = client.get_vault_messages(&plan_id);
assert_eq!(remaining.len(), 1);
assert_eq!(remaining.get(0).unwrap(), id_b);
}

#[test]
fn test_delete_legacy_message_fails_after_lock() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);
let message_id = make_message(&env, &client, &owner, plan_id);

client.finalize_legacy_message(&owner, &message_id);

let result = client.try_delete_legacy_message(&owner, &message_id);
assert_eq!(result, Err(Ok(InheritanceError::WillAlreadyFinalized)));
// Message still present
assert!(client.get_legacy_message(&message_id).is_some());
}

#[test]
fn test_delete_legacy_message_unauthorized() {
let env = Env::default();
env.mock_all_auths();
let (client, token_id, _admin, owner) = setup_with_token_and_admin(&env);
let plan_id = create_plan_and_get_id(&env, &client, &token_id, &owner);
let message_id = make_message(&env, &client, &owner, plan_id);

let stranger = Address::generate(&env);
let result = client.try_delete_legacy_message(&stranger, &message_id);
assert_eq!(result, Err(Ok(InheritanceError::Unauthorized)));
assert!(client.get_legacy_message(&message_id).is_some());
}
Original file line number Diff line number Diff line change
Expand Up @@ -1888,7 +1888,7 @@
"data": {
"vec": [
{
"string": "caught panic 'called `Result::unwrap()` on an `Err` value: ConversionError' from contract function 'Symbol(obj#407)'"
"string": "caught panic 'called `Result::unwrap()` on an `Err` value: ConversionError' from contract function 'Symbol(obj#417)'"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
Expand Down
Loading
Loading