diff --git a/src/internet_identity/src/account_management.rs b/src/internet_identity/src/account_management.rs index 8ef395204f..738a4ef31f 100644 --- a/src/internet_identity/src/account_management.rs +++ b/src/internet_identity/src/account_management.rs @@ -291,6 +291,11 @@ pub async fn prepare_account_delegation( }); update_root_hash(); + // Update last used timestamp + storage_borrow_mut(|storage| { + storage.set_account_last_used(anchor_number, origin.clone(), account_number, time()); + }); + delegation_bookkeeping(origin, ii_domain.clone(), session_duration_ns); Ok(PrepareAccountDelegation { diff --git a/src/internet_identity/src/storage.rs b/src/internet_identity/src/storage.rs index 2f3e7fd962..f34a1c7410 100644 --- a/src/internet_identity/src/storage.rs +++ b/src/internet_identity/src/storage.rs @@ -791,21 +791,135 @@ impl Storage { .map(|list| list.into_vec()) } + fn find_account_references( + &self, + anchor_number: AnchorNumber, + application_number: Option, + ) -> Option<( + (AnchorNumber, ApplicationNumber), + Vec, + )> { + let application_number = application_number?; + + let key = (anchor_number, application_number); + + let account_references = self.stable_account_reference_list_memory.get(&key)?; + + Some((key, account_references.into_vec())) + } + fn find_account_reference( &self, anchor_number: AnchorNumber, application_number: Option, account_number: Option, ) -> Option { - application_number.and_then(|app_num| { - self.lookup_account_references(anchor_number, app_num) - .and_then(|acc_ref_vec| { - acc_ref_vec - .iter() - .find(|acc_ref| acc_ref.account_number == account_number) - .cloned() - }) - }) + let (_, account_references) = + self.find_account_references(anchor_number, application_number)?; + + account_references + .into_iter() + .find(|account_reference| account_reference.account_number == account_number) + } + + /// Search for an account and account_reference and applies the function `f` if found. + /// + /// + /// The function `f` is called with a mutable reference to the account reference and an option to the mutable reference to the account. + /// + /// # Arguments + /// + /// * `anchor_number` - The anchor number of the account. + /// * `application_number` - The application number of the account. + /// * `account_number` - The account number of the account or None if synthetic account. + /// * `f` - The function to apply to the accounts. + /// + /// If the `account_number` is None, it means the storable account doesn't exist and account reference might exist. + /// * If the account reference exists, the function `f` is called with a mutable reference to the account reference and None as the second argument. + /// * If the account reference does not exist, None is returned. + /// + /// If the `account_number` is Some, it means the storable account exists (or existed at some point) and account references exists (or existed at some point). + /// * If the storable account exists, the function `f` is called with a mutable reference to the account reference and a mutable reference to the storable account. + /// * If the storable account does not exist, None is returned. + /// + /// # Returns + /// + /// * `None` if both account and account_reference are not found + /// * `Some(T)` if the account or account_reference are found where T is the result of the function `f`. + fn with_account_mut( + &mut self, + anchor_number: AnchorNumber, + application_number: Option, + maybe_account_number: Option, + f: F, + ) -> Option + where + F: FnOnce(&mut StorableAccountReference, Option<&mut StorableAccount>) -> T, + { + match maybe_account_number { + None => { + // We are looking for a synthetic account + let (key, mut account_references) = + self.find_account_references(anchor_number, application_number)?; + + let mut result = None; + + for account_reference in &mut account_references { + if account_reference.account_number == maybe_account_number { + result = Some(f(account_reference, None)); + break; + } + } + + let value = StorableAccountReferenceList::from_vec(account_references); + + self.stable_account_reference_list_memory.insert(key, value); + + result + } + Some(account_number) => { + // Account should be stored, otherwise, it was removed and we'll return `None`. + let mut storable_account = self.stable_account_memory.get(&account_number)?; + let (key, mut account_references) = + self.find_account_references(anchor_number, application_number)?; + + let mut result = None; + + for account_reference in &mut account_references { + if account_reference.account_number == maybe_account_number { + result = Some(f(account_reference, Some(&mut storable_account))); + break; + } + } + + let value = StorableAccountReferenceList::from_vec(account_references); + + self.stable_account_reference_list_memory.insert(key, value); + self.stable_account_memory + .insert(account_number, storable_account); + + result + } + } + } + + pub fn set_account_last_used( + &mut self, + anchor_number: AnchorNumber, + origin: FrontendHostname, + account_number: Option, + now: Timestamp, + ) -> Option<()> { + let application_number = self.lookup_application_number_with_origin(&origin); + + self.with_account_mut( + anchor_number, + application_number, + account_number, + |account_reference, _| { + account_reference.last_used = Some(now); + }, + ) } pub fn lookup_anchor_application_config( @@ -985,6 +1099,9 @@ impl Storage { self.stable_account_counter_discrepancy_counter_memory.get() } + /// Creates an account for that identity. + /// If the identity doesn't yet have accounts, it will create the account reference for the synthetic account. + /// But not a storable account for the synthetic one. pub fn create_additional_account( &mut self, params: CreateAccountParams, @@ -1008,6 +1125,9 @@ impl Storage { // Update counters with one more account. self.update_counters(app_num, anchor_number, AccountType::Account)?; + // last_used will be set once the user signs in with the account. + let last_used = None; + // Process account references match self .stable_account_reference_list_memory @@ -1022,11 +1142,11 @@ impl Storage { // This is because we don't create default accounts explicitly. let additional_account_reference = AccountReference { account_number: Some(account_number), - last_used: None, + last_used, }; let default_account_reference = AccountReference { account_number: None, - last_used: None, + last_used, }; self.stable_account_reference_list_memory.insert( (anchor_number, app_num), @@ -1039,7 +1159,7 @@ impl Storage { let mut refs_vec: Vec = existing_storable_list.into(); refs_vec.push(AccountReference { account_number: Some(account_number), - last_used: None, + last_used, }); self.stable_account_reference_list_memory .insert((anchor_number, app_num), refs_vec.into()); @@ -1191,21 +1311,28 @@ impl Storage { /// If the account number exists, then updates that account. /// If the account number doesn't exist, then gets or creates an application and creates and stores a default account. pub fn update_account(&mut self, params: UpdateAccountParams) -> Result { - check_frontend_length(¶ms.origin); - match params.account_number { + let UpdateAccountParams { + account_number, + anchor_number, + name, + origin, + } = params; + + check_frontend_length(&origin); + match account_number { Some(account_number) => self.update_existing_account(UpdateExistingAccountParams { account_number, - anchor_number: params.anchor_number, - name: params.name, - origin: params.origin, + anchor_number, + name, + origin, }), None => { // Default accounts are not stored by default. // They are created only once they are updated. self.create_default_account(CreateAccountParams { - anchor_number: params.anchor_number, - name: params.name, - origin: params.origin.clone(), + anchor_number, + name, + origin, }) } } @@ -1216,46 +1343,51 @@ impl Storage { &mut self, params: UpdateExistingAccountParams, ) -> Result { + let UpdateExistingAccountParams { + account_number, + anchor_number, + name, + origin, + } = params; + // Check if account reference exists for given anchor number, origin and account number, // if the account refence exists for a given anchor, that means the anchor has access. - let application_number = self.lookup_application_number_with_origin(¶ms.origin); - let account_reference = self - .find_account_reference( - params.anchor_number, - application_number, - Some(params.account_number), - ) - .ok_or(StorageError::AccountNotFound { - account_number: params.account_number, - })?; - // Check if the account reference has an account number, - // throw error if it doesn't since we only want to update - // accounts with an account number in this function. - let account_number = - account_reference - .account_number - .ok_or(StorageError::AccountNotFound { - account_number: params.account_number, - })?; - // Read account from storage - let mut storable_account = self.stable_account_memory.get(&account_number).ok_or( - StorageError::AccountNotFound { - account_number: params.account_number, - }, - )?; - // Update account and write back to storage - storable_account.name = params.name; - self.stable_account_memory - .insert(params.account_number, storable_account.clone()); - // Return updated account - Ok(Account::new_full( - params.anchor_number, - params.origin, - Some(storable_account.name), + let application_number = self.lookup_application_number_with_origin(&origin); + + let account_update_result = self.with_account_mut( + anchor_number, + application_number, Some(account_number), - account_reference.last_used, - storable_account.seed_from_anchor, - )) + |account_reference, maybe_storable_account| { + // Check if the account reference has an account number, + // throw error if it doesn't since we only want to update + // accounts with an account number in this function. + let account_number = account_reference.account_number?; + // Check if the storable_account exists. + // throw error if it doesn't since we only want to update + // existing accounts in this function. + let storable_account = maybe_storable_account?; + + // Update account and write back to storage + storable_account.name = name.clone(); + + // Return a user-facing account structure + Some(Account::new_full( + anchor_number, + origin, + Some(name), + Some(account_number), + account_reference.last_used, + storable_account.seed_from_anchor, + )) + }, + ); + + let Some(Some(account_update_result)) = account_update_result else { + return Err(StorageError::AccountNotFound { account_number }); + }; + + Ok(account_update_result) } /// Used in `update_account` to create a default account. @@ -1266,39 +1398,39 @@ impl Storage { &mut self, params: CreateAccountParams, ) -> Result { + let CreateAccountParams { + anchor_number, + name, + origin, + } = params; + // Create and store the default account. let new_account_number = self.allocate_account_number()?; let storable_account = StorableAccount { - name: params.name.clone(), + name: name.clone(), // This was a default account which uses the anchor number for the seed. - seed_from_anchor: Some(params.anchor_number), + seed_from_anchor: Some(anchor_number), }; self.stable_account_memory .insert(new_account_number, storable_account.clone()); // Get or create an application number from the account's origin. - let application_number = - self.lookup_or_insert_application_number_with_origin(¶ms.origin); + let application_number = self.lookup_or_insert_application_number_with_origin(&origin); // Update default account in the (anchor, origin) config. { let mut config = - self.lookup_anchor_application_config(params.anchor_number, application_number); + self.lookup_anchor_application_config(anchor_number, application_number); config.default_account_number = Some(new_account_number); - self.set_anchor_application_config(params.anchor_number, application_number, config); + self.set_anchor_application_config(anchor_number, application_number, config); } // Update counters with one more account. - self.update_counters( - application_number, - params.anchor_number, - AccountType::Account, - )?; + self.update_counters(application_number, anchor_number, AccountType::Account)?; - // Update the account references list. - let account_references_key = (params.anchor_number, application_number); + let account_references_key = (anchor_number, application_number); match self .stable_account_reference_list_memory .get(&account_references_key) @@ -1309,6 +1441,7 @@ impl Storage { // This is because we don't create default accounts explicitly. let new_ref = AccountReference { account_number: Some(new_account_number), + // The `last_used` field will be set when the user signs with this account. last_used: None, }; self.stable_account_reference_list_memory @@ -1316,7 +1449,7 @@ impl Storage { // One new account reference was created. self.update_counters( application_number, - params.anchor_number, + anchor_number, AccountType::AccountReference, )?; } @@ -1336,8 +1469,8 @@ impl Storage { // This could happen if the account was removed and now we try to update it. if !found_and_updated { return Err(StorageError::MissingAccount { - anchor_number: params.anchor_number, - name: params.name.clone(), + anchor_number, + name: name.clone(), }); } self.stable_account_reference_list_memory @@ -1347,8 +1480,8 @@ impl Storage { // Return created default account Ok(Account::new_full( - params.anchor_number, - params.origin, + anchor_number, + origin, Some(storable_account.name), Some(new_account_number), None, diff --git a/src/internet_identity/src/storage/storable/account_reference.rs b/src/internet_identity/src/storage/storable/account_reference.rs index 818a1ace20..83950af7c9 100644 --- a/src/internet_identity/src/storage/storable/account_reference.rs +++ b/src/internet_identity/src/storage/storable/account_reference.rs @@ -11,6 +11,8 @@ use std::borrow::Cow; pub struct StorableAccountReference { #[n(0)] pub account_number: Option, // None is the unreserved synthetic account + // Only updated when the account is used to create a delegation. + // For example, it's not changed when the account is renamed. #[n(1)] pub last_used: Option, } diff --git a/src/internet_identity/src/storage/storable/account_reference_list.rs b/src/internet_identity/src/storage/storable/account_reference_list.rs index dadff58a12..2f58720f32 100644 --- a/src/internet_identity/src/storage/storable/account_reference_list.rs +++ b/src/internet_identity/src/storage/storable/account_reference_list.rs @@ -29,6 +29,10 @@ impl StorableAccountReferenceList { pub fn into_vec(self) -> Vec { self.0 } + + pub fn from_vec(vec: Vec) -> Self { + Self(vec) + } } impl From for Vec { diff --git a/src/internet_identity/src/storage/tests.rs b/src/internet_identity/src/storage/tests.rs index 0e0669b847..81b78b0c3e 100644 --- a/src/internet_identity/src/storage/tests.rs +++ b/src/internet_identity/src/storage/tests.rs @@ -3,6 +3,7 @@ use crate::openid::OpenIdCredential; use crate::state::PersistentState; use crate::stats::activity_stats::activity_counter::active_anchor_counter::ActiveAnchorCounter; use crate::stats::activity_stats::{ActivityStats, CompletedActivityStats, OngoingActivityStats}; +use crate::storage::account::{CreateAccountParams, ReadAccountParams}; use crate::storage::anchor::{Anchor, Device}; use crate::storage::{Header, StorageError, MAX_ENTRIES}; use crate::Storage; @@ -316,6 +317,192 @@ fn should_not_overwrite_device_credential_lookup() { ); } +#[test] +fn should_set_account_last_used() { + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + let origin = "https://example.com".to_string(); + + // Create an anchor + let anchor = storage.allocate_anchor(0).unwrap(); + let anchor_number = anchor.anchor_number(); + storage.create(anchor).unwrap(); + + // Create an additional account for this anchor and origin + let account = storage + .create_additional_account(CreateAccountParams { + anchor_number, + name: "Test Account".to_string(), + origin: origin.clone(), + }) + .unwrap(); + + let account_number = account.account_number.unwrap(); + + // Initially, last_used should be None + let read_account = storage + .read_account(ReadAccountParams { + anchor_number, + origin: &origin, + account_number: Some(account_number), + known_app_num: None, + }) + .unwrap(); + assert_eq!(read_account.last_used, None); + + // Set last_used for the additional account + let timestamp = 123456789u64; + let result = storage.set_account_last_used( + anchor_number, + origin.clone(), + Some(account_number), + timestamp, + ); + assert!(result.is_some()); + + // Verify last_used was updated + let read_account = storage + .read_account(ReadAccountParams { + anchor_number, + origin: &origin, + account_number: Some(account_number), + known_app_num: None, + }) + .unwrap(); + assert_eq!(read_account.last_used, Some(timestamp)); + + // Update last_used again with a new timestamp + let new_timestamp = 987654321u64; + let result = storage.set_account_last_used( + anchor_number, + origin.clone(), + Some(account_number), + new_timestamp, + ); + assert!(result.is_some()); + + // Verify last_used was updated to the new timestamp + let read_account = storage + .read_account(ReadAccountParams { + anchor_number, + origin: &origin, + account_number: Some(account_number), + known_app_num: None, + }) + .unwrap(); + assert_eq!(read_account.last_used, Some(new_timestamp)); +} + +#[test] +fn should_set_account_last_used_for_synthethic_account() { + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + let origin = "https://example.com".to_string(); + + // Create an anchor + let anchor = storage.allocate_anchor(0).unwrap(); + let anchor_number = anchor.anchor_number(); + storage.create(anchor).unwrap(); + + // Set last_used for the synthetic account (account_number = None) + let timestamp = 555555u64; + let result = storage.set_account_last_used(anchor_number, origin.clone(), None, timestamp); + assert!(result.is_none()); + + // Verify last_used was updated for the synthetic account + let read_account = storage + .read_account(ReadAccountParams { + anchor_number, + origin: &origin, + account_number: None, + known_app_num: None, + }) + .unwrap(); + // Because the account reference doesn't exist, the `last_used` is not updated. + assert_eq!(read_account.last_used, None); +} + +#[test] +fn should_set_account_last_used_for_synthetic_account_with_reference() { + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + let origin = "https://example.com".to_string(); + + // Create an anchor + let anchor = storage.allocate_anchor(0).unwrap(); + let anchor_number = anchor.anchor_number(); + storage.create(anchor).unwrap(); + + // Create an additional account to force creation of account references + storage + .create_additional_account(CreateAccountParams { + anchor_number, + name: "Test Account".to_string(), + origin: origin.clone(), + }) + .unwrap(); + + // Set last_used for the synthetic account (account_number = None) + let timestamp = 555555u64; + let result = storage.set_account_last_used(anchor_number, origin.clone(), None, timestamp); + assert!(result.is_some()); + + // Verify last_used was updated for the synthetic account + let read_account = storage + .read_account(ReadAccountParams { + anchor_number, + origin: &origin, + account_number: None, + known_app_num: None, + }) + .unwrap(); + assert_eq!(read_account.last_used, Some(timestamp)); +} + +#[test] +fn should_return_none_when_setting_last_used_for_nonexistent_account() { + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + let origin = "https://example.com".to_string(); + + // Create an anchor + let anchor = storage.allocate_anchor(0).unwrap(); + let anchor_number = anchor.anchor_number(); + storage.create(anchor).unwrap(); + + // Try to set last_used for a non-existent account number + let nonexistent_account_number = 99999u64; + let timestamp = 123456u64; + let result = storage.set_account_last_used( + anchor_number, + origin, + Some(nonexistent_account_number), + timestamp, + ); + + // Should return None because the account doesn't exist + assert!(result.is_none()); +} + +#[test] +fn should_return_none_when_setting_last_used_for_nonexistent_origin() { + let memory = VectorMemory::default(); + let mut storage = Storage::new((10_000, 3_784_873), memory); + + // Create an anchor + let anchor = storage.allocate_anchor(0).unwrap(); + let anchor_number = anchor.anchor_number(); + storage.create(anchor).unwrap(); + + // Try to set last_used for an origin that hasn't been registered + let nonexistent_origin = "https://nonexistent.com".to_string(); + let timestamp = 123456u64; + let result = storage.set_account_last_used(anchor_number, nonexistent_origin, None, timestamp); + + // Should return None because the origin/application doesn't exist + assert!(result.is_none()); +} + fn sample_device() -> Device { Device { pubkey: ByteBuf::from("hello world, I am a public key"), diff --git a/src/internet_identity/tests/integration/accounts.rs b/src/internet_identity/tests/integration/accounts.rs index 750ea5641b..62cb7aaf36 100644 --- a/src/internet_identity/tests/integration/accounts.rs +++ b/src/internet_identity/tests/integration/accounts.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use canister_tests::{ api::internet_identity::{ api_v2::{ @@ -19,7 +17,9 @@ use internet_identity_interface::internet_identity::types::{ PrepareAccountDelegation, }; use pocket_ic::RejectResponse; +use pretty_assertions::assert_eq; use serde_bytes::ByteBuf; +use std::time::Duration; /// Verifies that one account can be created #[test] @@ -1028,3 +1028,341 @@ fn should_issue_different_principals_for_different_accounts() -> Result<(), Reje Ok(()) } + +/// Verifies that the last_used field is updated after prepare_account_delegation. +#[test] +fn should_update_last_used_after_prepare_account_delegation() -> Result<(), RejectResponse> { + let env = env(); + let canister_id = install_ii_with_archive(&env, None, None); + let user_number = flows::register_anchor(&env, canister_id); + let frontend_hostname = "https://some-dapp.com".to_string(); + let pub_session_key = ByteBuf::from("session public key"); + + // Create an account + let created_account = create_account( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + "Test Account".to_string(), + ) + .unwrap() + .unwrap(); + + // Verify last_used is initially None + assert_eq!(created_account.last_used, None); + + // Retrieve the account before prepare_account_delegation to verify last_used is None + let accounts_before = get_accounts( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + ) + .unwrap() + .unwrap(); + + let account_before = accounts_before + .iter() + .find(|account| account.account_number == created_account.account_number) + .expect("Account should exist in the list"); + + assert_eq!( + account_before.last_used, None, + "last_used should be None before prepare_account_delegation" + ); + + // Capture timestamp before prepare_account_delegation + let time_before = env.get_time().as_nanos_since_unix_epoch(); + + // Call prepare_account_delegation for the created account + let params = AccountDelegationParams::new( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + created_account.account_number, + pub_session_key, + ); + + prepare_account_delegation(¶ms, None).unwrap().unwrap(); + + // Capture timestamp after prepare_account_delegation + let time_after = env.get_time().as_nanos_since_unix_epoch(); + + // Retrieve the account again to check last_used + let accounts_list = get_accounts( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname, + ) + .unwrap() + .unwrap(); + + // Find the created account in the list (it should be at index 1, after the default account) + let updated_account = accounts_list + .iter() + .find(|account| account.account_number == created_account.account_number) + .expect("Account should exist in the list"); + + // Verify last_used is now populated + assert!( + updated_account.last_used.is_some(), + "last_used should be populated after prepare_account_delegation" + ); + + let last_used = updated_account.last_used.unwrap(); + + // Verify the timestamp is within the expected range + assert!( + last_used >= time_before && last_used <= time_after, + "last_used timestamp should be between {} and {}, but was {}", + time_before, + time_after, + last_used + ); + + Ok(()) +} + +/// Verifies that the last_used field is not updated after prepare_account_delegation +/// for synthetic accounts when the user doesn't have any other account. +#[test] +fn should_not_update_last_used_synthetic_account_after_prepare_account_delegation( +) -> Result<(), RejectResponse> { + let env = env(); + let canister_id = install_ii_with_archive(&env, None, None); + let user_number = flows::register_anchor(&env, canister_id); + let frontend_hostname = "https://some-dapp.com".to_string(); + let pub_session_key = ByteBuf::from("session public key"); + + // Retrieve the account before prepare_account_delegation to verify last_used is None + let accounts_before = get_accounts( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + ) + .unwrap() + .unwrap(); + + let account_before = accounts_before + .iter() + .find(|account| account.account_number.is_none()) + .expect("Account should exist in the list"); + + assert_eq!( + account_before.last_used, None, + "last_used should be None before prepare_account_delegation" + ); + + // Call prepare_account_delegation for the created account + let params = AccountDelegationParams::new( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + None, + pub_session_key, + ); + + prepare_account_delegation(¶ms, None).unwrap().unwrap(); + + // Retrieve the account again to check last_used + let accounts_list = get_accounts( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname, + ) + .unwrap() + .unwrap(); + + // Find the created account in the list (it should be at index 1, after the default account) + let updated_account = accounts_list + .iter() + .find(|account| account.account_number.is_none()) + .expect("Account should exist in the list"); + + // Verify last_used is now populated + assert!( + updated_account.last_used.is_none(), + "last_used should not be populated after prepare_account_delegation for synthetic accounts" + ); + + Ok(()) +} + +/// Verifies that last_used is tracked independently for different accounts. +#[test] +fn should_update_last_used_independently_for_different_accounts() -> Result<(), RejectResponse> { + let env = env(); + let canister_id = install_ii_with_archive(&env, None, None); + let user_number = flows::register_anchor(&env, canister_id); + let frontend_hostname = "https://some-dapp.com".to_string(); + let pub_session_key = ByteBuf::from("session public key"); + + // Create two different accounts + let account_1 = create_account( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + "Account 1".to_string(), + ) + .unwrap() + .unwrap(); + + let account_2 = create_account( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + "Account 2".to_string(), + ) + .unwrap() + .unwrap(); + + // Verify both accounts have last_used = None initially + assert_eq!(account_1.last_used, None); + assert_eq!(account_2.last_used, None); + + // Call prepare_account_delegation for account_1 + let params_1 = AccountDelegationParams::new( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + account_1.account_number, + pub_session_key.clone(), + ); + + let time_before_account_1 = env.get_time().as_nanos_since_unix_epoch(); + prepare_account_delegation(¶ms_1, None) + .unwrap() + .unwrap(); + let time_after_account_1 = env.get_time().as_nanos_since_unix_epoch(); + + // Retrieve accounts and verify account_1 has last_used set, account_2 still has None + let accounts_after_first_delegation = get_accounts( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + ) + .unwrap() + .unwrap(); + + let account_1_after_first = accounts_after_first_delegation + .iter() + .find(|account| account.account_number == account_1.account_number) + .expect("Account 1 should exist"); + + let account_2_after_first = accounts_after_first_delegation + .iter() + .find(|account| account.account_number == account_2.account_number) + .expect("Account 2 should exist"); + + assert!( + account_1_after_first.last_used.is_some(), + "account_1 last_used should be populated after prepare_account_delegation" + ); + let account_1_last_used = account_1_after_first.last_used.unwrap(); + + assert_eq!( + account_2_after_first.last_used, None, + "account_2 last_used should still be None after only account_1 delegation" + ); + + // Verify account_1's timestamp is within expected range + assert!( + account_1_last_used >= time_before_account_1 && account_1_last_used <= time_after_account_1, + "account_1 last_used should be between {} and {}", + time_before_account_1, + time_after_account_1 + ); + + // Advance time to create clear separation + env.advance_time(Duration::from_secs(60)); + + // Call prepare_account_delegation for account_2 + let params_2 = AccountDelegationParams::new( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname.clone(), + account_2.account_number, + pub_session_key, + ); + + let time_before_account_2 = env.get_time().as_nanos_since_unix_epoch(); + prepare_account_delegation(¶ms_2, None) + .unwrap() + .unwrap(); + let time_after_account_2 = env.get_time().as_nanos_since_unix_epoch(); + + // Retrieve accounts and verify both have last_used set, but account_1's hasn't changed + let accounts_after_second_delegation = get_accounts( + &env, + canister_id, + principal_1(), + user_number, + frontend_hostname, + ) + .unwrap() + .unwrap(); + + let account_1_after_second = accounts_after_second_delegation + .iter() + .find(|account| account.account_number == account_1.account_number) + .expect("Account 1 should exist"); + + let account_2_after_second = accounts_after_second_delegation + .iter() + .find(|account| account.account_number == account_2.account_number) + .expect("Account 2 should exist"); + + assert!( + account_2_after_second.last_used.is_some(), + "account_2 last_used should be populated after prepare_account_delegation" + ); + let account_2_last_used = account_2_after_second.last_used.unwrap(); + + // Verify account_1's last_used hasn't changed + assert_eq!( + account_1_after_second.last_used.unwrap(), + account_1_last_used, + "account_1 last_used should not change when account_2 is used" + ); + + // Verify account_2's timestamp is within expected range + assert!( + account_2_last_used >= time_before_account_2 && account_2_last_used <= time_after_account_2, + "account_2 last_used should be between {} and {}", + time_before_account_2, + time_after_account_2 + ); + + // Verify that account_1's timestamp is earlier than account_2's + assert!( + account_1_last_used < account_2_last_used, + "account_1 last_used ({}) should be earlier than account_2 last_used ({})", + account_1_last_used, + account_2_last_used + ); + + Ok(()) +}