Skip to content
Merged
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
202 changes: 200 additions & 2 deletions contracts/stellar-id/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<TransferRequest> = 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<u64> = 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<u64> = 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<Identity> = 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
// --------------------------------------------------------
Expand Down Expand Up @@ -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,
Expand Down
Loading