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