use super::*; use common_data::asset::Asset; use near_sdk::json_types::U128; use near_workspaces::{types::Gas, Account, Contract}; use serde_json::json; // ==================== Deposit Operations ==================== /// Perform FT deposit via ft_transfer_call pub async fn vault_deposit_ft( ft_contract: &Contract, vault: &Contract, user: &Account, amount: U128, is_request: bool, min_shares: U128, ) -> TestResult<()> { println!( "💰 FT Deposit: user={}, amount={}, is_request={}, min_shares={}", user.id(), amount.0, is_request, min_shares.0 ); let deposit_msg = json!({ "is_request": is_request, "min_shares": min_shares, "receiver_id": serde_json::Value::Null, "memo": serde_json::Value::Null }); let result = user .call(ft_contract.id(), "ft_transfer_call") .args_json(json!({ "receiver_id": vault.id(), "amount": amount, "memo": "Test deposit", "msg": deposit_msg.to_string() })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 FT deposit logs:"); for log in result.logs() { println!(" {}", log); } } println!("FT deposit completed"); Ok(()) } /// Perform MT deposit via mt_transfer_call pub async fn vault_deposit_mt( mt_contract: &Contract, vault: &Contract, user: &Account, token_id: &str, amount: U128, is_request: bool, min_shares: U128, ) -> TestResult<()> { println!( "💰 MT Deposit: user={}, token_id={}, amount={}, is_request={}, min_shares={}", user.id(), token_id, amount.0, is_request, min_shares.0 ); let deposit_msg = json!({ "is_request": is_request, "min_shares": min_shares, "receiver_id": serde_json::Value::Null, "memo": serde_json::Value::Null }); let result = user .call(mt_contract.id(), "mt_transfer_call") .args_json(json!({ "receiver_id": vault.id(), "token_id": token_id, "amount": amount, "approval": null, "memo": "Test deposit", "msg": deposit_msg.to_string() })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 MT deposit logs:"); for log in result.logs() { println!(" {}", log); } } println!("MT deposit completed"); Ok(()) } // ==================== Redeem Operations ==================== /// Perform sync redeem pub async fn vault_redeem( vault: &Contract, user: &Account, asset: &Asset, shares: U128, min_assets: U128, ) -> TestResult { println!( "💸 Redeem: user={}, asset={}, shares={}, min_assets={}", user.id(), asset, shares.0, min_assets.0 ); // Get user's asset balance before redeem let before: U128 = get_asset_balance(asset, user).await?; let result = user .call(vault.id(), "redeem") .args_json(json!({ "asset": asset, "shares": shares, "receiver_id": user.id(), "owner_id": user.id(), "min_asset_amount": min_assets, "memo": serde_json::Value::Null })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 Redeem logs:"); for log in result.logs() { println!(" {}", log); } } // After redeem completes, fetch user's asset balance again let after: U128 = get_asset_balance(asset, user).await?; println!( "Redeem transaction completed successfully. User asset balance: before={}, after={}", before.0, after.0 ); // Return assets gained from redeem let gained = if after.0 >= before.0 { after.0 - before.0 } else { 0 }; Ok(U128(gained)) } /// Request async redeem pub async fn vault_request_redeem( vault: &Contract, user: &Account, asset: &Asset, shares: U128, min_assets: U128, ) -> TestResult { println!( "Request Redeem: user={}, asset={}, shares={}, min_assets={}", user.id(), asset, shares.0, min_assets.0 ); let result = user .call(vault.id(), "request_redeem") .args_json(json!({ "asset": asset, "shares": shares, "receiver_id": user.id(), "owner_id": user.id(), "min_asset_amount": min_assets, "memo": serde_json::Value::Null })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 Request redeem logs:"); for log in result.logs() { println!(" {}", log); } } let request_id: u64 = result.json()?; println!("Redeem request created: ID {}", request_id); Ok(request_id) } // ==================== Query Operations ==================== /// Get share price for an asset pub async fn vault_get_share_price(vault: &Contract, asset: &Asset) -> TestResult { println!("🔍 Getting share price for asset: {}", asset); let rate: U128 = vault .call("get_share_price_in_asset") .args_json(json!({ "asset": asset })) .view() .await? .json()?; println!(" Share price: {}", rate.0); Ok(rate) } /// Get user's share balance pub async fn vault_get_share_balance( vault: &Contract, account_id: &near_sdk::AccountId, ) -> TestResult { println!("🔍 Getting share balance for account: {}", account_id); let balance: U128 = vault .call("ft_balance_of") .args_json(json!({ "account_id": account_id })) .view() .await? .json()?; println!(" Share balance: {}", balance.0); Ok(balance) } /// Get vault asset balance (VaultBalance struct) pub async fn vault_get_asset_balance( vault: &Contract, asset: &Asset, ) -> TestResult { println!("🔍 Getting vault asset balance for: {}", asset); let balance: serde_json::Value = vault .call("get_vault_balance") .args_json(json!({ "asset": asset })) .view() .await? .json()?; println!(" Vault balance: {:?}", balance); Ok(balance) } /// Get all pending deposits pub async fn vault_get_pending_deposits(vault: &Contract) -> TestResult> { println!("🔍 Getting all pending deposits"); let deposits: Vec = vault .call("get_all_pending_deposits") .view() .await? .json()?; println!(" Found {} pending deposits", deposits.len()); Ok(deposits) } /// Get pending deposits for a user pub async fn vault_get_user_pending_deposits( vault: &Contract, account_id: &near_sdk::AccountId, ) -> TestResult> { println!("🔍 Getting pending deposits for user: {}", account_id); let deposits: Vec = vault .call("get_user_pending_deposits") .args_json(json!({ "account_id": account_id })) .view() .await? .json()?; println!(" Found {} pending deposits", deposits.len()); Ok(deposits) } /// Get all pending redeems pub async fn vault_get_pending_redeems(vault: &Contract) -> TestResult> { println!("🔍 Getting all pending redeems"); let redeems: Vec = vault.call("get_all_pending_redeems").view().await?.json()?; println!(" Found {} pending redeems", redeems.len()); Ok(redeems) } /// Get withdraw info for a specific operation ID pub async fn vault_get_withdraw_info( vault: &Contract, operation_id: u64, ) -> TestResult { let withdraw: serde_json::Value = vault .call("get_withdraw_info") .args_json(json!({ "operation_id": operation_id })) .view() .await? .json()?; Ok(withdraw) } /// Calculate total assets needed to process confirmed redeems /// Returns (total_needed, asset) by querying vault state pub async fn vault_calculate_redeem_amounts( vault: &Contract, operation_ids: &[u64], ) -> TestResult<(U128, Asset)> { let mut total_needed = 0u128; let mut redeem_asset: Option = None; for &operation_id in operation_ids { let withdraw_value = vault_get_withdraw_info(vault, operation_id).await?; if withdraw_value.is_null() { return Err(format!("Redeem request {} not found", operation_id).into()); } let withdraw: serde_json::Value = withdraw_value; // Check if confirmed let is_confirmed = withdraw["confirmed"] .as_bool() .ok_or("Missing confirmed field")?; if !is_confirmed { return Err(format!("Redeem request {} is not confirmed", operation_id).into()); } // Get asset let asset: Asset = serde_json::from_value(withdraw["asset"].clone()) .map_err(|e| format!("Failed to parse asset: {}", e))?; // Verify all redeems use same asset if let Some(ref expected_asset) = redeem_asset { if &asset != expected_asset { return Err(format!( "All redeems must use same asset. Expected {}, got {}", expected_asset, asset ) .into()); } } else { redeem_asset = Some(asset.clone()); } // Get confirmed share price let share_price_str = withdraw["confirmed_share_price"] .as_str() .ok_or("Missing confirmed_share_price")?; let share_price: u128 = share_price_str .parse() .map_err(|e| format!("Failed to parse share price: {}", e))?; // Get shares let shares_str = withdraw["shares"].as_str().ok_or("Missing shares")?; let shares: u128 = shares_str .parse() .map_err(|e| format!("Failed to parse shares: {}", e))?; // Calculate gross assets from shares and share price // Formula: gross_assets = shares * share_price / 10^(extra_decimals + rate_decimals) // Using extra_decimals=18, rate_decimals=8 let gross_assets = (shares * share_price) / 10u128.pow(18 + 8); // Get withdrawal fee for this asset let withdrawal_fee_bps: u16 = vault .call("get_withdrawal_fee_by_asset") .args_json(json!({ "asset": asset })) .view() .await? .json()?; // Calculate fee (fee = gross_assets * fee_bps / 10000) let fee_amount = (gross_assets * withdrawal_fee_bps as u128) / 10000; let net_assets = gross_assets - fee_amount; total_needed += net_assets; } let asset = redeem_asset.ok_or("No redeems provided")?; println!( "Total assets needed for {} redeems: {} of {}", operation_ids.len(), total_needed, asset ); Ok((U128(total_needed), asset)) } /// Get pending redeems for a user pub async fn vault_get_user_pending_redeems( vault: &Contract, account_id: &near_sdk::AccountId, ) -> TestResult> { println!("🔍 Getting pending redeems for user: {}", account_id); let redeems: Vec = vault .call("get_user_pending_withdraws") .args_json(json!({ "account_id": account_id })) .view() .await? .json()?; println!(" Found {} pending redeems", redeems.len()); Ok(redeems) } /// Convert assets to shares using current share price pub async fn vault_convert_to_shares( vault: &Contract, asset: &Asset, assets: U128, ) -> TestResult { println!( "Converting {} assets to shares for asset: {}", assets.0, asset ); let shares: U128 = vault .call("convert_to_shares") .args_json(json!({ "asset": asset, "asset_amount": assets })) .view() .await? .json()?; println!(" Converted to {} shares", shares.0); Ok(shares) } /// Convert shares to assets using current share price pub async fn vault_convert_to_asset_amount( vault: &Contract, asset: &Asset, shares: U128, ) -> TestResult { println!( "Converting {} shares to assets for asset: {}", shares.0, asset ); let assets: U128 = vault .call("convert_to_asset_amount") .args_json(json!({ "asset": asset, "shares": shares })) .view() .await? .json()?; println!(" Converted to {} assets", assets.0); Ok(assets) } /// Get total supply of vault shares pub async fn vault_get_total_supply(vault: &Contract) -> TestResult { println!("🔍 Getting total share supply"); let total: U128 = vault.call("ft_total_supply").view().await?.json()?; println!(" Total supply: {}", total.0); Ok(total) } /// Get vault configuration pub async fn vault_get_config(vault: &Contract) -> TestResult { println!("🔍 Getting vault configuration"); let config: serde_json::Value = vault.call("get_vault_config").view().await?.json()?; println!(" Config: {:?}", config); Ok(config) } /// Check if vault is emergency paused pub async fn vault_is_paused(vault: &Contract) -> TestResult { println!("🔍 Checking if vault is emergency paused"); let paused: bool = vault.call("is_emergency_paused").view().await?.json()?; println!(" Emergency paused: {}", paused); Ok(paused) } /// Get accepted deposit assets pub async fn vault_get_accepted_deposit_assets(vault: &Contract) -> TestResult> { println!("🔍 Getting accepted deposit assets"); let assets: Vec = vault .call("get_accepted_deposit_assets") .view() .await? .json()?; println!(" Found {} accepted deposit assets", assets.len()); Ok(assets) } /// Get available redeem assets pub async fn vault_get_available_redeem_assets(vault: &Contract) -> TestResult> { println!("🔍 Getting available redeem assets"); let assets: Vec = vault .call("get_available_redeem_assets") .view() .await? .json()?; println!(" Found {} available redeem assets", assets.len()); Ok(assets) } // ==================== FT Token Operations (for user shares) ==================== /// Transfer vault shares from one user to another pub async fn vault_transfer_shares( vault: &Contract, sender: &Account, receiver_id: &near_sdk::AccountId, amount: U128, ) -> TestResult<()> { println!( "📤 Transferring {} shares from {} to {}", amount.0, sender.id(), receiver_id ); let result = sender .call(vault.id(), "ft_transfer") .args_json(json!({ "receiver_id": receiver_id, "amount": amount, "memo": "Share transfer" })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(DEFAULT_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 Transfer logs:"); for log in result.logs() { println!(" {}", log); } } println!("Share transfer completed"); Ok(()) } // ==================== Test Assertion Helpers ==================== /// Assert that share balance matches expected value pub async fn assert_share_balance( vault: &Contract, account_id: &near_sdk::AccountId, expected: U128, ) -> TestResult<()> { let actual = vault_get_share_balance(vault, account_id).await?; assert_eq!( actual.0, expected.0, "Share balance mismatch for {}: expected {}, got {}", account_id, expected.0, actual.0 ); Ok(()) } /// Assert that share price is within acceptable range pub async fn assert_share_price_in_range( vault: &Contract, asset: &Asset, min: u128, max: u128, ) -> TestResult<()> { let rate = vault_get_share_price(vault, asset).await?; assert!( rate.0 >= min && rate.0 <= max, "Share price {} for {} is outside expected range [{}, {}]", rate.0, asset, min, max ); Ok(()) } /// Assert that no pending deposits exist pub async fn assert_no_pending_deposits(vault: &Contract) -> TestResult<()> { let deposits = vault_get_pending_deposits(vault).await?; assert_eq!( deposits.len(), 0, "Expected no pending deposits, but found {}", deposits.len() ); Ok(()) } /// Assert that no pending redeems exist pub async fn assert_no_pending_redeems(vault: &Contract) -> TestResult<()> { let redeems = vault_get_pending_redeems(vault).await?; assert_eq!( redeems.len(), 0, "Expected no pending redeems, but found {}", redeems.len() ); Ok(()) } /// Assert vault is not paused pub async fn assert_vault_not_paused(vault: &Contract) -> TestResult<()> { let paused = vault_is_paused(vault).await?; assert!(!paused, "Expected vault to not be paused, but it is"); Ok(()) } /// Assert vault is paused pub async fn assert_vault_paused(vault: &Contract) -> TestResult<()> { let paused = vault_is_paused(vault).await?; assert!(paused, "Expected vault to be paused, but it is not"); Ok(()) } pub async fn get_asset_balance(asset: &Asset, user: &Account) -> TestResult { match asset { Asset::FungibleToken { contract_id } => { let v: U128 = user .view(contract_id, "ft_balance_of") .args_json(json!({ "account_id": user.id() })) .await? .json()?; Ok(v) } Asset::MultiToken { contract_id, token_id, } => { let v: U128 = user .view(contract_id, "mt_balance_of") .args_json(json!({ "account_id": user.id(), "token_id": token_id })) .await? .json()?; Ok(v) } } } // ==================== Cancel Operations ==================== /// Cancel a pending deposit request pub async fn vault_cancel_request_deposit( vault: &Contract, user: &Account, operation_id: u64, ) -> TestResult<()> { println!( "❌ Cancel Deposit Request: user={}, operation_id={}", user.id(), operation_id ); let result = user .call(vault.id(), "cancel_request_deposit") .args_json(json!({ "operation_id": operation_id, })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 Cancel deposit logs:"); for log in result.logs() { println!(" {}", log); } } println!("✅ Deposit request cancelled successfully"); Ok(()) } /// Cancel a pending redeem request (only unconfirmed) pub async fn vault_cancel_request_redeem( vault: &Contract, user: &Account, operation_id: u64, ) -> TestResult<()> { println!( "❌ Cancel Redeem Request: user={}, operation_id={}", user.id(), operation_id ); let result = user .call(vault.id(), "cancel_request_redeem") .args_json(json!({ "operation_id": operation_id, })) .deposit(near_workspaces::types::NearToken::from_yoctonear(1)) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 Cancel redeem logs:"); for log in result.logs() { println!(" {}", log); } } println!("✅ Redeem request cancelled successfully"); Ok(()) } // ==================== Claim Operations ==================== /// Claim processed redeem assets pub async fn vault_claim_assets(vault: &Contract, user: &Account, asset: &Asset) -> TestResult<()> { println!("💸 Claim Assets: user={}, asset={}", user.id(), asset); let result = user .call(vault.id(), "claim_assets") .args_json(json!({ "asset": asset, })) .gas(Gas::from_tgas(HIGH_GAS)) .transact() .await? .into_result()?; // Print logs for debugging if !result.logs().is_empty() { println!("📋 Claim assets logs:"); for log in result.logs() { println!(" {}", log); } } println!("✅ Assets claimed successfully"); Ok(()) } /// Get claimable assets for a user and asset pub async fn vault_get_claimable_assets( vault: &Contract, user: &Account, asset: &Asset, ) -> TestResult { println!( "🔍 Getting claimable assets: user={}, asset={}", user.id(), asset ); let result: U128 = vault .view("get_claimable_asset_amount") .args_json(json!({ "account_id": user.id(), "asset": asset, })) .await? .json()?; println!(" Claimable amount: {}", result.0); Ok(result) } /// Get all claimable assets for a user pub async fn vault_get_all_claimable_assets( vault: &Contract, user: &Account, ) -> TestResult> { println!("🔍 Getting all claimable assets: user={}", user.id()); let result: Vec<(Asset, U128)> = vault .view("get_all_claimable_asset_amounts") .args_json(json!({ "account_id": user.id(), })) .await? .json()?; if result.is_empty() { println!(" No claimable assets"); } else { println!(" Claimable assets:"); for (asset, amount) in &result { println!(" {}: {}", asset, amount.0); } } Ok(result) } // ==================== Fee Query Operations ==================== /// Get fees owed for a specific asset pub async fn vault_get_fees_owed_for_asset(vault: &Contract, asset: &Asset) -> TestResult { println!("🔍 Getting fees owed for asset: {}", asset); let fees: U128 = vault .call("get_fees_owed_for_asset") .args_json(json!({ "asset": asset })) .view() .await? .json()?; println!(" Fees owed: {}", fees.0); Ok(fees) } /// Get protocol fees owed for a specific asset pub async fn vault_get_protocol_fees_owed_for_asset( vault: &Contract, asset: &Asset, ) -> TestResult { println!("🔍 Getting protocol fees owed for asset: {}", asset); let fees: U128 = vault .call("get_protocol_fees_owed_for_asset") .args_json(json!({ "asset": asset })) .view() .await? .json()?; println!(" Protocol fees owed: {}", fees.0); Ok(fees) } /// Get all fees owed across all assets pub async fn vault_get_all_fees_owed(vault: &Contract) -> TestResult> { println!("🔍 Getting all fees owed"); let fees: Vec<(Asset, U128)> = vault.call("get_all_fees_owed").view().await?.json()?; if fees.is_empty() { println!(" No fees owed"); } else { println!(" Fees owed:"); for (asset, amount) in &fees { println!(" {}: {}", asset, amount.0); } } Ok(fees) } /// Get all protocol fees owed across all assets pub async fn vault_get_all_protocol_fees_owed(vault: &Contract) -> TestResult> { println!("🔍 Getting all protocol fees owed"); let fees: Vec<(Asset, U128)> = vault .call("get_all_protocol_fees_owed") .view() .await? .json()?; if fees.is_empty() { println!(" No protocol fees owed"); } else { println!(" Protocol fees owed:"); for (asset, amount) in &fees { println!(" {}: {}", asset, amount.0); } } Ok(fees) }