diff --git a/contracts/stellar-id/src/lib.rs b/contracts/stellar-id/src/lib.rs index c1d8015..d92e028 100644 --- a/contracts/stellar-id/src/lib.rs +++ b/contracts/stellar-id/src/lib.rs @@ -115,6 +115,19 @@ pub struct MultiSigCredentialRequest { pub expires_at: u64, } +/// A pending credential transfer request. +/// +/// Allows a subject to propose transferring a credential to a new address. +/// The original issuer must approve the transfer before it executes. +#[contracttype] +#[derive(Clone, Debug)] +pub struct TransferRequest { + pub credential_id: u64, + pub from: Address, + pub to: Address, + pub requested_at: u64, +} + /// Storage keys #[contracttype] pub enum DataKey { @@ -983,6 +996,193 @@ impl StellarIdContract { ); } + /// Proposes a credential transfer from the subject's current address to a new address. + /// + /// The `subject` address must authorize the call and must be the current owner + /// of `credential_id`. A transfer request is stored, pending approval by the + /// original issuer. + /// + /// Panics if `subject` does not authorize the call, `credential_id` does not + /// exist, `subject` is not the credential owner, or a transfer request already + /// exists for this credential. + pub fn propose_transfer(env: Env, subject: Address, credential_id: u64, new_address: Address) { + subject.require_auth(); + + let credential: Credential = env + .storage() + .persistent() + .get(&DataKey::Credential(credential_id)) + .expect("Credential not found"); + + assert!( + credential.subject == subject, + "Only the credential subject can propose transfer" + ); + + // Check no existing transfer request + let existing: Option = env + .storage() + .persistent() + .get(&DataKey::TransferRequest(credential_id)); + assert!(existing.is_none(), "Transfer request already exists"); + + let now = env.ledger().timestamp(); + let request = TransferRequest { + credential_id, + from: subject.clone(), + to: new_address.clone(), + requested_at: now, + }; + + env.storage() + .persistent() + .set(&DataKey::TransferRequest(credential_id), &request); + + env.events().publish( + (Symbol::new(&env, "credential_transfer_proposed"),), + (credential_id, subject, new_address), + ); + } + + /// Approves and executes a credential transfer proposed by the subject. + /// + /// The `issuer` address must authorize the call and must be the original issuer + /// of `credential_id`. The credential's subject field is updated to the new + /// address, and both the old and new subject's credential lists and identity + /// records are updated accordingly. + /// + /// Panics if `issuer` does not authorize the call, `credential_id` does not + /// exist, `issuer` is not the original issuer, no transfer request exists, or + /// the transfer request has been tampered with. + pub fn approve_transfer(env: Env, issuer: Address, credential_id: u64) { + issuer.require_auth(); + + let mut credential: Credential = env + .storage() + .persistent() + .get(&DataKey::Credential(credential_id)) + .expect("Credential not found"); + + assert!( + credential.issuer == issuer, + "Only the original issuer can approve transfer" + ); + + let request: TransferRequest = env + .storage() + .persistent() + .get(&DataKey::TransferRequest(credential_id)) + .expect("Transfer request not found"); + + // Verify the request matches the current credential state + assert!( + request.credential_id == credential_id, + "Transfer request credential ID mismatch" + ); + assert!( + request.from == credential.subject, + "Transfer request from address mismatch" + ); + + let old_subject = credential.subject.clone(); + let new_subject = request.to.clone(); + + // Update credential subject + credential.subject = new_subject.clone(); + env.storage() + .persistent() + .set(&DataKey::Credential(credential_id), &credential); + + // Remove credential ID from old subject's list + let mut old_subject_creds: Vec = env + .storage() + .persistent() + .get(&DataKey::SubjectCredentials(old_subject.clone())) + .expect("Old subject credentials not found"); + let old_index = old_subject_creds + .iter() + .position(|id| id == credential_id) + .expect("Credential ID not in old subject's list"); + old_subject_creds.remove(old_index as u32); + if old_subject_creds.is_empty() { + env.storage() + .persistent() + .remove(&DataKey::SubjectCredentials(old_subject.clone())); + } else { + env.storage() + .persistent() + .set(&DataKey::SubjectCredentials(old_subject.clone()), &old_subject_creds); + } + + // Add credential ID to new subject's list + let mut new_subject_creds: Vec = env + .storage() + .persistent() + .get(&DataKey::SubjectCredentials(new_subject.clone())) + .unwrap_or(Vec::new(&env)); + new_subject_creds.push_back(credential_id); + env.storage().persistent().set( + &DataKey::SubjectCredentials(new_subject.clone()), + &new_subject_creds, + ); + + // Update old subject's identity + let mut old_identity: Identity = env + .storage() + .persistent() + .get(&DataKey::Identity(old_subject.clone())) + .expect("Old subject identity not found"); + old_identity.credential_count -= 1; + let issuer_record: Issuer = env + .storage() + .persistent() + .get(&DataKey::Issuer(issuer.clone())) + .expect("Issuer not found"); + old_identity.reputation_score = + Self::compute_reputation(old_identity.credential_count, issuer_record.trust_level); + if old_identity.credential_count == 0 { + env.storage() + .persistent() + .remove(&DataKey::Identity(old_subject.clone())); + } else { + env.storage() + .persistent() + .set(&DataKey::Identity(old_subject.clone()), &old_identity); + } + + // Update new subject's identity + let existing_new_identity: Option = env + .storage() + .persistent() + .get(&DataKey::Identity(new_subject.clone())); + let new_identity = if let Some(mut id) = existing_new_identity { + id.credential_count += 1; + id.reputation_score = + Self::compute_reputation(id.credential_count, issuer_record.trust_level); + id + } else { + Identity { + subject: new_subject.clone(), + credential_count: 1, + reputation_score: issuer_record.trust_level / 10, + created_at: env.ledger().timestamp(), + } + }; + env.storage() + .persistent() + .set(&DataKey::Identity(new_subject.clone()), &new_identity); + + // Remove the transfer request + env.storage() + .persistent() + .remove(&DataKey::TransferRequest(credential_id)); + + env.events().publish( + (Symbol::new(&env, "credential_transfer_approved"),), + (credential_id, old_subject, new_subject, issuer), + ); + } + // -------------------------------------------------------- // Query Functions // -------------------------------------------------------- @@ -2942,8 +3142,6 @@ mod tests { let issuers = register_n_issuers(&env, &client, &admin, 2); let schema_id = register_schema_helper(&env, &client, &issuers.get(0).unwrap()); let subject = Address::generate(&env); - - // threshold 3 > 2 issuers client.create_multisig_request( &issuers.get(0).unwrap(), &subject,