diff --git a/app/contract/contracts/Folder/check_output.txt b/app/contract/contracts/Folder/check_output.txt deleted file mode 100644 index 51b5dc465..000000000 Binary files a/app/contract/contracts/Folder/check_output.txt and /dev/null differ diff --git a/app/contract/contracts/Folder/src/admin.rs b/app/contract/contracts/Folder/src/admin.rs index 10e69551a..262a66b36 100644 --- a/app/contract/contracts/Folder/src/admin.rs +++ b/app/contract/contracts/Folder/src/admin.rs @@ -1,4 +1,4 @@ -use crate::errors:: RustAcademyError; +use crate::errors::RustAcademyError; use crate::events::{ publish_admin_changed, publish_contract_initialized, publish_contract_migrated, publish_contract_paused, publish_fee_collector_rotated, publish_per_asset_fee_set, @@ -13,9 +13,9 @@ use soroban_sdk::{Address, BytesN, Env, Vec}; /// /// This is a one-time operation; subsequent calls fail with [`AlreadyInitialized`]. /// The initial admin is assigned the [`Role::Admin`] role. -pub fn initialize(env: &Env, admin: Address) -> Result<(), RustAcademyError> { +pub fn initialize(env: &Env, admin: Address) -> Result<(), RustAcademyError> { if storage::is_initialized(env) || has_admin(env) { - return Err( RustAcademyError::AlreadyInitialized); + return Err(RustAcademyError::AlreadyInitialized); } // Set initial admin address (singleton for compatibility). @@ -49,11 +49,11 @@ pub fn has_admin(env: &Env) -> bool { } /// Require that one-time contract initialization has completed. -pub fn require_initialized(env: &Env) -> Result<(), RustAcademyError> { +pub fn require_initialized(env: &Env) -> Result<(), RustAcademyError> { if storage::is_initialized(env) { Ok(()) } else { - Err( RustAcademyError::Unauthorized) + Err(RustAcademyError::Unauthorized) } } @@ -68,31 +68,59 @@ pub fn has_role(env: &Env, address: &Address, role: Role) -> bool { roles.contains(role) } +fn current_admin(env: &Env) -> Result { + let admin = storage::get_admin(env).ok_or(RustAcademyError::InvalidRoleState)?; + let roles = storage::get_roles(env, &admin); + if roles.contains(Role::Admin) { + Ok(admin) + } else { + Err(RustAcademyError::InvalidRoleState) + } +} + +fn apply_admin_transfer(env: &Env, old_admin: &Address, new_admin: &Address) { + storage::set_admin(env, new_admin); + storage::clear_pending_admin_transfer(env); + + let old_roles = storage::get_roles(env, old_admin); + let mut filtered_old_roles = Vec::new(env); + for role in old_roles { + if role != Role::Admin { + filtered_old_roles.push_back(role); + } + } + storage::set_roles(env, old_admin, &filtered_old_roles); + + let mut new_roles = storage::get_roles(env, new_admin); + if !new_roles.contains(Role::Admin) { + new_roles.push_back(Role::Admin); + storage::set_roles(env, new_admin, &new_roles); + } + + publish_admin_changed(env, old_admin.clone(), new_admin.clone()); +} + /// Require that the caller has at least one of the specified roles. -pub fn require_any_role(env: &Env, caller: &Address, roles: &[Role]) -> Result<(), RustAcademyError> { +pub fn require_any_role( + env: &Env, + caller: &Address, + roles: &[Role], +) -> Result<(), RustAcademyError> { require_initialized(env)?; caller.require_auth(); + let _ = current_admin(env)?; let user_roles = storage::get_roles(env, caller); for role in roles { if user_roles.contains(*role) { return Ok(()); } } - // Fallback: legacy deployments may not have role assignments. - // Accept the stored admin address as matching any Admin role request. - if roles.contains(&Role::Admin) { - if let Some(admin) = storage::get_admin(env) { - if admin == *caller { - return Ok(()); - } - } - } - Err( RustAcademyError::InsufficientRole) + Err(RustAcademyError::InsufficientRole) } /// Require that the caller is an Admin. -pub fn require_admin(env: &Env, caller: &Address) -> Result<(), RustAcademyError> { +pub fn require_admin(env: &Env, caller: &Address) -> Result<(), RustAcademyError> { require_any_role(env, caller, &[Role::Admin]) } @@ -102,8 +130,13 @@ pub fn grant_role( caller: Address, target: Address, role: Role, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_admin(env, &caller)?; + let admin = current_admin(env)?; + + if target == admin && role == Role::Admin { + return Err(RustAcademyError::InvalidRoleState); + } let mut roles = storage::get_roles(env, &target); if !roles.contains(role) { @@ -119,8 +152,13 @@ pub fn revoke_role( caller: Address, target: Address, role: Role, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_admin(env, &caller)?; + let admin = current_admin(env)?; + + if target == admin && role == Role::Admin { + return Err(RustAcademyError::InvalidRoleState); + } let roles = storage::get_roles(env, &target); let mut new_roles = Vec::new(env); @@ -134,35 +172,86 @@ pub fn revoke_role( } /// Set a new primary admin address (**Admin only**). -pub fn set_admin(env: &Env, caller: Address, new_admin: Address) -> Result<(), RustAcademyError> { +pub fn set_admin(env: &Env, caller: Address, new_admin: Address) -> Result<(), RustAcademyError> { require_admin(env, &caller)?; + let old_admin = current_admin(env)?; + + if old_admin == new_admin { + storage::clear_pending_admin_transfer(env); + return Ok(()); + } - let old_admin = storage::get_admin(env).unwrap(); - storage::set_admin(env, &new_admin); + apply_admin_transfer(env, &old_admin, &new_admin); + Ok(()) +} - // Revoke Admin role from old admin. - let roles = storage::get_roles(env, &old_admin); - let mut new_roles = Vec::new(env); - for r in roles { - if r != Role::Admin { - new_roles.push_back(r); - } +/// Propose an admin transfer that must later be accepted by the target. +pub fn propose_admin_transfer( + env: &Env, + caller: Address, + new_admin: Address, +) -> Result<(), RustAcademyError> { + require_admin(env, &caller)?; + let admin = current_admin(env)?; + + if admin == new_admin { + storage::clear_pending_admin_transfer(env); + return Ok(()); + } + + storage::set_pending_admin_transfer(env, &new_admin); + Ok(()) +} + +/// Accept the currently pending admin transfer. +pub fn accept_admin_transfer(env: &Env, caller: Address) -> Result<(), RustAcademyError> { + caller.require_auth(); + let new_admin = + storage::get_pending_admin_transfer(env).ok_or(RustAcademyError::NoPendingAdminTransfer)?; + if caller != new_admin { + return Err(RustAcademyError::InsufficientRole); + } + + let old_admin = current_admin(env)?; + if old_admin == new_admin { + storage::clear_pending_admin_transfer(env); + return Ok(()); + } + + apply_admin_transfer(env, &old_admin, &new_admin); + Ok(()) +} + +/// Cancel the pending admin transfer. +pub fn cancel_admin_transfer(env: &Env, caller: Address) -> Result<(), RustAcademyError> { + require_admin(env, &caller)?; + if storage::get_pending_admin_transfer(env).is_none() { + return Err(RustAcademyError::NoPendingAdminTransfer); } - storage::set_roles(env, &old_admin, &new_roles); - // Grant Admin role to new admin if not already present. - let mut roles = storage::get_roles(env, &new_admin); - if !roles.contains(Role::Admin) { + storage::clear_pending_admin_transfer(env); + Ok(()) +} + +/// Remove all roles from an account. +pub fn clear_roles(env: &Env, caller: Address, target: Address) -> Result<(), RustAcademyError> { + require_admin(env, &caller)?; + let admin = current_admin(env)?; + + if target == admin { + let mut roles = Vec::new(env); roles.push_back(Role::Admin); - storage::set_roles(env, &new_admin, &roles); + storage::set_roles(env, &target, &roles); + return Ok(()); } - publish_admin_changed(env, old_admin, new_admin); + let new_roles = Vec::new(env); + storage::set_roles(env, &target, &new_roles); Ok(()) } /// Set the paused state (**Admin or Operator only**). -pub fn set_paused(env: &Env, caller: Address, new_state: bool) -> Result<(), RustAcademyError> { +pub fn set_paused(env: &Env, caller: Address, new_state: bool) -> Result<(), RustAcademyError> { require_any_role(env, &caller, &[Role::Admin, Role::Operator])?; storage::set_paused(env, new_state); @@ -179,14 +268,14 @@ pub fn get_version(env: &Env) -> u32 { storage::get_contract_version(env).unwrap_or(storage::LEGACY_CONTRACT_VERSION) } -pub fn migrate(env: &Env, caller: &Address) -> Result { +pub fn migrate(env: &Env, caller: &Address) -> Result { let from_version = get_version(env); if from_version == storage::LEGACY_CONTRACT_VERSION { caller.require_auth(); - let admin = storage::get_admin(env).ok_or( RustAcademyError::Unauthorized)?; + let admin = storage::get_admin(env).ok_or(RustAcademyError::Unauthorized)?; if admin != *caller { - return Err( RustAcademyError::InsufficientRole); + return Err(RustAcademyError::InsufficientRole); } // Legacy deployments may not have role assignments. Seed Admin role so @@ -201,14 +290,14 @@ pub fn migrate(env: &Env, caller: &Address) -> Result { } if from_version > storage::CURRENT_CONTRACT_VERSION { - return Err( RustAcademyError::InvalidContractVersion); + return Err(RustAcademyError::InvalidContractVersion); } let mut version = from_version; while version < storage::CURRENT_CONTRACT_VERSION { version = match version { storage::LEGACY_CONTRACT_VERSION => migrate_legacy_to_v1(env), - _ => return Err( RustAcademyError::InvalidContractVersion), + _ => return Err(RustAcademyError::InvalidContractVersion), }; } @@ -218,7 +307,7 @@ pub fn migrate(env: &Env, caller: &Address) -> Result { // Post-upgrade invariant checks (Issue #432) if let Err(_msg) = storage::assert_post_upgrade_invariants(env) { - env.panic_with_error( RustAcademyError::InternalError); + env.panic_with_error(RustAcademyError::InternalError); } Ok(version) @@ -244,7 +333,7 @@ pub fn set_upgrade_window( caller: &Address, start: u64, end: u64, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_admin(env, caller)?; storage::set_upgrade_window(env, start, end); Ok(()) @@ -259,16 +348,16 @@ pub fn start_upgrade( caller: &Address, new_version: u32, new_wasm_hash: BytesN<32>, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_admin(env, caller)?; // Check upgrade window is active (Issue #432 AC1) if !storage::is_upgrade_window_active(env) { - return Err( RustAcademyError::InvalidAmount); // Repurpose for "upgrade window not active" + return Err(RustAcademyError::InvalidAmount); // Repurpose for "upgrade window not active" } if storage::is_upgrade_in_progress(env) { - return Err( RustAcademyError::ContractPaused); // Reuse for "upgrade in progress" + return Err(RustAcademyError::ContractPaused); // Reuse for "upgrade in progress" } let old_version = get_version(env); @@ -299,22 +388,22 @@ pub fn upgrade( env: &Env, caller: &Address, new_wasm_hash: BytesN<32>, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_admin(env, caller)?; if !storage::is_upgrade_in_progress(env) { - return Err( RustAcademyError::InternalError); + return Err(RustAcademyError::InternalError); } if !storage::is_upgrade_window_active(env) { - return Err( RustAcademyError::InvalidAmount); + return Err(RustAcademyError::InvalidAmount); } - let pending_hash = storage::get_pending_upgrade_wasm_hash(env) - .ok_or( RustAcademyError::InternalError)?; + let pending_hash = + storage::get_pending_upgrade_wasm_hash(env).ok_or(RustAcademyError::InternalError)?; if new_wasm_hash != pending_hash { - return Err( RustAcademyError::CommitmentMismatch); + return Err(RustAcademyError::CommitmentMismatch); } storage::set_wasm_hash(env, &new_wasm_hash); @@ -330,7 +419,7 @@ pub fn upgrade( } /// Cancel a pending upgrade and clear gating state (**Admin only**). -pub fn cancel_upgrade(env: &Env, caller: &Address) -> Result<(), RustAcademyError> { +pub fn cancel_upgrade(env: &Env, caller: &Address) -> Result<(), RustAcademyError> { require_admin(env, caller)?; storage::clear_pending_upgrade(env); Ok(()) @@ -344,27 +433,27 @@ pub fn complete_upgrade( env: &Env, caller: &Address, new_version: u32, -) -> Result { +) -> Result { if !storage::is_upgrade_in_progress(env) { - return Err( RustAcademyError::InternalError); // Not in upgrade state + return Err(RustAcademyError::InternalError); // Not in upgrade state } // Verify version and hash (Issue #432 AC2) - let pending_version = storage::get_pending_upgrade_version(env) - .ok_or( RustAcademyError::InternalError)?; - let pending_hash = storage::get_pending_upgrade_wasm_hash(env) - .ok_or( RustAcademyError::InternalError)?; + let pending_version = + storage::get_pending_upgrade_version(env).ok_or(RustAcademyError::InternalError)?; + let pending_hash = + storage::get_pending_upgrade_wasm_hash(env).ok_or(RustAcademyError::InternalError)?; if new_version != pending_version && new_version != 0 { - return Err( RustAcademyError::InvalidContractVersion); + return Err(RustAcademyError::InvalidContractVersion); } // Verify currently running WASM matches pending hash // Note: in Soroban, we can't directly check the current WASM hash from within the contract // except by checking what we just stored in storage::set_wasm_hash during upgrade(). - let actual_hash = storage::get_wasm_hash(env).ok_or( RustAcademyError::InternalError)?; + let actual_hash = storage::get_wasm_hash(env).ok_or(RustAcademyError::InternalError)?; if actual_hash != pending_hash { - return Err( RustAcademyError::InternalError); + return Err(RustAcademyError::InternalError); } let old_version = get_version(env); @@ -374,7 +463,7 @@ pub fn complete_upgrade( // Ensure migrated version matches expected if migrated_version != pending_version && pending_version != 0 { - return Err( RustAcademyError::InvalidContractVersion); + return Err(RustAcademyError::InvalidContractVersion); } storage::clear_pending_upgrade(env); @@ -385,9 +474,9 @@ pub fn complete_upgrade( /// Require that the contract is not paused. #[allow(dead_code)] -pub fn require_not_paused(env: &Env) -> Result<(), RustAcademyError> { +pub fn require_not_paused(env: &Env) -> Result<(), RustAcademyError> { if is_paused(env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } Ok(()) } @@ -398,7 +487,7 @@ pub fn set_pause_flags( caller: &Address, flags_to_enable: u64, flags_to_disable: u64, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_any_role(env, caller, &[Role::Admin, Role::Operator])?; storage::set_pause_flags(env, caller, flags_to_enable, flags_to_disable); @@ -406,7 +495,11 @@ pub fn set_pause_flags( } /// Set fee configuration (**Admin or Operator only**). -pub fn set_fee_config(env: &Env, caller: &Address, config: FeeConfig) -> Result<(), RustAcademyError> { +pub fn set_fee_config( + env: &Env, + caller: &Address, + config: FeeConfig, +) -> Result<(), RustAcademyError> { require_any_role(env, caller, &[Role::Admin, Role::Operator])?; storage::set_fee_config(env, &config); @@ -420,9 +513,12 @@ pub fn set_per_asset_fee( caller: &Address, token: Address, config: PerAssetFeeConfig, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_any_role(env, caller, &[Role::Admin, Role::Operator])?; + if config.fee_bps > 10_000 || config.arbiter_bps > 10_000 { + return Err(RustAcademyError::InvalidAmount); + } config.validate()?; storage::set_per_asset_fee(env, &token, &config); @@ -442,7 +538,7 @@ pub fn set_oracle_fee_config( env: &Env, caller: &Address, config: crate::types::OracleFeeConfig, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_any_role(env, caller, &[Role::Admin, Role::Operator])?; storage::set_oracle_fee_config(env, &config); @@ -454,7 +550,7 @@ pub fn set_platform_wallet( env: &Env, caller: &Address, wallet: Address, -) -> Result<(), RustAcademyError> { +) -> Result<(), RustAcademyError> { require_admin(env, caller)?; storage::set_platform_wallet(env, &wallet); @@ -467,7 +563,7 @@ pub fn rotate_fee_collector( env: &Env, caller: &Address, new_collector: Address, -) -> Result { +) -> Result { require_admin(env, caller)?; let next_index = fee_router::rotate_collector(env, &new_collector); diff --git a/app/contract/contracts/Folder/src/errors.rs b/app/contract/contracts/Folder/src/errors.rs index cf9d4f508..4c8161be1 100644 --- a/app/contract/contracts/Folder/src/errors.rs +++ b/app/contract/contracts/Folder/src/errors.rs @@ -10,7 +10,7 @@ use soroban_sdk::contracterror; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] -pub enum RustAcademyError { +pub enum RustAcademyError { // Validation failures (100-199) InvalidAmount = 100, InvalidSalt = 101, @@ -19,6 +19,10 @@ pub enum RustAcademyError { Unauthorized = 200, AlreadyInitialized = 201, InsufficientRole = 202, + /// The stored admin/role snapshot is internally inconsistent. + InvalidRoleState = 203, + /// No pending admin transfer is available to accept or cancel. + NoPendingAdminTransfer = 204, // State, escrow, and commitment violations (300-399) ContractPaused = 300, PrivacyAlreadySet = 301, diff --git a/app/contract/contracts/Folder/src/hook_oracle_test.rs.bak b/app/contract/contracts/Folder/src/hook_oracle_test.rs.bak deleted file mode 100644 index d46287a43..000000000 --- a/app/contract/contracts/Folder/src/hook_oracle_test.rs.bak +++ /dev/null @@ -1,229 +0,0 @@ -#![allow(dead_code)] -#![allow(clippy::result_unit_err)] - -use crate::{ - types::{FeeConfig, HookEventKind, OracleFeeConfig}, - RustAcademyContract, RustAcademyContractClient, -}; -use soroban_sdk::{ - contract, contractimpl, - testutils::{Address as _, Ledger}, - token, Address, Bytes, BytesN, Env, Symbol, -}; - -#[contract] -pub struct HookStubContract; - -#[contractimpl] -impl HookStubContract { - pub fn on_escrow_event( - env: Env, - _event_kind: u32, - _escrow_id: BytesN<32>, - _owner: Address, - _token: Address, - _amount: i128, - _fee: i128, - ) -> Result<(), ()> { - let key = Symbol::short("invocations"); - let mut count: i128 = env.storage().persistent().get(&key).unwrap_or(0); - count += 1; - env.storage().persistent().set(&key, &count); - Ok(()) - } - - pub fn get_invocations(env: Env) -> i128 { - env.storage() - .persistent() - .get(&Symbol::short("invocations")) - .unwrap_or(0) - } -} - -#[contract] -pub struct MaliciousHookContract; - -#[contractimpl] -impl MaliciousHookContract { - pub fn init(env: Env, target: Address) { - env.storage() - .persistent() - .set(&Symbol::short("target"), &target); - } - - pub fn on_escrow_event( - env: Env, - event_kind: u32, - _escrow_id: BytesN<32>, - _owner: Address, - _token: Address, - _amount: i128, - _fee: i128, - ) -> Result<(), ()> { - if event_kind != HookEventKind::Settle as u32 { - return Ok(()); - } - let target: Address = env - .storage() - .persistent() - .get(&Symbol::short("target")) - .unwrap(); - let client = RustAcademyContractClient::new(&env, &target); - let random_hook = Address::generate(&env); - let _ = client.register_hook(&random_hook); - Err(()) - } -} - -#[contract] -pub struct MockOracleContract; - -#[contractimpl] -impl MockOracleContract { - pub fn set_price(env: Env, price_micros: i128, timestamp: u64) { - env.storage() - .persistent() - .set(&Symbol::short("price"), &price_micros); - env.storage() - .persistent() - .set(&Symbol::short("timestamp"), ×tamp); - } - - pub fn get_price(env: Env) -> Result<(i128, u64), ()> { - let price: i128 = env - .storage() - .persistent() - .get(&Symbol::short("price")) - .unwrap_or(0); - let timestamp: u64 = env - .storage() - .persistent() - .get(&Symbol::short("timestamp")) - .unwrap_or(0); - if price <= 0 { - return Err(()); - } - Ok((price, timestamp)) - } -} - -fn setup<'a>( - env: &'a Env, -) -> ( - RustAcademyContractClient<'a>, - Address, - Address, - Address, - Address, -) { - let admin = Address::generate(env); - let platform_wallet = Address::generate(env); - let owner = Address::generate(env); - let recipient = Address::generate(env); - let contract_id = env.register( RustAcademyContract, ()); - let client = RustAcademyContractClient::new(env, &contract_id); - - client.initialize(&admin); - (client, admin, platform_wallet, owner, recipient) -} - -#[test] -fn test_hook_callbacks_do_not_block_escrow_flow() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _platform_wallet, owner, _) = setup(&env); - - let hook_contract_id = env.register(HookStubContract, ()); - client.register_hook(&hook_contract_id); - - let token_admin = Address::generate(&env); - let token_id = env - .register_stellar_asset_contract_v2(token_admin.clone()) - .address(); - let token_client = token::Client::new(&env, &token_id); - let token_admin_client = token::StellarAssetClient::new(&env, &token_id); - token_admin_client.mint(&owner, &10000); - - let salt = Bytes::from_array(&env, &[1; 32]); - let commitment = client.deposit(&token_id, &1000i128, &owner, &salt, &3600, &None); - client.withdraw(&token_id, &1000i128, &commitment, &owner, &salt); - - let hook_client = HookStubContractClient::new(&env, &hook_contract_id); - assert_eq!(hook_client.get_invocations(), 2); - assert_eq!(token_client.balance(&owner), 9999); -} - -#[test] -fn test_reentrant_hook_does_not_break_primary_transaction() { - let env = Env::default(); - env.mock_all_auths(); - let (client, _admin, _platform_wallet, owner, _) = setup(&env); - - let hook_contract_id = env.register(MaliciousHookContract, ()); - let hook_client = MaliciousHookContractClient::new(&env, &hook_contract_id); - hook_client.init(&client.address); - client.register_hook(&hook_contract_id); - - let token_admin = Address::generate(&env); - let token_id = env - .register_stellar_asset_contract_v2(token_admin.clone()) - .address(); - let token_client = token::Client::new(&env, &token_id); - let token_admin_client = token::StellarAssetClient::new(&env, &token_id); - token_admin_client.mint(&owner, &10000); - - let salt = Bytes::from_array(&env, &[2; 32]); - let commitment = client.deposit(&token_id, &1000i128, &owner, &salt, &3600, &None); - client.withdraw(&token_id, &1000i128, &commitment, &owner, &salt); - - assert_eq!(token_client.balance(&owner), 9999); -} - -#[test] -fn test_oracle_dynamic_fee_and_stale_fallback() { - let env = Env::default(); - let (client, admin, platform_wallet, owner, _) = setup(&env); - env.mock_all_auths(); - - let token_admin = Address::generate(&env); - let token_id = env - .register_stellar_asset_contract_v2(token_admin.clone()) - .address(); - let token_client = token::Client::new(&env, &token_id); - let token_admin_client = token::StellarAssetClient::new(&env, &token_id); - token_admin_client.mint(&owner, &10000); - - client.set_platform_wallet(&admin, &platform_wallet); - client.set_fee_config(&admin, &FeeConfig { fee_bps: 1000 }); - - let oracle_id = env.register(MockOracleContract, ()); - let oracle_client = MockOracleContractClient::new(&env, &oracle_id); - oracle_client.set_price(&1_000_000i128, &1000u64); - - client.set_oracle_fee_config( - &admin, - &OracleFeeConfig { - oracle: oracle_id.clone(), - usd_fee_micros: 1_000_000, - stale_threshold_secs: 1000, - }, - ); - - let salt = Bytes::from_array(&env, &[3; 32]); - let commitment = client.deposit(&token_id, &1000i128, &owner, &salt, &3600, &None); - client.withdraw(&token_id, &1000i128, &commitment, &owner, &salt); - - assert_eq!(token_client.balance(&owner), 9999); - assert_eq!(token_client.balance(&platform_wallet), 1); - - // Make oracle data stale and verify fallback to static basis points. - env.ledger().with_mut(|li| li.timestamp = 3000); - oracle_client.set_price(&1_000_000i128, &1000u64); - - let salt2 = Bytes::from_array(&env, &[4; 32]); - let commitment2 = client.deposit(&token_id, &1000i128, &owner, &salt2, &3600, &None); - client.withdraw(&token_id, &1000i128, &commitment2, &owner, &salt2); - - assert_eq!(token_client.balance(&owner), 9999 + 900); - assert_eq!(token_client.balance(&platform_wallet), 1 + 100); -} diff --git a/app/contract/contracts/Folder/src/lib.rs b/app/contract/contracts/Folder/src/lib.rs index cad876ecb..e96b381a2 100644 --- a/app/contract/contracts/Folder/src/lib.rs +++ b/app/contract/contracts/Folder/src/lib.rs @@ -45,7 +45,7 @@ mod types; #[cfg(test)] mod upgrade_test; -use errors:: RustAcademyError; +use errors::RustAcademyError; use storage::*; use types::{ DeploymentMetadata, EscrowEntry, EscrowStatus, FeeConfig, OracleFeeConfig, @@ -81,11 +81,11 @@ pub use types::FeeRatio; /// Disputed --> Refunded: resolve_dispute() [arbiter decides for owner] /// ``` #[contract] -pub struct RustAcademyContract; +pub struct RustAcademyContract; #[contractimpl] #[allow(clippy::too_many_arguments)] -impl RustAcademyContract { +impl RustAcademyContract { /// Withdraw escrowed funds by proving commitment ownership. /// /// The caller (`to`) must authorize; the commitment is recomputed from `to`, `amount`, and `salt` @@ -114,12 +114,12 @@ impl RustAcademyContract { _commitment: BytesN<32>, to: Address, salt: Bytes, - ) -> Result { + ) -> Result { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Withdrawal) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } hook::assert_not_reentrant(&env)?; escrow::withdraw(&env, amount, to, salt) @@ -172,7 +172,7 @@ impl RustAcademyContract { /// # Errors /// * `ContractPaused` - Contract is currently paused /// * `PrivacyAlreadySet` - Privacy state is already at the requested value - pub fn set_privacy(env: Env, owner: Address, enabled: bool) -> Result<(), RustAcademyError> { + pub fn set_privacy(env: Env, owner: Address, enabled: bool) -> Result<(), RustAcademyError> { admin::require_initialized(&env)?; privacy::set_privacy(&env, owner, enabled) } @@ -215,15 +215,15 @@ impl RustAcademyContract { salt: Bytes, timeout_secs: u64, arbiter: Option
, - ) -> Result, RustAcademyError> { + ) -> Result, RustAcademyError> { if storage::is_emergency_mode(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Deposit) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } hook::assert_not_reentrant(&env)?; escrow::deposit(&env, token, amount, owner, salt, timeout_secs, arbiter) @@ -247,7 +247,7 @@ impl RustAcademyContract { salt: Bytes, timeout_secs: u64, arbiter: Option
, - ) -> Result, RustAcademyError> { + ) -> Result, RustAcademyError> { escrow_id::derive_escrow_id(&env, &token, amount, &owner, &salt, timeout_secs, &arbiter) } @@ -278,7 +278,7 @@ impl RustAcademyContract { owner: Address, amount: i128, salt: Bytes, - ) -> Result, RustAcademyError> { + ) -> Result, RustAcademyError> { commitment::create_amount_commitment(&env, owner, amount, salt) } @@ -350,15 +350,15 @@ impl RustAcademyContract { commitment: BytesN<32>, timeout_secs: u64, arbiter: Option
, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { if storage::is_emergency_mode(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::DepositWithCommitment) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } hook::assert_not_reentrant(&env)?; escrow::deposit_with_commitment( @@ -372,11 +372,11 @@ impl RustAcademyContract { ) } /// Activate emergency mode (irreversible). Only admin can call. Emits event. - pub fn activate_emergency_mode(env: Env, caller: Address) -> Result<(), RustAcademyError> { + pub fn activate_emergency_mode(env: Env, caller: Address) -> Result<(), RustAcademyError> { // Only admin can activate - let admin = get_admin(&env).ok_or( RustAcademyError::Unauthorized)?; + let admin = get_admin(&env).ok_or(RustAcademyError::Unauthorized)?; if caller != admin { - return Err( RustAcademyError::Unauthorized); + return Err(RustAcademyError::Unauthorized); } if storage::is_emergency_mode(&env) { return Ok(()); // Already set @@ -416,12 +416,12 @@ impl RustAcademyContract { salt: Bytes, timeout_secs: u64, arbiter: Option
, - ) -> Result, RustAcademyError> { + ) -> Result, RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Deposit) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } hook::assert_not_reentrant(&env)?; escrow::deposit_partial( @@ -459,12 +459,12 @@ impl RustAcademyContract { commitment: BytesN<32>, payer: Address, payment_amount: i128, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Deposit) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } hook::assert_not_reentrant(&env)?; escrow::partial_payment(&env, commitment, payer, payment_amount) @@ -485,12 +485,16 @@ impl RustAcademyContract { /// * `AlreadySpent` - Escrow is already in a terminal state /// * `EscrowNotExpired` - Escrow has no expiry or has not yet expired /// * `InvalidOwner` - Caller is not the original owner - pub fn refund(env: Env, commitment: BytesN<32>, caller: Address) -> Result<(), RustAcademyError> { + pub fn refund( + env: Env, + commitment: BytesN<32>, + caller: Address, + ) -> Result<(), RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Refund) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } hook::assert_not_reentrant(&env)?; @@ -500,7 +504,7 @@ impl RustAcademyContract { /// Cleanup terminal escrow entries to reclaim storage deposits. /// /// Only escrows in `Spent` or `Refunded` status can be removed. - pub fn cleanup_escrow(env: Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> { + pub fn cleanup_escrow(env: Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> { admin::require_initialized(&env)?; escrow::cleanup_escrow(&env, commitment) } @@ -508,7 +512,7 @@ impl RustAcademyContract { /// Extend the storage TTL of an escrow record. /// /// Any user can call this to keep an escrow from being archived. - pub fn extend_escrow_ttl(env: Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> { + pub fn extend_escrow_ttl(env: Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> { admin::require_initialized(&env)?; escrow::extend_escrow_ttl(&env, commitment) } @@ -526,9 +530,9 @@ impl RustAcademyContract { /// * `CommitmentNotFound` - No escrow exists for the commitment /// * `NoArbiter` - No arbiter assigned to the escrow /// * `InvalidDisputeState` - Escrow is not in `Pending` status - pub fn dispute(env: Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> { + pub fn dispute(env: Env, commitment: BytesN<32>) -> Result<(), RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } hook::assert_not_reentrant(&env)?; escrow::dispute(&env, commitment) @@ -556,9 +560,9 @@ impl RustAcademyContract { commitment: BytesN<32>, resolve_for_owner: bool, recipient: Address, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } hook::assert_not_reentrant(&env)?; escrow::resolve_dispute(&env, caller, commitment, resolve_for_owner, recipient) @@ -585,9 +589,9 @@ impl RustAcademyContract { caller: Address, commitment: BytesN<32>, resolve_for_owner: bool, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } hook::assert_not_reentrant(&env)?; escrow::vote_for_dispute(&env, caller, commitment, resolve_for_owner) @@ -611,9 +615,9 @@ impl RustAcademyContract { env: Env, commitment: BytesN<32>, recipient: Address, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } hook::assert_not_reentrant(&env)?; escrow::resolve_dispute_multi_sig(&env, commitment, recipient) @@ -629,7 +633,7 @@ impl RustAcademyContract { /// /// # Errors /// * `AlreadyInitialized` - Contract has already been initialized - pub fn initialize(env: Env, admin: Address) -> Result<(), RustAcademyError> { + pub fn initialize(env: Env, admin: Address) -> Result<(), RustAcademyError> { admin::initialize(&env, admin) } @@ -665,7 +669,7 @@ impl RustAcademyContract { /// /// This entrypoint is intended to be called immediately after upgrading the contract WASM /// whenever the new release introduces storage or schema changes. - pub fn migrate(env: Env, caller: Address) -> Result { + pub fn migrate(env: Env, caller: Address) -> Result { admin::migrate(&env, &caller) } @@ -680,9 +684,9 @@ impl RustAcademyContract { /// /// # Errors /// * `Unauthorized` - Caller is not the admin, or admin not set - pub fn set_paused(env: Env, caller: Address, new_state: bool) -> Result<(), RustAcademyError> { + pub fn set_paused(env: Env, caller: Address, new_state: bool) -> Result<(), RustAcademyError> { if storage::is_emergency_mode(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } admin::set_paused(&env, caller, new_state) } @@ -705,9 +709,9 @@ impl RustAcademyContract { /// /// # Errors /// * `Unauthorized` - Caller is not the admin, or admin not set - pub fn pause_features(env: Env, caller: Address, mask: u64) -> Result<(), RustAcademyError> { + pub fn pause_features(env: Env, caller: Address, mask: u64) -> Result<(), RustAcademyError> { if storage::is_emergency_mode(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } admin::set_pause_flags(&env, &caller, mask, 0) } @@ -722,9 +726,9 @@ impl RustAcademyContract { /// /// # Errors /// * `Unauthorized` - Caller is not the admin, or admin not set - pub fn unpause_features(env: Env, caller: Address, mask: u64) -> Result<(), RustAcademyError> { + pub fn unpause_features(env: Env, caller: Address, mask: u64) -> Result<(), RustAcademyError> { if storage::is_emergency_mode(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } admin::set_pause_flags(&env, &caller, 0, mask) } @@ -740,13 +744,36 @@ impl RustAcademyContract { /// /// # Errors /// * `Unauthorized` - Caller is not the admin, or admin not set - pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), RustAcademyError> { + pub fn set_admin( + env: Env, + caller: Address, + new_admin: Address, + ) -> Result<(), RustAcademyError> { if storage::is_emergency_mode(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } admin::set_admin(&env, caller, new_admin) } + /// Propose a new primary admin address (**Admin only**). + pub fn propose_admin_transfer( + env: Env, + caller: Address, + new_admin: Address, + ) -> Result<(), RustAcademyError> { + admin::propose_admin_transfer(&env, caller, new_admin) + } + + /// Accept a pending admin transfer (**pending admin only**). + pub fn accept_admin_transfer(env: Env, caller: Address) -> Result<(), RustAcademyError> { + admin::accept_admin_transfer(&env, caller) + } + + /// Cancel a pending admin transfer (**Admin only**). + pub fn cancel_admin_transfer(env: Env, caller: Address) -> Result<(), RustAcademyError> { + admin::cancel_admin_transfer(&env, caller) + } + /// Check if the contract is currently paused. /// /// Returns `true` if paused, `false` otherwise. @@ -767,14 +794,14 @@ impl RustAcademyContract { } /// Register an external hook contract to receive escrow lifecycle callbacks. - pub fn register_hook(env: Env, hook_contract: Address) -> Result<(), RustAcademyError> { + pub fn register_hook(env: Env, hook_contract: Address) -> Result<(), RustAcademyError> { admin::require_initialized(&env)?; hook::assert_not_reentrant(&env)?; hook::register_hook(&env, hook_contract) } /// Unregister a hook contract. - pub fn unregister_hook(env: Env, hook_contract: Address) -> Result<(), RustAcademyError> { + pub fn unregister_hook(env: Env, hook_contract: Address) -> Result<(), RustAcademyError> { admin::require_initialized(&env)?; hook::assert_not_reentrant(&env)?; hook::unregister_hook(&env, hook_contract) @@ -790,7 +817,7 @@ impl RustAcademyContract { env: Env, caller: Address, config: FeeConfig, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { hook::assert_not_reentrant(&env)?; admin::set_fee_config(&env, &caller, config) } @@ -801,7 +828,7 @@ impl RustAcademyContract { caller: Address, token: Address, config: PerAssetFeeConfig, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { hook::assert_not_reentrant(&env)?; admin::set_per_asset_fee(&env, &caller, token, config) } @@ -816,7 +843,7 @@ impl RustAcademyContract { env: Env, caller: Address, config: OracleFeeConfig, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { hook::assert_not_reentrant(&env)?; admin::set_oracle_fee_config(&env, &caller, config) } @@ -836,7 +863,7 @@ impl RustAcademyContract { env: Env, caller: Address, wallet: Address, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { hook::assert_not_reentrant(&env)?; admin::set_platform_wallet(&env, &caller, wallet) } @@ -846,7 +873,7 @@ impl RustAcademyContract { env: Env, caller: Address, new_collector: Address, - ) -> Result { + ) -> Result { hook::assert_not_reentrant(&env)?; admin::rotate_fee_collector(&env, &caller, new_collector) } @@ -988,12 +1015,12 @@ impl RustAcademyContract { pub fn register_ephemeral_key( env: Env, params: StealthDepositParams, - ) -> Result, RustAcademyError> { + ) -> Result, RustAcademyError> { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Deposit) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } stealth::register_ephemeral_key(&env, params) } @@ -1025,12 +1052,12 @@ impl RustAcademyContract { eph_pub: BytesN<32>, spend_pub: BytesN<32>, stealth_address: BytesN<32>, - ) -> Result { + ) -> Result { if admin::is_paused(&env) { - return Err( RustAcademyError::ContractPaused); + return Err(RustAcademyError::ContractPaused); } if is_feature_paused(&env, PauseFlag::Withdrawal) { - return Err( RustAcademyError::OperationPaused); + return Err(RustAcademyError::OperationPaused); } stealth::stealth_withdraw(&env, recipient, eph_pub, spend_pub, stealth_address) } @@ -1067,7 +1094,7 @@ impl RustAcademyContract { env: Env, caller: Address, new_wasm_hash: BytesN<32>, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { admin::upgrade(&env, &caller, new_wasm_hash) } @@ -1090,7 +1117,7 @@ impl RustAcademyContract { caller: Address, start: u64, end: u64, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { admin::set_upgrade_window(&env, &caller, start, end) } @@ -1122,12 +1149,12 @@ impl RustAcademyContract { caller: Address, new_version: u32, new_wasm_hash: BytesN<32>, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { admin::start_upgrade(&env, &caller, new_version, new_wasm_hash) } /// Cancel a pending upgrade and clear gating state (**Admin only**). - pub fn cancel_upgrade(env: Env, caller: Address) -> Result<(), RustAcademyError> { + pub fn cancel_upgrade(env: Env, caller: Address) -> Result<(), RustAcademyError> { admin::cancel_upgrade(&env, &caller) } @@ -1150,7 +1177,7 @@ impl RustAcademyContract { env: Env, caller: Address, new_version: u32, - ) -> Result { + ) -> Result { admin::complete_upgrade(&env, &caller, new_version) } @@ -1164,7 +1191,7 @@ impl RustAcademyContract { caller: Address, target: Address, role: Role, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { admin::grant_role(&env, caller, target, role) } @@ -1174,12 +1201,22 @@ impl RustAcademyContract { caller: Address, target: Address, role: Role, - ) -> Result<(), RustAcademyError> { + ) -> Result<(), RustAcademyError> { admin::revoke_role(&env, caller, target, role) } + /// Remove all roles from an account. + pub fn clear_roles(env: Env, caller: Address, target: Address) -> Result<(), RustAcademyError> { + admin::clear_roles(&env, caller, target) + } + /// Get all roles assigned to an account. pub fn get_roles(env: Env, account: Address) -> Vec { storage::get_roles(&env, &account) } + + /// Get the pending admin transfer target, if any. + pub fn get_pending_admin_transfer(env: Env) -> Option
{ + storage::get_pending_admin_transfer(&env) + } } diff --git a/app/contract/contracts/Folder/src/role_test.rs b/app/contract/contracts/Folder/src/role_test.rs index 50ce8b31c..745acc977 100644 --- a/app/contract/contracts/Folder/src/role_test.rs +++ b/app/contract/contracts/Folder/src/role_test.rs @@ -1,5 +1,5 @@ -use crate::{errors:: RustAcademyError, test_context::TestContext, types::Role}; -use soroban_sdk::{testutils::Address as _, Address}; +use crate::{errors::RustAcademyError, storage, test_context::TestContext, types::Role}; +use soroban_sdk::{testutils::Address as _, Address, Vec}; #[test] fn test_initial_admin_has_role() { @@ -24,6 +24,82 @@ fn test_grant_and_revoke_role() { assert!(!roles.contains(Role::Operator)); } +#[test] +fn test_admin_transfer_requires_acceptance_and_can_be_cancelled() { + let ctx = TestContext::with_admin(); + let pending_admin = ctx.bob.clone(); + + ctx.client + .propose_admin_transfer(&ctx.admin, &pending_admin); + assert_eq!( + ctx.client.get_pending_admin_transfer(), + Some(pending_admin.clone()) + ); + + ctx.client.cancel_admin_transfer(&ctx.admin); + assert_eq!(ctx.client.get_pending_admin_transfer(), None); + + let cancelled_accept = ctx.client.try_accept_admin_transfer(&pending_admin); + assert!(matches!( + cancelled_accept, + Err(Ok(RustAcademyError::NoPendingAdminTransfer)) + )); + + ctx.client + .propose_admin_transfer(&ctx.admin, &pending_admin); + ctx.client.accept_admin_transfer(&pending_admin); + + assert_eq!(ctx.client.get_admin(), Some(pending_admin.clone())); + assert!(ctx.client.get_roles(&pending_admin).contains(Role::Admin)); + assert!(!ctx.client.get_roles(&ctx.admin).contains(Role::Admin)); +} + +#[test] +fn test_clear_roles_preserves_current_admin_role() { + let ctx = TestContext::with_admin(); + + ctx.client + .grant_role(&ctx.admin, &ctx.admin, &Role::Operator); + ctx.client + .grant_role(&ctx.admin, &ctx.admin, &Role::Arbiter); + + ctx.client.clear_roles(&ctx.admin, &ctx.admin); + + let roles = ctx.client.get_roles(&ctx.admin); + assert!(roles.contains(Role::Admin)); + assert!(!roles.contains(Role::Operator)); + assert!(!roles.contains(Role::Arbiter)); +} + +#[test] +fn test_cannot_revoke_admin_role_from_current_admin() { + let ctx = TestContext::with_admin(); + + let result = ctx + .client + .try_revoke_role(&ctx.admin, &ctx.admin, &Role::Admin); + assert!(matches!( + result, + Err(Ok(RustAcademyError::InvalidRoleState)) + )); +} + +#[test] +fn test_corrupt_admin_role_state_blocks_public_calls() { + let ctx = TestContext::with_admin(); + + ctx.env.as_contract(&ctx.client.address, || { + let roles = Vec::new(&ctx.env); + storage::set_roles(&ctx.env, &ctx.admin, &roles); + }); + + let result = ctx.client.try_set_paused(&ctx.admin, &true); + assert!(matches!( + result, + Err(Ok(RustAcademyError::InvalidRoleState)) + )); +} + #[test] fn test_unauthorized_grant_fails() { let ctx = TestContext::with_admin(); @@ -97,7 +173,7 @@ fn test_insufficient_role_error() { .try_set_fee_config(&ctx.alice, &crate::types::FeeConfig { fee_bps: 100 }); match res { - Err(Ok( RustAcademyError::InsufficientRole)) => (), + Err(Ok(RustAcademyError::InsufficientRole)) => (), _ => panic!("Expected InsufficientRole error"), } } diff --git a/app/contract/contracts/Folder/src/storage.rs b/app/contract/contracts/Folder/src/storage.rs index e5aa7de08..81f8e34ec 100644 --- a/app/contract/contracts/Folder/src/storage.rs +++ b/app/contract/contracts/Folder/src/storage.rs @@ -39,8 +39,6 @@ //! - **Value layout**: Changing `EscrowEntry` fields may require migration logic; adding optional //! fields can be done carefully with defaults. - - use soroban_sdk::{contracttype, Address, Bytes, BytesN, Env, Vec}; use crate::types::{DisputeVote, EscrowEntry, FeeConfig, Role, StealthEscrowEntry}; @@ -133,6 +131,8 @@ pub enum DataKey { ContractVersion, /// Admin address (singleton). Admin, + /// Pending admin transfer target (singleton). + PendingAdminTransfer, /// Explicit one-time initialization flag (singleton). Initialized, /// Paused state (singleton). @@ -454,6 +454,25 @@ pub fn get_admin(env: &Env) -> Option
{ env.storage().persistent().get(&key) } +/// Set the pending admin transfer target. +pub fn set_pending_admin_transfer(env: &Env, pending_admin: &Address) { + let key = DataKey::PendingAdminTransfer; + env.storage().persistent().set(&key, pending_admin); +} + +/// Get the pending admin transfer target. +pub fn get_pending_admin_transfer(env: &Env) -> Option
{ + let key = DataKey::PendingAdminTransfer; + env.storage().persistent().get(&key) +} + +/// Clear any pending admin transfer target. +pub fn clear_pending_admin_transfer(env: &Env) { + env.storage() + .persistent() + .remove(&DataKey::PendingAdminTransfer); +} + // ----------------------------------------------------------------------------- // TTL Helper // ----------------------------------------------------------------------------- diff --git a/app/frontend/src/lib/i18n.ts.backup b/app/frontend/src/lib/i18n.ts.backup deleted file mode 100644 index b8bf99c6e..000000000 --- a/app/frontend/src/lib/i18n.ts.backup +++ /dev/null @@ -1,619 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -i18n - .use(initReactI18next) - .init({ - lng: 'en', - fallbackLng: 'en', - resources: { - en: { - translation: { - // Navigation - dashboard: 'Dashboard', - linkGenerator: 'Link Generator', - settings: 'Settings', - profileSettings: 'Profile Settings', - services: 'Services', - settingsTitle: 'Settings', - profileCustomization: 'Profile Customization', - profileCustomizationDescription: 'Customize your public payment page at RustAcademy.to/{{username}}', - generalTab: 'General', - developerTab: 'Developer', - themeSettings: 'Theme Settings', - primaryColor: 'Primary Color', - avatarUrl: 'Avatar URL', - bioLabel: 'Bio (max 160 characters)', - socialLinks: 'Social Links', - twitterHandleLabel: 'Twitter/X Handle', - discordUsernameLabel: 'Discord Username', - githubHandleLabel: 'GitHub Handle', - languageLabel: 'Language', - changeLanguage: 'Change language for app text and formatting', - saveChanges: 'Save Changes', - preview: 'Preview', - show: 'Show', - hide: 'Hide', - livePreview: 'Live Preview', - - // Generator page - createPayment: 'Create a payment', - requestInstantly: 'request instantly.', - advancedModeDescription: 'Advanced mode supports path payments: choose what you receive and let payers settle in multiple assets.', - amountLabel: 'Amount (recipient receives)', - amountPlaceholder: '0.00', - loadingAssets: 'Loading assets…', - destinationLabel: 'Destination', - destinationPlaceholder: 'Receiver public key', - memoLabel: 'Memo (optional)', - memoPlaceholder: "What's this payment for?", - advancedSettings: 'Advanced settings', - hide: 'Hide', - show: 'Show', - recipientAsset: 'Recipient asset', - recipientAssetDescription: 'Same as the amount currency above — what lands in the receiver\'s account after the path executes.', - allowedSourceAssets: 'Allowed source assets (payers)', - allowedSourceAssetsDescription: 'Payers may use any of the selected assets; Horizon suggests paths and send amounts.', - pathPreview: 'Path preview', - fetchingEstimates: 'Fetching estimates…', - noPathsFound: 'No paths found for this combination on {{horizonUrl}}. Try other source assets or a smaller amount.', - payReceive: 'Pay {{sourceAmount}} ({{sourceAsset}}) → receive {{destinationAmount}} ({{destinationAsset}})', - hops: 'Hops: {{hopCount}}', - sorobanPreflight: 'Soroban preflight (composer)', - sorobanPreflightDescription: 'Runs the same simulation as POST /transactions/compose with health_check on RustAcademy_CONTRACT_ID.', - sourceAccountPlaceholder: 'Source account G… (funded, for sequence)', - simulating: 'Simulating…', - runPreflight: 'Run preflight simulation', - simulationOk: 'Simulation OK — fees estimated.', - totalFee: 'Total fee (incl. resource): {{totalFee}} XLM', - latency: 'Latency {{latency}} ms', - simulationFailed: 'Simulation failed', - - // Errors - amountRequired: 'Amount is required.', - enterValidNumber: 'Enter a valid number.', - destinationRequired: 'Destination address is required.', - selectRecipientAsset: 'Select a recipient asset.', - couldNotLoadAssets: 'Could not load verified assets.', - invalidPublicKey: 'Enter a valid 56-character Stellar public key (G…).', - preflightUnavailable: 'Soroban preflight is not configured on this server.', - preflightFailed: 'Preflight request failed.', - networkError: 'Network error calling preflight.', - requestFailed: 'Request failed', - - // Footer - copyright: '© 2026 RustAcademy Platform. Built by Pulsefy.', - github: 'GitHub', - terms: 'Terms', - privacy: 'Privacy', - - // Home page - heroTitle: 'Privacy-focused
payments on Stellar.', - heroSubtitle: 'Create unique, shareable usernames and generate instant payment requests for USDC or XLM. Powered by Soroban smart contracts for shielded transactions.', - generateLink: 'Generate Link', - goToDashboard: 'Go to Dashboard', - shareableUsernames: 'Shareable Usernames', - shareableUsernamesDesc: 'Claim your unique name like RustAcademy.to/alex and receive payments easily.', - instantPayments: 'Instant Payments', - instantPaymentsDesc: 'Generate payment links instantly with advanced path payment support.', - shieldedTransactions: 'Shielded Transactions', - shieldedTransactionsDesc: 'Privacy-preserving payments powered by Soroban smart contracts.', - - // Dashboard - loadingDashboard: 'Loading dashboard...', - noTransactionsYet: 'No transactions yet. Create your first payment link!', - extendTTL: 'Storage TTL extended for 6 months!', - cleanupDeposit: 'Storage deposit reclaimed and record cleaned up!', - dashboardTitle: 'Dashboard', - recentActivity: 'Recent Activity', - marketplaceActivity: 'Marketplace Activity', - yourBids: 'Your Bids', - yourListings: 'Your Listings', - noBids: 'No active bids', - noListings: 'No active listings', - endsIn: 'Ends in', - currentBid: 'Current Bid', - placeBid: 'Place Bid', - viewListing: 'View Listing', - - // Dashboard continued - welcomeBack: 'Welcome back.', - paymentsScaling: 'Your global payments are scaling beautifully.', - withdrawFunds: 'Withdraw Funds', - totalRevenue: 'Total Revenue', - successRate: 'Success Rate', - payoutPending: 'Payout Pending', - recentActivity: 'Recent Activity', - marketplaceActivity: 'Marketplace Activity', - yourBids: 'Your Bids', - yourListings: 'Your Listings', - noBids: 'No active bids', - noListings: 'No active listings', - - // Dashboard table - availablePayout: 'Available Payout', - estimatedSettlement: 'Estimated settlement: 3 seconds', - activityFeed: 'Activity Feed', - syncedWithHorizon: 'Synced with Stellar Horizon API', - last30Days: 'Last 30 Days', - yearly: 'Yearly', - transactionId: 'Transaction ID', - asset: 'Asset', - memoStatus: 'Memo / Status', - timestamp: 'Timestamp', - actions: 'Actions', - } - }, - es: { - translation: { - // Navigation - dashboard: 'Panel de Control', - linkGenerator: 'Generador de Enlaces', - settings: 'Configuración', - profileSettings: 'Configuración de Perfil', - services: 'Servicios', - settingsTitle: 'Configuración', - profileCustomization: 'Personalización de Perfil', - profileCustomizationDescription: 'Personaliza tu página de pago público en RustAcademy.to/{{username}}', - generalTab: 'General', - developerTab: 'Desarrollador', - themeSettings: 'Configuración de Tema', - primaryColor: 'Color Primario', - avatarUrl: 'URL del Avatar', - bioLabel: 'Biografía (máximo 160 caracteres)', - socialLinks: 'Enlaces Sociales', - twitterHandleLabel: 'Usuario de Twitter/X', - discordUsernameLabel: 'Usuario de Discord', - githubHandleLabel: 'Usuario de GitHub', - languageLabel: 'Idioma', - changeLanguage: 'Cambiar idioma para el texto y formato de la aplicación', - saveChanges: 'Guardar Cambios', - preview: 'Vista Previa', - show: 'Mostrar', - hide: 'Ocultar', - livePreview: 'Vista Previa en Vivo', - - // Generator page - createPayment: 'Crear un pago', - requestInstantly: 'solicitud al instante.', - advancedModeDescription: 'El modo avanzado soporta pagos de ruta: elige lo que recibes y deja que los pagadores se liquiden en múltiples activos.', - amountLabel: 'Cantidad (recibe el destinatario)', - amountPlaceholder: '0.00', - loadingAssets: 'Cargando activos…', - destinationLabel: 'Destino', - destinationPlaceholder: 'Clave pública del receptor', - memoLabel: 'Memo (opcional)', - memoPlaceholder: '¿Para qué es este pago?', - advancedSettings: 'Configuración avanzada', - recipientAsset: 'Activo del destinatario', - recipientAssetDescription: 'Igual a la moneda de la cantidad anterior — lo que llega a la cuenta del receptor después de que se ejecute la ruta.', - allowedSourceAssets: 'Activos fuente permitidos (pagadores)', - allowedSourceAssetsDescription: 'Los pagadores pueden usar cualquiera de los activos seleccionados; Horizon sugiere rutas y envía cantidades.', - pathPreview: 'Vista previa de ruta', - fetchingEstimates: 'Obteniendo estimaciones…', - noPathsFound: 'No se encontraron rutas para esta combinación en {{horizonUrl}}. Prueba otros activos fuente o una cantidad menor.', - payReceive: 'Pagar {{sourceAmount}} ({{sourceAsset}}) → recibir {{destinationAmount}} ({{destinationAsset}})', - hops: 'Saltos: {{hopCount}}', - sorobanPreflight: 'Preflight de Soroban (compositor)', - sorobanPreflightDescription: 'Ejecuta la misma simulación que POST /transactions/compose con health_check en RustAcademy_CONTRACT_ID.', - sourceAccountPlaceholder: 'Cuenta fuente G… (financiada, para secuencia)', - simulating: 'Simulando…', - runPreflight: 'Ejecutar simulación de preflight', - simulationOk: 'Simulación OK — tarifas estimadas.', - totalFee: 'Tarifa total (incl. recurso): {{totalFee}} XLM', - latency: 'Latencia {{latency}} ms', - simulationFailed: 'Simulación fallida', - - // Errors - amountRequired: 'La cantidad es requerida.', - enterValidNumber: 'Ingresa un número válido.', - destinationRequired: 'La dirección de destino es requerida.', - selectRecipientAsset: 'Selecciona un activo del destinatario.', - couldNotLoadAssets: 'No se pudieron cargar los activos verificados.', - invalidPublicKey: 'Ingresa una clave pública Stellar válida de 56 caracteres (G…).', - preflightUnavailable: 'El preflight de Soroban no está configurado en este servidor.', - preflightFailed: 'La solicitud de preflight falló.', - networkError: 'Error de red llamando a preflight.', - requestFailed: 'Solicitud fallida', - - // Footer - copyright: '© 2026 Plataforma RustAcademy. Construida por Pulsefy.', - github: 'GitHub', - terms: 'Términos', - privacy: 'Privacidad', - - // Home page - heroTitle: 'Pagos enfocados en la privacidad
en Stellar.', - heroSubtitle: 'Crea nombres de usuario únicos y compartibles, y genera solicitudes de pago instantáneas para USDC o XLM. Impulsado por contratos inteligentes Soroban para transacciones blindadas.', - generateLink: 'Generar Enlace', - goToDashboard: 'Ir al Panel de Control', - shareableUsernames: 'Nombres de Usuario Compartibles', - shareableUsernamesDesc: 'Reclama tu nombre único como RustAcademy.to/alex y recibe pagos fácilmente.', - instantPayments: 'Pagos Instantáneos', - instantPaymentsDesc: 'Genera enlaces de pago al instante con soporte avanzado para pagos de ruta.', - shieldedTransactions: 'Transacciones Blindadas', - shieldedTransactionsDesc: 'Pagos que preservan la privacidad impulsados por contratos inteligentes Soroban.', - - // Dashboard - loadingDashboard: 'Cargando panel de control...', - noTransactionsYet: '¡Aún no hay transacciones. Crea tu primer enlace de pago!', - extendTTL: '¡TTL de almacenamiento extendido por 6 meses!', - cleanupDeposit: '¡Depósito de almacenamiento reclamado y registro limpiado!', - dashboardTitle: 'Panel de Control', - recentActivity: 'Actividad Reciente', - marketplaceActivity: 'Actividad del Mercado', - yourBids: 'Tus Ofertas', - yourListings: 'Tus Listados', - noBids: 'Sin ofertas activas', - noListings: 'Sin listados activos', - endsIn: 'Termina en', - currentBid: 'Oferta Actual', - placeBid: 'Hacer Oferta', - viewListing: 'Ver Listado', - - // Dashboard continued - welcomeBack: 'Bienvenido de vuelta.', - paymentsScaling: 'Tus pagos globales están escalando maravillosamente.', - withdrawFunds: 'Retirar Fondos', - totalRevenue: 'Ingresos Totales', - successRate: 'Tasa de Éxito', - payoutPending: 'Pago Pendiente', - recentActivity: 'Actividad Reciente', - marketplaceActivity: 'Actividad del Mercado', - yourBids: 'Tus Ofertas', - yourListings: 'Tus Listados', - noBids: 'Sin ofertas activas', - noListings: 'Sin listados activos', - - // Dashboard table - availablePayout: 'Pago Disponible', - estimatedSettlement: 'Liquidación estimada: 3 segundos', - activityFeed: 'Feed de Actividad', - syncedWithHorizon: 'Sincronizado con Stellar Horizon API', - last30Days: 'Últimos 30 Días', - yearly: 'Anual', - transactionId: 'ID de Transacción', - asset: 'Activo', - memoStatus: 'Memo / Estado', - timestamp: 'Marca de Tiempo', - actions: 'Acciones', - } - }, - fr: { - translation: { - // Navigation - dashboard: 'Tableau de Bord', - linkGenerator: 'Générateur de Liens', - settings: 'Paramètres', - profileSettings: 'Paramètres de Profil', - services: 'Services', - settingsTitle: 'Paramètres', - profileCustomization: 'Personnalisation du Profil', - profileCustomizationDescription: 'Personnalisez votre page de paiement publique sur RustAcademy.to/{{username}}', - generalTab: 'Général', - developerTab: 'Développeur', - themeSettings: 'Paramètres de Thème', - primaryColor: 'Couleur Principale', - avatarUrl: 'URL de l\'Avatar', - bioLabel: 'Biographie (maximum 160 caractères)', - socialLinks: 'Liens Sociaux', - twitterHandleLabel: 'Nom d\'utilisateur Twitter/X', - discordUsernameLabel: 'Nom d\'utilisateur Discord', - githubHandleLabel: 'Nom d\'utilisateur GitHub', - languageLabel: 'Langue', - changeLanguage: 'Changer la langue pour le texte et le formatage de l\'application', - saveChanges: 'Sauvegarder les Modifications', - preview: 'Aperçu', - show: 'Afficher', - hide: 'Masquer', - livePreview: 'Aperçu en Direct', - - // Generator page - createPayment: 'Créer un paiement', - requestInstantly: 'demande instantanément.', - advancedModeDescription: 'Le mode avancé prend en charge les paiements de chemin : choisissez ce que vous recevez et laissez les payeurs se liquider dans plusieurs actifs.', - amountLabel: 'Montant (reçu par le destinataire)', - amountPlaceholder: '0.00', - loadingAssets: 'Chargement des actifs…', - destinationLabel: 'Destination', - destinationPlaceholder: 'Clé publique du destinataire', - memoLabel: 'Mémo (optionnel)', - memoPlaceholder: 'À quoi sert ce paiement ?', - advancedSettings: 'Paramètres avancés', - recipientAsset: 'Actif du destinataire', - recipientAssetDescription: 'Identique à la devise du montant ci-dessus — ce qui arrive sur le compte du destinataire après l\'exécution du chemin.', - allowedSourceAssets: 'Actifs source autorisés (payeurs)', - allowedSourceAssetsDescription: 'Les payeurs peuvent utiliser n\'importe quel actif sélectionné ; Horizon suggère des chemins et envoie des montants.', - pathPreview: 'Aperçu du chemin', - fetchingEstimates: 'Récupération des estimations…', - noPathsFound: 'Aucun chemin trouvé pour cette combinaison sur {{horizonUrl}}. Essayez d\'autres actifs source ou un montant plus petit.', - payReceive: 'Payer {{sourceAmount}} ({{sourceAsset}}) → recevoir {{destinationAmount}} ({{destinationAsset}})', - hops: 'Sauts : {{hopCount}}', - sorobanPreflight: 'Pré-vol Soroban (compositeur)', - sorobanPreflightDescription: 'Exécute la même simulation que POST /transactions/compose avec health_check sur RustAcademy_CONTRACT_ID.', - sourceAccountPlaceholder: 'Compte source G… (financé, pour séquence)', - simulating: 'Simulation…', - runPreflight: 'Exécuter la simulation de pré-vol', - simulationOk: 'Simulation OK — frais estimés.', - totalFee: 'Frais total (incl. ressource) : {{totalFee}} XLM', - latency: 'Latence {{latency}} ms', - simulationFailed: 'Échec de la simulation', - - - // Dashboard - loadingDashboard: 'Chargement du tableau de bord', - - // Dashboard continued - welcomeBack: 'Bienvenue de retour.', - paymentsScaling: 'Vos paiements mondiaux évoluent magnifiquement.', - withdrawFunds: 'Retirer des Fonds', - totalRevenue: 'Revenus Totaux', - successRate: 'Taux de Réussite', - payoutPending: 'Paiement en Attente', - recentActivity: 'Activité Récente', - marketplaceActivity: 'Activité du Marché', - yourBids: 'Vos Offres', - yourListings: 'Vos Annonces', - noBids: 'Aucune offre active', - noListings: 'Aucune annonce active', - - // Dashboard table - availablePayout: 'Paiement Disponible', - estimatedSettlement: 'Liquidation estimée : 3 secondes', - activityFeed: 'Flux d\'Activité', - syncedWithHorizon: 'Synchronisé avec Stellar Horizon API', - last30Days: '30 Derniers Jours', - yearly: 'Annuel', - transactionId: 'ID de Transaction', - asset: 'Actif', - memoStatus: 'Mémo / Statut', - timestamp: 'Horodatage', - actions: 'Actions', - noTransactionsYet: 'Aucune transaction pour le moment. Créez votre premier lien de paiement !', - extendTTL: 'TTL de stockage étendu de 6 mois !', - cleanupDeposit: 'Dépôt de stockage récupéré et enregistrement nettoyé !', - dashboardTitle: 'Tableau de Bord', - recentActivity: 'Activité Récente', - marketplaceActivity: 'Activité du Marché', - yourBids: 'Vos Offres', - yourListings: 'Vos Annonces', - noBids: 'Aucune offre active', - noListings: 'Aucune annonce active', - endsIn: 'Se termine dans', - currentBid: 'Offre Actuelle', - placeBid: 'Faire une Offre', - viewListing: 'Voir l\'Annonce', - // Errors - amountRequired: 'Le montant est requis.', - enterValidNumber: 'Entrez un nombre valide.', - destinationRequired: 'L\'adresse de destination est requise.', - selectRecipientAsset: 'Sélectionnez un actif destinataire.', - - // Home page - heroTitle: 'Paiements axés sur la confidentialité
sur Stellar.', - heroSubtitle: 'Créez des noms d\'utilisateur uniques et partageables et générez des demandes de paiement instantanées pour USDC ou XLM. Alimenté par des contrats intelligents Soroban pour des transactions blindées.', - generateLink: 'Générer un Lien', - goToDashboard: 'Aller au Tableau de Bord', - shareableUsernames: 'Noms d\'Utilisateur Partageables', - shareableUsernamesDesc: 'Réclamez votre nom unique comme RustAcademy.to/alex et recevez des paiements facilement.', - instantPayments: 'Paiements Instantanés', - instantPaymentsDesc: 'Générez des liens de paiement instantanément avec prise en charge avancée des paiements de chemin.', - shieldedTransactions: 'Transactions Blindées', - shieldedTransactionsDesc: 'Paiements préservant la confidentialité alimentés par des contrats intelligents Soroban.', - couldNotLoadAssets: 'Impossible de charger les actifs vérifiés.', - invalidPublicKey: 'Entrez une clé publique Stellar valide de 56 caractères (G…).', - preflightUnavailable: 'Le pré-vol Soroban n\'est pas configuré sur ce serveur.', - preflightFailed: 'La demande de pré-vol a échoué.', - networkError: 'Erreur réseau lors de l\'appel du pré-vol.', - requestFailed: 'Échec de la demande', - - // Footer - copyright: '© 2026 Plateforme RustAcademy. Construite par Pulsefy.', - github: 'GitHub', - terms: 'Conditions', - privacy: 'Confidentialité', - } - } - /* - es: { - translation: { - // Navigation - dashboard: 'Panel de Control', - linkGenerator: 'Generador de Enlaces', - settings: 'Configuración', - profileSettings: 'Configuración de Perfil', - services: 'Servicios', - settingsTitle: 'Configuración', - profileCustomization: 'Personalización del Perfil', - profileCustomizationDescription: 'Personaliza tu página de pago pública en RustAcademy.to/{{username}}', - generalTab: 'General', - developerTab: 'Desarrollador', - themeSettings: 'Configuración de Tema', - primaryColor: 'Color Primario', - avatarUrl: 'URL del Avatar', - bioLabel: 'Bio (máx 160 caracteres)', - socialLinks: 'Enlaces Sociales', - twitterHandleLabel: 'Usuario de Twitter/X', - discordUsernameLabel: 'Usuario de Discord', - githubHandleLabel: 'Usuario de GitHub', - languageLabel: 'Idioma', - changeLanguage: 'Cambiar idioma para el texto y formato de la app', - saveChanges: 'Guardar Cambios', - preview: 'Vista Previa', - show: 'Mostrar', - hide: 'Ocultar', - livePreview: 'Vista Previa en Vivo', - - // Generator page - createPayment: 'Crear un pago', - requestInstantly: 'solicitar instantáneamente.', - advancedModeDescription: 'El modo avanzado soporta pagos de ruta: elige lo que recibes y deja que los pagadores liquiden en múltiples activos.', - amountLabel: 'Monto (destinatario recibe)', - amountPlaceholder: '0.00', - loadingAssets: 'Cargando activos…', - destinationLabel: 'Destino', - destinationPlaceholder: 'Clave pública del destinatario', - memoLabel: 'Memo (opcional)', - memoPlaceholder: '¿Para qué es este pago?', - advancedSettings: 'Configuración avanzada', - advancedMode: 'Modo Avanzado', - basicMode: 'Modo Básico', - assetLabel: 'Activo', - assetPlaceholder: 'Seleccionar un activo', - pathPayment: 'Pago de Ruta', - pathPaymentDescription: 'Permitir que los pagadores usen cualquier activo para pagar.', - enablePathPayment: 'Habilitar pago de ruta', - fetchingEstimates: 'Obteniendo estimaciones…', - estimatedSettlement: 'Liquidación estimada: 3 segundos', - simulationFailed: 'Fallo en la simulación', - networkError: 'Error de red al llamar preflight.', - requestFailed: 'Fallo en la solicitud', - - // Home page - heroTitle: 'Pagos Globales Instantáneos', - heroSubtitle: 'Genera enlaces de pago en segundos. Acepta pagos en cualquier moneda con Stellar.', - generateLink: 'Generar Enlace', - goToDashboard: 'Ir al Panel de Control', - shareableUsernames: 'Nombres de Usuario Compartibles', - shareableUsernamesDesc: 'Reclama tu nombre único como RustAcademy.to/alex y recibe pagos fácilmente.', - instantPayments: 'Pagos Instantáneos', - instantPaymentsDesc: 'Genera enlaces de pago instantáneamente con soporte avanzado para pagos de ruta.', - shieldedTransactions: 'Transacciones Blindadas', - shieldedTransactionsDesc: 'Pagos que preservan la privacidad impulsados por contratos inteligentes Soroban.', - couldNotLoadAssets: 'No se pudieron cargar los activos verificados.', - invalidPublicKey: 'Ingresa una clave pública Stellar válida de 56 caracteres (G…).', - preflightUnavailable: 'El preflight de Soroban no está configurado en este servidor.', - preflightFailed: 'La solicitud de preflight falló.', - networkError: 'Error de red al llamar preflight.', - requestFailed: 'Fallo en la solicitud', - - // Dashboard - loadingDashboard: 'Cargando panel de control', - welcomeBack: 'Bienvenido de vuelta.', - paymentsScaling: 'Tus pagos globales escalando magníficamente.', - withdrawFunds: 'Retirar Fondos', - totalRevenue: 'Ingresos Totales', - last30Days: 'Últimos 30 Días', - yearly: 'Anual', - transactionId: 'ID de Transacción', - asset: 'Activo', - memoStatus: 'Memo / Estado', - timestamp: 'Marca de Tiempo', - actions: 'Acciones', - noTransactionsYet: 'Aún no hay transacciones. ¡Crea tu primer enlace de pago!', - extendTTL: 'TTL de almacenamiento extendido por 6 meses!', - cleanupDeposit: 'Depósito de almacenamiento recuperado y registro limpiado!', - dashboardTitle: 'Panel de Control', - recentActivity: 'Actividad Reciente', - marketplaceActivity: 'Actividad del Mercado', - yourBids: 'Tus Ofertas', - yourListings: 'Tus Listados', - noBids: 'Sin ofertas activas', - noListings: 'Sin listados activos', - bidAmount: 'Monto de Oferta', - listingPrice: 'Precio de Listado', - currentBid: 'Oferta Actual', - placeBid: 'Hacer Oferta', - buyNow: 'Comprar Ahora', - viewDetails: 'Ver Detalles', - activityFeed: 'Feed de Actividad', - syncedWithHorizon: 'Sincronizado con Stellar Horizon API', - estimatedSettlement: 'Liquidación estimada: 3 segundos', - simulationFailed: 'Fallo en la simulación', - networkError: 'Error de red al llamar preflight.', - requestFailed: 'Fallo en la solicitud', - - // Footer - copyright: '© 2026 Plataforma RustAcademy. Construida por Pulsefy.', - github: 'GitHub', - terms: 'Términos', - privacy: 'Privacidad', - } - }, - */ - /* - fr: { - translation: { - // Navigation - dashboard: 'Tableau de Bord', - linkGenerator: 'Générateur de Liens', - settings: 'Paramètres', - profileSettings: 'Paramètres de Profil', - services: 'Services', - settingsTitle: 'Paramètres', - profileCustomization: 'Personnalisation du Profil', - profileCustomizationDescription: 'Personnalisez votre page de paiement publique sur RustAcademy.to/{{username}}', - generalTab: 'Général', - developerTab: 'Développeur', - themeSettings: 'Paramètres de Thème', - primaryColor: 'Couleur Principale', - avatarUrl: 'URL de l\'Avatar', - bioLabel: 'Bio (max 160 caractères)', - socialLinks: 'Liens Sociaux', - twitterHandleLabel: 'Nom d\'utilisateur Twitter/X', - discordUsernameLabel: 'Nom d\'utilisateur Discord', - githubHandleLabel: 'Nom d\'utilisateur GitHub', - languageLabel: 'Langue', - changeLanguage: 'Changer la langue pour le texte et le formatage de l\'app', - saveChanges: 'Sauvegarder les Modifications', - preview: 'Aperçu', - show: 'Afficher', - hide: 'Masquer', - livePreview: 'Aperçu en Direct', - - // Generator page - createPayment: 'Créer un paiement', - requestInstantly: 'demander instantanément.', - advancedModeDescription: 'Le mode avancé prend en charge les paiements de chemin : choisissez ce que vous recevez et laissez les payeurs régler dans plusieurs actifs.', - amountLabel: 'Montant (destinataire reçoit)', - amountPlaceholder: '0.00', - loadingAssets: 'Chargement des actifs…', - destinationLabel: 'Destination', - destinationPlaceholder: 'Clé publique du destinataire', - memoLabel: 'Mémo (optionnel)', - memoPlaceholder: 'À quoi sert ce paiement ?', - advancedSettings: 'Paramètres avancés', - advancedMode: 'Mode Avancé', - basicMode: 'Mode Basique', - assetLabel: 'Actif', - assetPlaceholder: 'Sélectionnez un actif', - pathPayment: 'Paiement de Chemin', - pathPaymentDescription: 'Permettre aux payeurs d\'utiliser n\'importe quel actif pour payer.', - enablePathPayment: 'Activer le paiement de chemin', - fetchingEstimates: 'Récupération des estimations…', - estimatedSettlement: 'Règlement estimé : 3 secondes', - simulationFailed: 'Échec de la simulation', - networkError: 'Erreur réseau lors de l\'appel de pré-vol.', - requestFailed: 'Échec de la demande', - - // Home page - heroTitle: 'Paiements Instantanés Mondiaux', - heroSubtitle: 'Générez des liens de paiement en quelques secondes. Acceptez des paiements dans n\'importe quelle devise avec Stellar.', - generateLink: 'Générer un Lien', - goToDashboard: 'Aller au Tableau de Bord', - shareableUsernames: 'Noms d\'Utilisateur Partageables', - shareableUsernamesDesc: 'Réclamez votre nom unique comme RustAcademy.to/alex et recevez des paiements facilement.', - instantPayments: 'Paiements Instantanés', - instantPaymentsDesc: 'Générez des liens de paiement instantanément avec prise en charge avancée des paiements de chemin.', - shieldedTransactions: 'Transactions Blindées', - shieldedTransactionsDesc: 'Paiements préservant la confidentialité alimentés par des contrats intelligents Soroban.', - couldNotLoadAssets: 'Impossible de charger les actifs vérifiés.', - invalidPublicKey: 'Entrez une clé publique Stellar valide de 56 caractères (G…).', - preflightUnavailable: 'Le pré-vol Soroban n\'est pas configuré sur ce serveur.', - preflightFailed: 'La demande de pré-vol a échoué.', - networkError: 'Erreur réseau lors de l\'appel du pré-vol.', - requestFailed: 'Échec de la demande', - - // Footer - copyright: '© 2026 Plateforme RustAcademy. Construite par Pulsefy.', - github: 'GitHub', - terms: 'Conditions', - privacy: 'Confidentialité', - } - } - */ - } - } -}; - -export default i18n; \ No newline at end of file diff --git a/node.tar.xz b/node.tar.xz deleted file mode 100644 index c1110eb7a..000000000 Binary files a/node.tar.xz and /dev/null differ