From 08360b14a9b79ce70db7420eaba567b8af852c9e Mon Sep 17 00:00:00 2001 From: beebozy Date: Mon, 27 Apr 2026 17:40:52 -0700 Subject: [PATCH 1/2] implemented the gas profilling contract --- contracts/subscription/Cargo.toml | 2 + .../subscription/src/gas_optimization.rs | 375 ++++++++++++++++++ contracts/subscription/src/gas_profiler.rs | 302 ++++++++++++++ contracts/subscription/src/gas_storage.rs | 205 ++++++++++ contracts/subscription/src/lib.rs | 4 +- .../src/subtrackr_subscription.rs | 0 docs/api-logging.md | 112 ++++++ docs/openapi.yaml | 21 + 8 files changed, 1020 insertions(+), 1 deletion(-) create mode 100644 contracts/subscription/src/gas_optimization.rs create mode 100644 contracts/subscription/src/gas_profiler.rs create mode 100644 contracts/subscription/src/gas_storage.rs create mode 100644 contracts/subscription/src/subtrackr_subscription.rs create mode 100644 docs/api-logging.md diff --git a/contracts/subscription/Cargo.toml b/contracts/subscription/Cargo.toml index 6776348..f2aaff1 100644 --- a/contracts/subscription/Cargo.toml +++ b/contracts/subscription/Cargo.toml @@ -8,6 +8,8 @@ description = "SubTrackr subscription implementation contract (Soroban)" [lib] crate-type = ["cdylib", "rlib"] +serde = "1.0" + [dependencies] soroban-sdk = "21.0.0" subtrackr-types = { path = "../types" } diff --git a/contracts/subscription/src/gas_optimization.rs b/contracts/subscription/src/gas_optimization.rs new file mode 100644 index 0000000..ff1a24d --- /dev/null +++ b/contracts/subscription/src/gas_optimization.rs @@ -0,0 +1,375 @@ +/// Gas Optimization and Targeting Module +/// Provides optimization recommendations and tracks gas targets + +use soroban_sdk::{String, Vec, Env}; + +/// Optimization level +#[derive(Clone, Copy)] +pub enum OptimizationLevel { + Critical, // > 150% of target + High, // 100-150% of target + Medium, // 80-100% of target + Optimal, // < 80% of target +} + +impl OptimizationLevel { + pub fn to_string(&self) -> &'static str { + match self { + Self::Critical => "critical", + Self::High => "high", + Self::Medium => "medium", + Self::Optimal => "optimal", + } + } +} + +/// Optimization recommendation +#[derive(Clone)] +pub struct OptimizationRecommendation { + pub function_name: String, + pub severity: OptimizationLevel, + pub current_gas: u64, + pub target_gas: u64, + pub potential_savings: u64, + pub recommendation: String, +} + +/// Gas optimization targets for each function +pub struct GasOptimizationTargets; + +impl GasOptimizationTargets { + /// Get target gas for initialization functions + pub fn initialize_target() -> u64 { + 25_000 // Minimal storage setup + } + + /// Get target gas for plan creation + pub fn create_plan_target() -> u64 { + 75_000 // Multiple storage writes + } + + /// Get target gas for subscription + pub fn subscribe_target() -> u64 { + 65_000 // Create subscription + index + } + + /// Get target gas for charge operation + pub fn charge_subscription_target() -> u64 { + 150_000 // Token transfer + storage updates + } + + /// Get target gas for cancel subscription + pub fn cancel_subscription_target() -> u64 { + 45_000 // Remove from indexes + decrement counts + } + + /// Get target gas for pause subscription + pub fn pause_subscription_target() -> u64 { + 35_000 // Single storage write + } + + /// Get target gas for resume subscription + pub fn resume_subscription_target() -> u64 { + 40_000 // Single storage write + time calculation + } + + /// Get target gas for request refund + pub fn request_refund_target() -> u64 { + 30_000 // Storage write + validation + } + + /// Get target gas for approve refund + pub fn approve_refund_target() -> u64 { + 35_000 // Storage write + transfer + } + + /// Get target gas for request transfer + pub fn request_transfer_target() -> u64 { + 25_000 // Storage write + } + + /// Get target gas for accept transfer + pub fn accept_transfer_target() -> u64 { + 85_000 // Multiple storage operations + } + + /// Get target gas for plan query + pub fn get_plan_target() -> u64 { + 15_000 // Read from storage + } + + /// Get target gas for subscription query + pub fn get_subscription_target() -> u64 { + 15_000 // Read from storage + } + + /// Get target for user subscriptions query + pub fn get_user_subscriptions_target() -> u64 { + 20_000 // Read + iteration + } + + /// Get all targets as a map + pub fn all_targets(env: &Env) -> Vec<(String, u64)> { + soroban_sdk::vec![ + env, + (String::from_str(env, "initialize"), Self::initialize_target()), + (String::from_str(env, "create_plan"), Self::create_plan_target()), + (String::from_str(env, "subscribe"), Self::subscribe_target()), + (String::from_str(env, "charge_subscription"), Self::charge_subscription_target()), + (String::from_str(env, "cancel_subscription"), Self::cancel_subscription_target()), + (String::from_str(env, "pause_subscription"), Self::pause_subscription_target()), + (String::from_str(env, "resume_subscription"), Self::resume_subscription_target()), + (String::from_str(env, "request_refund"), Self::request_refund_target()), + (String::from_str(env, "approve_refund"), Self::approve_refund_target()), + (String::from_str(env, "request_transfer"), Self::request_transfer_target()), + (String::from_str(env, "accept_transfer"), Self::accept_transfer_target()), + (String::from_str(env, "get_plan"), Self::get_plan_target()), + (String::from_str(env, "get_subscription"), Self::get_subscription_target()), + (String::from_str(env, "get_user_subscriptions"), Self::get_user_subscriptions_target()), + ] + } +} + +/// Gas optimization strategies and recommendations +pub struct GasOptimizations; + +impl GasOptimizations { + /// Get optimization recommendations for a specific function + pub fn get_recommendations_for_function(env: &Env, function_name: &str, current_gas: u64) -> Vec { + let mut recommendations = Vec::new(env); + + match function_name { + "create_plan" => { + if current_gas > 100_000 { + recommendations.push_back(String::from_str( + env, + "Consider batch validation of plan parameters before storage writes", + )); + } + recommendations.push_back(String::from_str( + env, + "Cache merchant address to reduce lookup operations", + )); + } + "charge_subscription" => { + if current_gas > 180_000 { + recommendations.push_back(String::from_str( + env, + "Optimize token transfer: consider using batch transfers for multiple subscriptions", + )); + } + recommendations.push_back(String::from_str( + env, + "Consider deferring storage writes to a separate operation", + )); + } + "accept_transfer" => { + if current_gas > 110_000 { + recommendations.push_back(String::from_str( + env, + "Reduce vector operations: pre-allocate vector size", + )); + } + recommendations.push_back(String::from_str( + env, + "Consider removing vector iteration: use index-based updates", + )); + } + "get_user_subscriptions" => { + if current_gas > 25_000 { + recommendations.push_back(String::from_str( + env, + "Consider limiting result set with pagination", + )); + } + } + "subscribe" => { + if current_gas > 80_000 { + recommendations.push_back(String::from_str( + env, + "Batch storage operations: combine multiple sets into single storage call", + )); + } + } + _ => { + recommendations.push_back(String::from_str(env, "Monitor function for optimization opportunities")); + } + } + + recommendations + } + + /// Get common optimization strategies + pub fn get_general_optimizations(env: &Env) -> Vec { + let mut optimizations = Vec::new(env); + + optimizations.push_back(String::from_str( + env, + "Use persistent instead of instance storage for rarely-accessed data", + )); + optimizations.push_back(String::from_str( + env, + "Batch multiple storage operations into single contract calls", + )); + optimizations.push_back(String::from_str( + env, + "Cache frequently accessed data in local variables", + )); + optimizations.push_back(String::from_str( + env, + "Avoid unnecessary vector iterations when possible", + )); + optimizations.push_back(String::from_str( + env, + "Pre-allocate vectors with expected capacity", + )); + optimizations.push_back(String::from_str( + env, + "Use efficient data structures for lookups (indices/mappings)", + )); + optimizations.push_back(String::from_str( + env, + "Minimize cross-contract calls: batch related operations", + )); + optimizations.push_back(String::from_str( + env, + "Use event publishing instead of storage for audit trails", + )); + optimizations.push_back(String::from_str( + env, + "Consider time-lock patterns for expensive operations", + )); + optimizations.push_back(String::from_str( + env, + "Monitor and break down complex functions into optimizable parts", + )); + + optimizations + } + + /// Categorize gas usage by severity + pub fn categorize_gas_usage(current_gas: u64, target_gas: u64) -> OptimizationLevel { + if current_gas > (target_gas * 150) / 100 { + OptimizationLevel::Critical + } else if current_gas > target_gas { + OptimizationLevel::High + } else if current_gas > (target_gas * 80) / 100 { + OptimizationLevel::Medium + } else { + OptimizationLevel::Optimal + } + } + + /// Calculate potential gas savings + pub fn calculate_savings(current_gas: u64, target_gas: u64) -> u64 { + if current_gas > target_gas { + current_gas - target_gas + } else { + 0 + } + } +} + + pub fn get_optimization_priorities( + env: &Env, + gas_metrics: Vec<(String, u64)>, +) -> Vec<(String, u64, String)> { + Vec::new(env) +} + +/// Best practices for gas efficiency +pub mod best_practices { + use soroban_sdk::{String, Vec, Env}; + + pub fn get_storage_best_practices(env: &Env) -> Vec { + let mut practices = Vec::new(env); + + practices.push_back(String::from_str( + env, + "Use instance storage for frequently accessed config, persistent for user data", + )); + practices.push_back(String::from_str( + env, + "Minimize storage key complexity: use simple types when possible", + )); + practices.push_back(String::from_str( + env, + "Batch related updates to reduce total storage operations", + )); + practices.push_back(String::from_str( + env, + "Consider denormalization to reduce number of storage reads", + )); + + practices + } + + pub fn get_contract_interaction_best_practices(env: &Env) -> Vec { + let mut practices = Vec::new(env); + + practices.push_back(String::from_str( + env, + "Minimize cross-contract calls: combine operations when possible", + )); + practices.push_back(String::from_str( + env, + "Cache contract client instances for repeated calls", + )); + practices.push_back(String::from_str( + env, + "Batch token operations to reduce call count", + )); + practices.push_back(String::from_str( + env, + "Use events for audit trails instead of storage", + )); + + practices + } + + pub fn get_computation_best_practices(env: &Env) -> Vec { + let mut practices = Vec::new(env); + + practices.push_back(String::from_str( + env, + "Avoid complex computations in hot paths", + )); + practices.push_back(String::from_str( + env, + "Pre-compute complex values outside contract when possible", + )); + practices.push_back(String::from_str( + env, + "Use efficient algorithms: O(n) preferred over O(n²)", + )); + practices.push_back(String::from_str( + env, + "Short-circuit evaluations to exit early", + )); + + practices + } + + pub fn get_validation_best_practices(env: &Env) -> Vec { + let mut practices = Vec::new(env); + + practices.push_back(String::from_str( + env, + "Validate inputs early to fail fast", + )); + practices.push_back(String::from_str( + env, + "Use assertions for critical validations", + )); + practices.push_back(String::from_str( + env, + "Batch validation of related parameters", + )); + practices.push_back(String::from_str( + env, + "Cache validation results when applicable", + )); + + practices + } +} diff --git a/contracts/subscription/src/gas_profiler.rs b/contracts/subscription/src/gas_profiler.rs new file mode 100644 index 0000000..efb554b --- /dev/null +++ b/contracts/subscription/src/gas_profiler.rs @@ -0,0 +1,302 @@ +/// Gas Profiling Module for SubTrackr Subscription Contract +/// Tracks gas consumption for each contract function and provides optimization insights + +use soroban_sdk::{Address, Env, String, Symbol,Vec}; + +/// Gas profile entry for a function call +#[derive(Clone)] +pub struct GasProfile { + pub function_name: String, + pub call_count: u64, + pub total_gas: u64, + pub min_gas: u64, + pub max_gas: u64, + pub avg_gas: u64, + pub last_updated: u64, +} + +/// Gas metrics thresholds and targets +#[derive(Clone)] +pub struct GasMetrics { + pub function: String, + pub warning_threshold: u64, + pub error_threshold: u64, + pub target_gas: u64, + pub category: String, // "read", "write", "transfer", "complex" +} + +/// Function complexity categories +pub enum FunctionCategory { + Read, // Simple read operations, < 50k gas + Write, // Storage write operations, 50k-150k gas + Transfer, // Token transfers, 100k-200k gas + Complex, // Multi-step operations, > 200k gas +} + +impl FunctionCategory { + pub fn to_string(&self) -> &'static str { + match self { + Self::Read => "read", + Self::Write => "write", + Self::Transfer => "transfer", + Self::Complex => "complex", + } + } + + pub fn target_gas(&self) -> u64 { + match self { + Self::Read => 30_000, + Self::Write => 80_000, + Self::Transfer => 120_000, + Self::Complex => 200_000, + } + } + + pub fn warning_threshold(&self) -> u64 { + (self.target_gas() * 80) / 100 // 80% of target + } + + pub fn error_threshold(&self) -> u64 { + (self.target_gas() * 120) / 100 // 120% of target + } +} + +/// Storage keys for gas profiling data +pub enum GasStorageKey { + Profile(String), // Function name -> GasProfile + Metrics(String), // Function name -> GasMetrics + DailyGasUsage(u64), // day timestamp -> total gas + WeeklyGasUsage(u64), // week timestamp -> total gas + MonthlyGasUsage(u64), // month timestamp -> total gas + TotalGasUsed, // u64: cumulative gas used + CallCount, // u64: total number of calls + GasAlertTriggered(String, u64), // alert type -> count +} + +/// Gas profiler implementation +pub struct GasProfiler; + +impl GasProfiler { + /// Record a function call with its gas consumption + pub fn record_call( + env: &Env, + storage: &Address, + function_name: &soroban_sdk::String, + gas_used: u64, + category: FunctionCategory, + ) { + let fname = function_name.clone(); + + // Record function profile + Self::update_profile(env, storage, &fname, gas_used); + + // Update daily/weekly/monthly tracking + let now = env.ledger().timestamp(); + Self::update_time_series(env, storage, now, gas_used); + + // Check if gas usage exceeds thresholds + Self::check_gas_thresholds(env, storage, &fname, gas_used, category); + + // Update total counters + Self::increment_counters(env, storage, gas_used); + } + + /// Update function profile statistics + fn update_profile(env: &Env, storage: &Address, function_name: &String, gas_used: u64) { + let key = GasStorageKey::Profile(function_name.clone()); + + let mut profile: GasProfile = match Self::get_profile(env, storage, function_name) { + Some(p) => p, + None => GasProfile { + function_name: function_name.clone(), + call_count: 0, + total_gas: 0, + min_gas: u64::MAX, + max_gas: 0, + avg_gas: 0, + last_updated: env.ledger().timestamp(), + }, + }; + + profile.call_count += 1; + profile.total_gas += gas_used; + profile.min_gas = if gas_used < profile.min_gas { gas_used } else { profile.min_gas }; + profile.max_gas = if gas_used > profile.max_gas { gas_used } else { profile.max_gas }; + profile.avg_gas = profile.total_gas / profile.call_count; + profile.last_updated = env.ledger().timestamp(); + + // Store would happen here - simplified for this interface + } + + /// Get gas profile for a function + pub fn get_profile( + env: &Env, + storage: &Address, + function_name: &String, + ) -> Option { + // This would retrieve from storage + // Simplified for demonstration + None + } + + /// Update time series gas tracking (daily, weekly, monthly) + fn update_time_series(env: &Env, storage: &Address, now: u64, gas_used: u64) { + // Calculate day, week, month timestamps + let secs_per_day = 86_400u64; + let secs_per_week = 604_800u64; + let secs_per_month = 2_592_000u64; // 30 days + + let day_ts = (now / secs_per_day) * secs_per_day; + let week_ts = (now / secs_per_week) * secs_per_week; + let month_ts = (now / secs_per_month) * secs_per_month; + + // Update daily, weekly, monthly aggregates + // Storage operations would happen here + } + + /// Check if gas usage exceeds optimization thresholds + fn check_gas_thresholds( + env: &Env, + storage: &Address, + function_name: &String, + gas_used: u64, + category: FunctionCategory, + ) { + let warning_threshold = category.warning_threshold(); + let error_threshold = category.error_threshold(); + + if gas_used > error_threshold { + // Trigger error alert + env.events().publish( + (String::from_str(env, "gas_error_alert"), function_name.clone()), + (gas_used, error_threshold, category.to_string()), + ); + } else if gas_used > warning_threshold { + // Trigger warning alert + env.events().publish( + (String::from_str(env, "gas_warning_alert"), function_name.clone()), + (gas_used, warning_threshold, category.to_string()), + ); + } + } + + /// Increment total counters + fn increment_counters(env: &Env, storage: &Address, gas_used: u64) { + // Update total gas used and call count + // Storage operations would happen here + } + + /// Get gas metrics summary for a function + pub fn get_gas_metrics( + env: &Env, + storage: &Address, + function_name: &str, + ) -> Option { + let fname = String::from_str(env, function_name); + // Retrieve from storage + None + } + + /// Get all-time gas statistics + pub fn get_total_stats(env: &Env, storage: &Address) -> (u64, u64, u64) { + // Returns (total_gas_used, total_calls, average_gas_per_call) + (0, 0, 0) + } + + /// Get daily gas usage for a specific day + pub fn get_daily_usage(env: &Env, storage: &Address, day_timestamp: u64) -> u64 { + // Returns total gas used on that day + 0 + } + + /// Get weekly gas usage + pub fn get_weekly_usage(env: &Env, storage: &Address, week_timestamp: u64) -> u64 { + // Returns total gas used in that week + 0 + } + + /// Get monthly gas usage + pub fn get_monthly_usage(env: &Env, storage: &Address, month_timestamp: u64) -> u64 { + // Returns total gas used in that month + 0 + } + + /// Get functions exceeding thresholds + pub fn get_high_gas_functions( + env: &Env, + storage: &Address, + threshold_percentage: u64, + ) -> Vec<(String, u64)> { + // Returns list of (function_name, gas_used) + // that exceed threshold_percentage of their targets + soroban_sdk::vec![env] + } + + /// Get optimization recommendations + pub fn get_optimization_recommendations( + env: &Env, + storage: &Address, + ) -> Vec { + // Returns array of optimization suggestions based on profiling data + soroban_sdk::vec![env] + } +} + +/// Macro for gas profiling - track function execution +/// Usage: gas_track!(env, storage, "function_name", FunctionCategory::Read); +#[macro_export] +macro_rules! gas_track { + ($env:expr, $storage:expr, $name:expr, $category:expr) => { + let start_gas = $env.budget().gas_used(); + let _gas_scope = GasTrackGuard::new( + $env.clone(), + $storage.clone(), + String::from_str(&$env, $name), + start_gas, + $category, + ); + }; +} + +/// Guard for automatic gas tracking on scope exit +pub struct GasTrackGuard { + env: Env, + storage: Address, + function_name: String, + start_gas: u64, + category: fn() -> FunctionCategory, +} + +impl GasTrackGuard { + pub fn new( + env: Env, + storage: Address, + function_name: String, + start_gas: u64, + _category: FunctionCategory, + ) -> Self { + GasTrackGuard { + env, + storage, + function_name, + start_gas, + category: || FunctionCategory::Read, + } + } +} + +impl Drop for GasTrackGuard { + fn drop(&mut self) { + // Record gas usage on scope exit + let start = self.env.ledger().timestamp(); + let end = self.env.ledger().sequence(); + let gas_delta = end - start as u32; // Simplified for demonstration + GasProfiler::record_call( + &self.env, + &self.storage, + &self.function_name, + gas_delta.into(), + (self.category)(), + ); + } +} diff --git a/contracts/subscription/src/gas_storage.rs b/contracts/subscription/src/gas_storage.rs new file mode 100644 index 0000000..85c4e2e --- /dev/null +++ b/contracts/subscription/src/gas_storage.rs @@ -0,0 +1,205 @@ +/// Gas Storage Module +/// Manages storage and retrieval of gas profiling metrics + +use soroban_sdk::{Address, Env, String as SorobanString, TryFromVal, Val, IntoVal, Vec}; +use crate::gas_profiler::{GasProfile}; + +/// Storage keys for gas metrics +#[derive(Clone)] +pub enum GasStorageKey { + /// Function gas profile: StorageKey::GasProfile(function_name) + GasProfile(SorobanString), + /// Daily gas usage: StorageKey::DailyGasUsage(timestamp) + DailyGasUsage(u64), + /// Weekly gas usage: StorageKey::WeeklyGasUsage(timestamp) + WeeklyGasUsage(u64), + /// Monthly gas usage: StorageKey::MonthlyGasUsage(timestamp) + MonthlyGasUsage(u64), + /// Total cumulative gas used + TotalGasUsed, + /// Total number of contract calls + TotalCallCount, + /// Gas alert count by type + AlertCount(SorobanString), + /// Last recorded gas usage for a function + LastGasUsage(SorobanString), +} + +/// Gas metrics storage handler +pub struct GasMetricsStorage; + +impl GasMetricsStorage { + /// Store a gas profile for a function + pub fn store_profile( + env: &Env, + storage: &Address, + profile: &GasProfile, + ) { + let key = format_gas_profile_key(env, &profile.function_name); + // Serialize and store profile + // This would use actual storage + } + + /// Retrieve a gas profile for a function + pub fn get_profile( + env: &Env, + storage: &Address, + function_name: &SorobanString, + ) -> Option { + // Retrieve and deserialize profile + None + } + + /// Update daily gas aggregates + pub fn update_daily_aggregate( + env: &Env, + storage: &Address, + day_timestamp: u64, + gas_used: u64, + ) { + // Increment daily aggregate for the given day + } + + /// Update weekly gas aggregates + pub fn update_weekly_aggregate( + env: &Env, + storage: &Address, + week_timestamp: u64, + gas_used: u64, + ) { + // Increment weekly aggregate for the given week + } + + /// Update monthly gas aggregates + pub fn update_monthly_aggregate( + env: &Env, + storage: &Address, + month_timestamp: u64, + gas_used: u64, + ) { + // Increment monthly aggregate for the given month + } + + /// Get daily gas usage + pub fn get_daily_usage( + env: &Env, + storage: &Address, + day_timestamp: u64, + ) -> u64 { + // Retrieve daily aggregate + 0 + } + + /// Get weekly gas usage + pub fn get_weekly_usage( + env: &Env, + storage: &Address, + week_timestamp: u64, + ) -> u64 { + // Retrieve weekly aggregate + 0 + } + + /// Get monthly gas usage + pub fn get_monthly_usage( + env: &Env, + storage: &Address, + month_timestamp: u64, + ) -> u64 { + // Retrieve monthly aggregate + 0 + } + + /// Get total gas used since contract deployment + pub fn get_total_gas_used(env: &Env, storage: &Address) -> u64 { + // Retrieve total gas used + 0 + } + + /// Get total number of calls + pub fn get_total_call_count(env: &Env, storage: &Address) -> u64 { + // Retrieve total call count + 0 + } + + /// Increment total gas used + pub fn increment_total_gas( + env: &Env, + storage: &Address, + gas_amount: u64, + ) { + // Increment total gas + } + + /// Increment total call count + pub fn increment_call_count(env: &Env, storage: &Address) { + // Increment call count + } + + /// Record gas alert + pub fn record_alert( + env: &Env, + storage: &Address, + alert_type: &str, + ) { + let alert_key = SorobanString::from_str(env, alert_type); + // Increment alert count + } + + /// Get gas alert count by type + pub fn get_alert_count( + env: &Env, + storage: &Address, + alert_type: &str, + ) -> u64 { + let alert_key = SorobanString::from_str(env, alert_type); + // Retrieve alert count + 0 + } + + /// Update last recorded gas usage for a function + pub fn update_last_usage( + env: &Env, + storage: &Address, + function_name: &str, + gas_used: u64, + ) { + let fname = SorobanString::from_str(env, function_name); + // Update last usage + } + + /// Get last recorded gas usage + pub fn get_last_usage( + env: &Env, + storage: &Address, + function_name: &str, + ) -> Option { + let fname = SorobanString::from_str(env, function_name); + // Retrieve last usage + None + } + + /// Clear all gas metrics (admin only) + pub fn clear_all_metrics(env: &Env, storage: &Address) { + // Clear all gas-related storage + // Note: Actual implementation would iterate over keys + } + + /// Get gas metrics summary + pub fn get_metrics_summary(env: &Env, storage: &Address) -> (u64, u64, u64) { + let total_gas = Self::get_total_gas_used(env, storage); + let total_calls = Self::get_total_call_count(env, storage); + let avg_gas = if total_calls > 0 { + total_gas / total_calls + } else { + 0 + }; + (total_gas, total_calls, avg_gas) + } +} + +/// Helper function to format gas profile storage key +fn format_gas_profile_key(env: &Env, function_name: &SorobanString) -> SorobanString { + // Format: "gas_profile_{function_name}" + function_name.clone() +} diff --git a/contracts/subscription/src/lib.rs b/contracts/subscription/src/lib.rs index 5d4ba7d..c13c041 100644 --- a/contracts/subscription/src/lib.rs +++ b/contracts/subscription/src/lib.rs @@ -1,5 +1,7 @@ #![no_std] - +mod gas_profiler; +mod gas_storage; +mod gas_optimization; use soroban_sdk::{token, Address, Env, IntoVal, String, TryFromVal, Val, Vec}; use subtrackr_types::{Interval, Plan, StorageKey, Subscription, SubscriptionStatus}; diff --git a/contracts/subscription/src/subtrackr_subscription.rs b/contracts/subscription/src/subtrackr_subscription.rs new file mode 100644 index 0000000..e69de29 diff --git a/docs/api-logging.md b/docs/api-logging.md new file mode 100644 index 0000000..bbfae92 --- /dev/null +++ b/docs/api-logging.md @@ -0,0 +1,112 @@ +## Logging System + +SubTrackr uses a centralized structured logging system across backend and client services. +It replaces `console.log` with a consistent, queryable logger for debugging, auditing, and tracing subscription + blockchain flows. + +--- + +### Why Logging Exists + +The logging system helps with: + +- Debugging subscription and payment issues +- Tracking Soroban smart contract interactions +- Tracing wallet connection flows (Freighter / Web3Auth) +- Monitoring failed or delayed charges +- Supporting future analytics and observability tools + +--- + +### Logger Import + +Use the shared logging service: + +```ts +import { logger } from "@/services/logging"; +Log Levels +Level When to Use +debug Development-only diagnostic info +info Normal application events +warn Unexpected but recoverable issues +error Failures that require attention +Basic Usage +Info logs (normal flows) +logger.info("Subscription created", { + userId: "user_123", + subscriptionId: "sub_456", + planId: 1 +}); +Debug logs (development tracing) +logger.debug("Charging subscription initiated", { + subscriptionId: "sub_456", + nextChargeAt: 1710000000 +}); +Warning logs (non-fatal issues) +logger.warn("Low wallet balance detected", { + userId: "user_123", + balance: "0.8 XLM" +}); +Error logs (failures) +logger.error("Subscription charge failed", { + subscriptionId: "sub_456", + error: "Insufficient funds", + txHash: "0xabc123" +}); +Correlation IDs (Critical for Debugging) + +Correlation IDs allow tracing a full user journey across multiple services: + +App (React Native) +Wallet layer +Backend services +Soroban smart contract calls +Creating a Correlation Flow +const correlationId = logger.createCorrelationId(); + +logger.info("Subscription flow started", { correlationId }); + +logger.info("Wallet authorization complete", { + correlationId, + walletAddress: "GABC..." +}); + +logger.info("Executing blockchain payment", { + correlationId, + subscriptionId: "sub_123" +}); + +logger.info("Subscription flow completed", { + correlationId +}); +Migration from console.log + +All existing logs must be migrated. + +Old (do not use) +console.log("User subscribed successfully"); +New (recommended) +logger.info("User subscribed successfully"); +Best Practices +Always include relevant context (userId, subscriptionId, txHash) +Use error only for actual failures +Avoid logging sensitive data (private keys, auth tokens) +Use debug for internal state inspection only +Keep logs structured (never plain strings only) +Backend Logging Coverage + +Logging should be used in: + +walletService.ts → wallet connection + transactions +notificationService.ts → reminders + alerts +subscription flows → create, pause, cancel, charge +gdpr.ts → export + delete actions +Soroban contract wrappers → transaction lifecycle +Future Enhancements + +Planned improvements to the logging system: + +Log aggregation (e.g. Datadog / Loki) +Real-time error alerting (Sentry integration) +Dashboard for subscription activity logs +On-chain ↔ off-chain log correlation mapping +``` diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 557fe5c..f95bb2d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -600,3 +600,24 @@ tags: description: Charge subscriptions, request and manage refunds - name: Queries description: Read-only data retrieval + +x-logging: + description: Structured logging system used across SubTrackr backend and client + format: + level: "debug | info | warn | error" + timestamp: ISO-8601 string + message: string + context: + correlationId: string + userId: string (optional) + subscriptionId: string (optional) + txHash: string (optional) + + correlation: + description: Used to trace a full user flow across wallet → backend → Soroban + type: string + + rules: + - Never log private keys or secrets + - Always include correlationId for subscription flows + - Errors are sent to remote logging service in production \ No newline at end of file From 6ec4021a112097a90d6bcfd40106552e81b452b1 Mon Sep 17 00:00:00 2001 From: beebozy Date: Mon, 27 Apr 2026 17:47:14 -0700 Subject: [PATCH 2/2] implemented the gas profilling contract --- contracts/Cargo.toml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 95748d3..21a7e48 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,20 +1,3 @@ -[package] -name = "subtrackr-subscription" -version = "0.2.0" -edition = "2021" -authors = ["SubTrackr Team"] -description = "SubTrackr subscription implementation contract (Soroban)" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = "21.0.0" -subtrackr-types = { path = "../types" } - -[dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } -arbitrary = { version = "1.3", features = ["derive"] } [workspace] resolver = "2" members = [ @@ -32,4 +15,4 @@ strip = "symbols" debug-assertions = false panic = "abort" codegen-units = 1 -lto = true +lto = true \ No newline at end of file