diff --git a/README.md b/README.md index 854d1f3..9c6221b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A powerful command-line interface for interacting with the Pipe distributed storage network. +> **Note**: This branch (`cleanup-warnings`) is compatible with the pipe-store `cleanup-warnings` branch. No API changes were made - only internal server warning cleanup. + ## Features - **Decentralized Storage**: Upload and download files to/from the Pipe network @@ -122,11 +124,6 @@ pipe upload-directory /path/to/folder --tier normal # Download a directory (NEW!) pipe download-directory folder ~/restored/folder --parallel 10 -# Manage referrals -pipe referral generate # Generate your referral code -pipe referral show # Show your code and stats -pipe referral apply CODE-1234 # Apply someone's referral code - # Sync directories (NEW!) pipe sync ./local/folder remote/folder # Upload sync pipe sync remote/folder ./local/folder # Download sync (limited) @@ -317,7 +314,7 @@ Configuration is stored in `~/.pipe-cli.json` by default: { "user_id": "your-user-id", "user_app_key": "your-app-key", - "api_endpoints": ["https:/us-west-00-firestarter.pipenetwork.com", "https://us-east-00-firestarter.pipenetwork.com"], + "api_endpoints": ["https://us-west-01-firestarter.pipenetwork.com"], "jwt_token": "your-jwt-token" } ``` @@ -476,8 +473,8 @@ pipe upload-directory /large/dataset --skip-uploaded ### Custom API Endpoint ```bash -# Use a different endpoint (default is https://us-west-00-firestarter.pipenetwork.com) -pipe upload-file data.csv mydata --api https://us-east-00-firestarter.pipenetwork.com +# Use a different endpoint (default is https://us-west-01-firestarter.pipenetwork.com) +pipe upload-file data.csv mydata --api https://custom-endpoint.pipenetwork.com ``` ### List Upload History @@ -594,7 +591,21 @@ Downloads are automatically base64 decoded. If you encounter issues: ## Recent Updates -### v0.3.x (Latest) +### Mainnet Release (Latest) +- **BREAKING**: Removed testnet-only features for mainnet deployment + - Removed referral system (no longer available) + - Removed SOL withdrawal commands + - Removed token withdrawal commands + - Removed `check-sol` command +- **Added**: Prepaid deposit system with one-way PIPE deposits + - `check-deposit` - View deposit balance and storage quotas + - `estimate-cost` - Estimate upload costs before uploading + - `sync-deposits` - Manually sync deposits from wallet +- **Changed**: USD-based pricing model ($10/TB = $0.01/GB) +- **Changed**: Deposits are burned as storage is used (no withdrawals) +- **Important**: This is a production mainnet release - all deposits use real PIPE tokens + +### v0.3.x - **Added**: Directory sync with `.pipe-sync` metadata tracking - **Added**: Incremental sync - only sync files that have changed - **Added**: Blake3 hash-based change detection @@ -634,52 +645,98 @@ pipe download-file large-video.mp4 pipe download-file large-video.mp4 --legacy ``` -### Referral Program +### Deposit System & Storage Management -Earn PIPE tokens by referring friends to the Pipe Network! +pipe-cli uses a prepaid deposit system where you deposit PIPE tokens that are burned as you use storage. Deposits are one-way and cannot be withdrawn. -#### How It Works +#### Check Deposit Balance -1. **Generate Your Code**: Run `pipe referral generate` to get your unique referral code -2. **Share**: Give your code to friends who want to join Pipe Network -3. **Earn**: Receive 100 PIPE tokens when they complete a qualifying swap (1+ DevNet SOL) +View your deposit balance, available storage across all tiers, and pricing information: + +```bash +pipe check-deposit +``` -#### Program Rules +This shows: +- Current deposit balance in PIPE and USD +- Live PIPE price from Jupiter +- Available storage for each tier (Normal, Priority, Premium, Ultra, Enterprise) +- Cost per GB in both PIPE and USD +- Your deposit wallet address +- Lifetime deposit and burn statistics -- **Minimum Swap**: Referred user must swap at least 1 DevNet SOL to activate reward -- **Reward Amount**: 100 PIPE tokens per successful referral -- **Processing Time**: Rewards may take up to 24 hours to process -- **Fraud Prevention**: All referrals are subject to automated fraud checks -- **DevNet SOL**: Get free DevNet SOL at [https://faucet.solana.com/](https://faucet.solana.com/) +#### Estimate Upload Cost -#### Commands +Calculate the cost to upload a file before actually uploading: ```bash -# Generate your referral code -pipe referral generate +# Estimate cost for normal tier +pipe estimate-cost video.mp4 -# Check your referral stats -pipe referral show +# Estimate cost for premium tier +pipe estimate-cost large-dataset.zip --tier premium -# Apply a referral code (for new users) -pipe referral apply USERNAME-XXXX +# Estimate cost for enterprise tier +pipe estimate-cost backup.tar.gz --tier enterprise ``` -### Token and Balance Management +The estimate shows: +- File size in GB and bytes +- Selected tier and pricing +- Estimated cost in PIPE and USD +- Your balance before and after upload +- Whether you can afford the upload -Check your balances: +#### Sync Deposits + +Manually trigger a deposit sync from your wallet to your deposit balance: ```bash -# Check PIPE token balance -pipe check-token +pipe sync-deposits +``` + +The system auto-syncs every 30 seconds, but use this for immediate confirmation after depositing PIPE to your wallet. + +#### How the Deposit System Works -# Check SOL balance -pipe check-sol +1. **Deposit PIPE**: Send PIPE tokens to your deposit wallet address (shown in `pipe check-deposit`) +2. **Sync**: Run `pipe sync-deposits` or wait 30 seconds for automatic sync +3. **Use Storage**: Upload files - costs are automatically deducted from your deposit balance +4. **Monitor**: Use `pipe check-deposit` to track remaining balance and available storage -# Swap SOL for PIPE tokens -pipe swap-sol-for-pipe 0.5 # Swap 0.5 SOL for PIPE +**Important**: Deposits are one-way by design. PIPE deposited into the system cannot be withdrawn - it's burned as you use storage. + +#### Pricing Model + +Storage is priced at **$10 USD per 1 TB**, which equals **$0.01 USD per GB**. + +The amount of PIPE you need adjusts based on market price: + +| PIPE Price | PIPE per GB | For 1 TB | +|-----------|-------------|----------| +| $0.05 USD | 0.2 PIPE | 200 PIPE | +| $0.10 USD | 0.1 PIPE | 100 PIPE | +| $0.50 USD | 0.02 PIPE | 20 PIPE | +| $1.00 USD | 0.01 PIPE | 10 PIPE | + +Tier multipliers: +- **Normal:** 1x = $0.01/GB +- **Priority:** 2.5x = $0.025/GB +- **Premium:** 5x = $0.05/GB +- **Ultra:** 10x = $0.10/GB +- **Enterprise:** 25x = $0.25/GB + +### Token Balance Management + +Check your wallet PIPE token balance: + +```bash +# Check PIPE token balance (different from deposit balance) +pipe check-token ``` +Note: This shows your wallet balance, which is different from your deposit balance. Use `pipe check-deposit` to see your prepaid storage balance. + ### Token Usage Tracking Track how your PIPE tokens are being spent on storage vs bandwidth: diff --git a/src/lib.rs b/src/lib.rs index 34c4036..4b4e9a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,7 +130,7 @@ pub struct VersionCheckResponse { pub struct Cli { #[arg( long, - default_value = "https://us-west-00-firestarter.pipenetwork.com", + default_value = "https://us-west-01-firestarter.pipenetwork.com", global = true, help = "Base URL for the Pipe Network client API" )] @@ -329,14 +329,6 @@ pub enum Commands { public_key: String, }, - /// Check SOL balance - CheckSol { - #[arg(long)] - user_id: Option, - #[arg(long)] - user_app_key: Option, - }, - /// Check custom token balance CheckToken { #[arg(long)] @@ -359,36 +351,6 @@ pub enum Commands { user_id: Option, }, - /// Swap SOL for PIPE tokens - SwapSolForPipe { - #[arg(long)] - user_id: Option, - #[arg(long)] - user_app_key: Option, - amount_sol: f64, - }, - - /// Withdraw SOL to an external Solana address - WithdrawSol { - #[arg(long)] - user_id: Option, - #[arg(long)] - user_app_key: Option, - amount_sol: f64, - to_pubkey: String, - }, - - /// Withdraw custom tokens to an external address - WithdrawCustomToken { - #[arg(long)] - user_id: Option, - #[arg(long)] - user_app_key: Option, - token_mint: String, - amount: u64, - to_pubkey: String, - }, - CreatePublicLink { #[arg(long)] user_id: Option, @@ -479,10 +441,6 @@ pub enum Commands { /// Get pricing for all upload tiers GetTierPricing, - /// Manage referral codes - #[command(subcommand)] - Referral(ReferralCommands), - PriorityUpload { #[arg(long)] user_id: Option, @@ -582,18 +540,29 @@ pub enum Commands { #[arg(long, default_value = "5")] parallel: usize, }, -} - -#[derive(Subcommand, Debug)] -pub enum ReferralCommands { - /// Generate your referral code - Generate, - /// Show your referral code and stats - Show, - /// Apply a referral code to your account - Apply { - /// The referral code to apply - code: String, + + /// Check deposit balance and storage quota + CheckDeposit { + #[arg(long)] + user_id: Option, + }, + + /// Estimate upload cost for a file + EstimateCost { + /// Path to file to estimate + file_path: String, + + #[arg(long, default_value = "normal", help = "Upload tier: normal, priority, premium, ultra, enterprise")] + tier: String, + + #[arg(long)] + user_id: Option, + }, + + /// Manually sync deposits from wallet to deposit balance + SyncDeposits { + #[arg(long)] + user_id: Option, }, } @@ -673,70 +642,72 @@ pub struct CheckCustomTokenResponse { pub ui_amount: f64, } -#[derive(Serialize, Deserialize)] -pub struct SwapSolForPipeRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub user_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub user_app_key: Option, - pub amount_sol: f64, +#[derive(Serialize, Deserialize, Debug)] +pub struct PriorityFeeResponse { + pub priority_fee_per_gb: f64, } #[derive(Serialize, Deserialize, Debug)] -pub struct SwapSolForPipeResponse { - pub user_id: String, - pub sol_spent: f64, - pub tokens_minted: u64, +pub struct GetTierPricingResponse { + pub normal_fee_per_gb: f64, + pub priority_fee_per_gb: f64, + pub premium_fee_per_gb: f64, + pub ultra_fee_per_gb: f64, + pub enterprise_fee_per_gb: f64, } -#[derive(Serialize, Deserialize)] -pub struct WithdrawSolRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub user_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub user_app_key: Option, - pub to_pubkey: String, - pub amount_sol: f64, +// Deposit system structures +#[derive(Serialize, Deserialize, Debug)] +pub struct DepositBalanceResponse { + pub user_id: String, + pub deposit_balance_lamports: u64, + pub deposit_balance_pipe: f64, + pub total_deposited: u64, + pub total_burned: u64, + pub storage_quota: StorageQuota, + pub last_deposit_at: Option, + pub wallet_address: String, } #[derive(Serialize, Deserialize, Debug)] -pub struct WithdrawSolResponse { - pub user_id: String, - pub to_pubkey: String, - pub amount_sol: f64, - pub signature: String, +pub struct StorageQuota { + pub deposit_balance_lamports: u64, + pub deposit_balance_pipe: f64, + pub pipe_price_usd: f64, + pub tier_estimates: Vec, } -#[derive(Serialize, Deserialize)] -pub struct WithdrawTokenRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub user_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub user_app_key: Option, - pub to_pubkey: String, - pub amount: u64, +#[derive(Serialize, Deserialize, Debug)] +pub struct TierEstimate { + pub tier_name: String, + pub cost_per_gb_pipe: f64, + pub cost_per_gb_usd: f64, + pub available_gb: f64, } #[derive(Serialize, Deserialize, Debug)] -pub struct WithdrawTokenResponse { - pub user_id: String, - pub to_pubkey: String, - pub amount: u64, - pub signature: String, +pub struct EstimateUploadRequest { + pub file_size_bytes: u64, + pub tier: Option, } #[derive(Serialize, Deserialize, Debug)] -pub struct PriorityFeeResponse { - pub priority_fee_per_gb: f64, +pub struct UploadEstimate { + pub file_size_bytes: u64, + pub tier_name: String, + pub estimated_cost_lamports: u64, + pub estimated_cost_pipe: f64, + pub estimated_cost_usd: f64, + pub can_afford: bool, + pub remaining_balance_pipe: f64, } #[derive(Serialize, Deserialize, Debug)] -pub struct GetTierPricingResponse { - pub normal_fee_per_gb: f64, - pub priority_fee_per_gb: f64, - pub premium_fee_per_gb: f64, - pub ultra_fee_per_gb: f64, - pub enterprise_fee_per_gb: f64, +pub struct SyncDepositsResponse { + pub success: bool, + pub deposited_amount: u64, + pub deposited_pipe: f64, + pub message: String, } #[derive(Serialize, Deserialize)] @@ -1542,7 +1513,10 @@ where // Check if it's a retryable error let is_rate_limit = error_str.contains("429") || error_str.contains("Too Many Requests"); - let is_transient_error = error_str.contains("500") && ( + // Treat common upstream errors as transient (nginx/backends) + let is_5xx_gateway = error_str.contains("502") || error_str.contains("503") || error_str.contains("504") + || error_str.contains("Bad Gateway") || error_str.contains("Service Unavailable") || error_str.contains("Gateway Timeout"); + let is_transient_500 = error_str.contains("500") && ( error_str.contains("Failed to flush buffer") || error_str.contains("Failed to write to file") || error_str.contains("Storage full") || @@ -1551,6 +1525,7 @@ where error_str.contains("timed out") || error_str.contains("broken") ); + let is_transient_error = is_5xx_gateway || is_transient_500; if is_rate_limit || is_transient_error { if retry_count >= MAX_RETRIES { @@ -2887,7 +2862,7 @@ async fn upload_file_with_shared_progress( return Err(anyhow!("Upload failed: {}", message)); } } - return Err(anyhow!("Upload failed: Insufficient tokens. Please use 'pipe swap-sol-for-pipe' to get more tokens.")); + return Err(anyhow!("Upload failed: Insufficient tokens. Please contact support or add more PIPE tokens to your account.")); } // Provide more user-friendly error messages for common server errors @@ -3112,7 +3087,7 @@ async fn upload_file_priority_with_shared_progress( return Err(anyhow!("Priority upload failed: {}", message)); } } - return Err(anyhow!("Priority upload failed: Insufficient tokens. Please use 'pipe swap-sol-for-pipe' to get more tokens.")); + return Err(anyhow!("Priority upload failed: Insufficient tokens. Please contact support or add more PIPE tokens to your account.")); } // Provide more user-friendly error messages for common server errors @@ -3461,12 +3436,8 @@ pub async fn run_cli() -> Result<()> { | Commands::DownloadFile { .. } | Commands::DeleteFile { .. } | Commands::FileInfo { .. } - | Commands::CheckSol { .. } | Commands::CheckToken { .. } | Commands::TokenUsage { .. } - | Commands::SwapSolForPipe { .. } - | Commands::WithdrawSol { .. } - | Commands::WithdrawCustomToken { .. } | Commands::CreatePublicLink { .. } | Commands::DeletePublicLink { .. } | Commands::PublicDownload { .. } @@ -4042,7 +4013,7 @@ pub async fn run_cli() -> Result<()> { if current_balance < estimated_cost { println!("⚠️ Insufficient balance!"); println!(" Need {:.4} more PIPE tokens", estimated_cost - current_balance); - println!("\n Run: pipe swap-sol-for-pipe {:.1}", (estimated_cost - current_balance) / 10.0 + 0.1); + println!("\n Please contact support to add more PIPE tokens to your account."); } else { println!("✅ Sufficient balance for upload"); } @@ -4378,57 +4349,6 @@ pub async fn run_cli() -> Result<()> { println!("feature is not yet implemented in pipe-cli."); } - Commands::CheckSol { - user_id, - user_app_key, - } => { - // Load credentials and check for JWT - let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { - anyhow!("No credentials found. Please create a user or login first.") - })?; - - // Ensure we have valid JWT token if available - ensure_valid_token(&client, base_url, &mut creds, config_path).await?; - - // Override with command-line args if provided (only for legacy auth) - if let Some(uid) = user_id { - creds.user_id = uid; - } - if let Some(key) = user_app_key { - creds.user_app_key = key; - } - - let mut request = client.post(format!("{}/checkWallet", base_url)); - - // Use add_auth_headers for consistent authentication - request = add_auth_headers(request, &creds, false); - - // Always send empty body - auth is in headers - let req_body = CheckWalletRequest { - user_id: None, - user_app_key: None, - }; - request = request.json(&req_body); - - let resp = request.send().await?; - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - let json = serde_json::from_str::(&text_body)?; - println!( - "SOL Balance for user: {}\nPubkey: {}\nLamports: {}\nSOL: {}", - json.user_id, json.public_key, json.balance_lamports, json.balance_sol - ); - } else { - return Err(anyhow!( - "Check SOL balance failed. Status = {}, Body = {}", - status, - text_body - )); - } - } - Commands::CheckToken { user_id, user_app_key, @@ -4468,7 +4388,7 @@ pub async fn run_cli() -> Result<()> { if status.is_success() { let json = serde_json::from_str::(&text_body)?; println!( - "Token Balance for user: {}\nPubkey: {}\nMint: {}\nAmount: {}\nUI: {}", + "Token Balance for user: {}\nPubkey: {}\nMint: {}\nAmount: {}\nPIPE: {}", json.user_id, json.public_key, json.token_mint, json.amount, json.ui_amount ); } else { @@ -4633,171 +4553,6 @@ pub async fn run_cli() -> Result<()> { } } - Commands::SwapSolForPipe { - user_id, - user_app_key, - amount_sol, - } => { - // Load credentials and check for JWT - let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { - anyhow!("No credentials found. Please create a user or login first.") - })?; - - // Ensure we have valid JWT token if available - ensure_valid_token(&client, base_url, &mut creds, config_path).await?; - - // Override with command-line args if provided (only for legacy auth) - if let Some(uid) = user_id { - creds.user_id = uid; - } - if let Some(key) = user_app_key { - creds.user_app_key = key; - } - - let mut request = client.post(format!("{}/exchangeSolForTokens", base_url)); - - // Use add_auth_headers for consistent authentication - request = add_auth_headers(request, &creds, true); - - // Always send only amount - auth is in headers - let req_body = SwapSolForPipeRequest { - user_id: None, - user_app_key: None, - amount_sol, - }; - request = request.json(&req_body); - - let resp = request.send().await?; - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - let json = serde_json::from_str::(&text_body)?; - println!( - "Swap SOL -> PIPE complete!\nUser: {}\nSOL spent: {}\nPIPE minted: {}", - json.user_id, json.sol_spent, json.tokens_minted - ); - } else { - return Err(anyhow!( - "SwapSolForPipe failed. Status = {}, Body = {}", - status, - text_body - )); - } - } - - Commands::WithdrawSol { - user_id, - user_app_key, - amount_sol, - to_pubkey, - } => { - // Load credentials and check for JWT - let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { - anyhow!("No credentials found. Please create a user or login first.") - })?; - - // Ensure we have valid JWT token if available - ensure_valid_token(&client, base_url, &mut creds, config_path).await?; - - // Override with command-line args if provided (only for legacy auth) - if let Some(uid) = user_id { - creds.user_id = uid; - } - if let Some(key) = user_app_key { - creds.user_app_key = key; - } - - let mut request = client.post(format!("{}/withdrawSol", base_url)); - - // Use add_auth_headers for consistent authentication - request = add_auth_headers(request, &creds, true); - - // Always send withdrawal details only - auth is in headers - let req_body = WithdrawSolRequest { - user_id: None, - user_app_key: None, - amount_sol, - to_pubkey, - }; - request = request.json(&req_body); - - let resp = request.send().await?; - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - let json = serde_json::from_str::(&text_body)?; - println!( - "SOL Withdrawal complete!\nUser: {}\nTo: {}\nAmount SOL: {}\nSignature: {}", - json.user_id, json.to_pubkey, json.amount_sol, json.signature - ); - } else { - return Err(anyhow!( - "Withdraw SOL failed. Status = {}, Body = {}", - status, - text_body - )); - } - } - - Commands::WithdrawCustomToken { - user_id, - user_app_key, - token_mint, - amount, - to_pubkey, - } => { - // Load credentials and check for JWT - let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { - anyhow!("No credentials found. Please create a user or login first.") - })?; - - // Ensure we have valid JWT token if available - ensure_valid_token(&client, base_url, &mut creds, config_path).await?; - - // Override with command-line args if provided (only for legacy auth) - if let Some(uid) = user_id { - creds.user_id = uid; - } - if let Some(key) = user_app_key { - creds.user_app_key = key; - } - - let mut request = client.post(format!("{}/withdrawToken", base_url)); - - // Use add_auth_headers for consistent authentication - request = add_auth_headers(request, &creds, true); - - // Always send withdrawal details only - auth is in headers - let req_body = WithdrawTokenRequest { - user_id: None, - user_app_key: None, - to_pubkey, - amount, - }; - request = request.json(&req_body); - - let resp = request.send().await?; - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - let json = serde_json::from_str::(&text_body)?; - println!( - "Token Withdrawal complete!\nUser: {}\nTo: {}\nAmount: {}\nSignature: {}", - json.user_id, json.to_pubkey, json.amount, json.signature - ); - println!("Token mint used: {}", token_mint); - } else { - return Err(anyhow!( - "Withdraw custom token failed. Status = {}, Body = {}", - status, - text_body - )); - } - } - Commands::CreatePublicLink { user_id, user_app_key, @@ -5166,7 +4921,7 @@ pub async fn run_cli() -> Result<()> { "Needed: {:.4} PIPE tokens", total_cost_estimate - current_balance ); - eprintln!("\nPlease use 'pipe swap-sol-for-pipe {:.1}' to get enough tokens.", + eprintln!("\nPlease contact support to add more PIPE tokens to your account. You need approximately {:.1} PIPE tokens.", (total_cost_estimate - current_balance) / 10.0 + 0.1); return Ok(()); } @@ -5246,12 +5001,15 @@ pub async fn run_cli() -> Result<()> { let failed_count = Arc::new(TokioMutex::new(0u32)); let total_cost = Arc::new(TokioMutex::new(0.0f64)); + // Share credentials across tasks so we can refresh tokens mid-run + let shared_creds = Arc::new(TokioMutex::new(creds)); + for path in file_entries { let sem_clone = Arc::clone(&sem); let client_clone = client.clone(); let base_url_clone = base_url.to_string(); let service_cache_clone = service_cache.clone(); - let creds_clone = creds.clone(); + let shared_creds_clone = shared_creds.clone(); let shared_progress_clone = shared_progress.clone(); let completed_clone = completed_count.clone(); let failed_clone = failed_count.clone(); @@ -5274,12 +5032,16 @@ pub async fn run_cli() -> Result<()> { let _permit = sem_clone.acquire_owned().await.unwrap(); // Get endpoint for this specific file upload + let user_id_owned = { + let creds_guard = shared_creds_clone.lock().await; + creds_guard.user_id.clone() + }; let selected_endpoint = get_endpoint_for_operation( &service_cache_clone, &client_clone, &base_url_clone, "upload", - &creds_clone.user_id, + &user_id_owned, Some(&rel_path), ) .await; @@ -5306,16 +5068,57 @@ pub async fn run_cli() -> Result<()> { // Use retry wrapper for directory uploads let upload_result = upload_with_retry(&format!("upload of {}", rel_path), || { - upload_file_with_encryption( - &client_clone, - &path, - &url, - &rel_path, - &creds_clone, - encrypt_clone, - password_clone.clone(), - Some(shared_progress_clone.clone()), - ) + let client_inner = client_clone.clone(); + let base_url_inner = base_url_clone.clone(); + let shared_creds_inner = shared_creds_clone.clone(); + let path_inner = path.clone(); + let url_inner = url.clone(); + let rel_inner = rel_path.clone(); + let shared_prog_inner = shared_progress_clone.clone(); + let pwd_inner = password_clone.clone(); + async move { + // Ensure token valid preflight + let mut creds_guard = shared_creds_inner.lock().await; + let _ = ensure_valid_token(&client_inner, &base_url_inner, &mut *creds_guard, None).await; + let creds_snapshot = creds_guard.clone(); + drop(creds_guard); + + // Attempt upload + match upload_file_with_encryption( + &client_inner, + &path_inner, + &url_inner, + &rel_inner, + &creds_snapshot, + encrypt_clone, + pwd_inner.clone(), + Some(shared_prog_inner.clone()), + ).await { + Ok(ok) => Ok(ok), + Err(e) => { + let es = e.to_string(); + if es.contains("401") || es.contains("Unauthorized") || es.contains("Authentication required") { + // Refresh and retry once + let mut creds_guard2 = shared_creds_inner.lock().await; + let _ = ensure_valid_token(&client_inner, &base_url_inner, &mut *creds_guard2, None).await; + let creds_snapshot2 = creds_guard2.clone(); + drop(creds_guard2); + upload_file_with_encryption( + &client_inner, + &path_inner, + &url_inner, + &rel_inner, + &creds_snapshot2, + encrypt_clone, + pwd_inner.clone(), + Some(shared_prog_inner.clone()), + ).await + } else { + Err(e) + } + } + } + } }) .await; @@ -5530,7 +5333,7 @@ pub async fn run_cli() -> Result<()> { "Needed: {:.4} PIPE tokens", total_cost_estimate - current_balance ); - eprintln!("\nPlease use 'pipe swap-sol-for-pipe {:.1}' to get enough tokens.", + eprintln!("\nPlease contact support to add more PIPE tokens to your account. You need approximately {:.1} PIPE tokens.", (total_cost_estimate - current_balance) / 10.0 + 0.1); return Ok(()); } @@ -5603,12 +5406,15 @@ pub async fn run_cli() -> Result<()> { let failed_count = Arc::new(TokioMutex::new(0u32)); let total_cost = Arc::new(TokioMutex::new(0.0f64)); + // Share credentials across tasks for auto-refresh + let shared_creds = Arc::new(TokioMutex::new(creds)); + for path in file_entries { let sem_clone = Arc::clone(&sem); let client_clone = client.clone(); let base_url_clone = base_url.to_string(); let service_cache_clone = service_cache.clone(); - let creds_clone = creds.clone(); + let shared_creds_clone = shared_creds.clone(); let shared_progress_clone = shared_progress.clone(); let completed_clone = completed_count.clone(); let failed_clone = failed_count.clone(); @@ -5628,12 +5434,16 @@ pub async fn run_cli() -> Result<()> { let _permit = sem_clone.acquire_owned().await.unwrap(); // Get endpoint for this specific file upload + let user_id_owned = { + let creds_guard = shared_creds_clone.lock().await; + creds_guard.user_id.clone() + }; let selected_endpoint = get_endpoint_for_operation( &service_cache_clone, &client_clone, &base_url_clone, "upload", - &creds_clone.user_id, + &user_id_owned, Some(&rel_path), ) .await; @@ -5647,14 +5457,49 @@ pub async fn run_cli() -> Result<()> { // Use retry wrapper for priority directory uploads let upload_result = upload_with_retry(&format!("priority upload of {}", rel_path), || { - upload_file_priority_with_shared_progress( - &client_clone, - &path, - &url, - &rel_path, - &creds_clone, - Some(shared_progress_clone.clone()), - ) + let client_inner = client_clone.clone(); + let base_url_inner = base_url_clone.clone(); + let shared_creds_inner = shared_creds_clone.clone(); + let path_inner = path.clone(); + let url_inner = url.clone(); + let rel_inner = rel_path.clone(); + let shared_prog_inner = shared_progress_clone.clone(); + async move { + // Preflight refresh + let mut creds_guard = shared_creds_inner.lock().await; + let _ = ensure_valid_token(&client_inner, &base_url_inner, &mut *creds_guard, None).await; + let creds_snapshot = creds_guard.clone(); + drop(creds_guard); + match upload_file_priority_with_shared_progress( + &client_inner, + &path_inner, + &url_inner, + &rel_inner, + &creds_snapshot, + Some(shared_prog_inner.clone()), + ).await { + Ok(ok) => Ok(ok), + Err(e) => { + let es = e.to_string(); + if es.contains("401") || es.contains("Unauthorized") || es.contains("Authentication required") { + let mut creds_guard2 = shared_creds_inner.lock().await; + let _ = ensure_valid_token(&client_inner, &base_url_inner, &mut *creds_guard2, None).await; + let creds_snapshot2 = creds_guard2.clone(); + drop(creds_guard2); + upload_file_priority_with_shared_progress( + &client_inner, + &path_inner, + &url_inner, + &rel_inner, + &creds_snapshot2, + Some(shared_prog_inner.clone()), + ).await + } else { + Err(e) + } + } + } + } }) .await; @@ -5886,7 +5731,7 @@ pub async fn run_cli() -> Result<()> { if current_balance < estimated_cost { println!("⚠️ Insufficient balance!"); println!(" Need {:.4} more PIPE tokens", estimated_cost - current_balance); - println!("\n Run: pipe swap-sol-for-pipe {:.1}", (estimated_cost - current_balance) / 10.0 + 0.1); + println!("\n Please contact support to add more PIPE tokens to your account."); } else { println!("✅ Sufficient balance for upload"); } @@ -6231,6 +6076,235 @@ pub async fn run_cli() -> Result<()> { ) .await?; } + + Commands::CheckDeposit { user_id } => { + // Load credentials and check for JWT + let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { + anyhow!("No credentials found. Please create a user or login first.") + })?; + + // Ensure we have valid JWT token if available + ensure_valid_token(&client, base_url, &mut creds, config_path).await?; + + // Override with command-line args if provided + if let Some(uid) = user_id { + creds.user_id = uid; + } + + let mut request = client.get(format!("{}/deposit/balance", base_url)); + request = add_auth_headers(request, &creds, false); + + let resp = request.send().await?; + let status = resp.status(); + let text_body = resp.text().await?; + + if status.is_success() { + let json = serde_json::from_str::(&text_body)?; + + println!("╔══════════════════════════════════════════════════════════════╗"); + println!("║ 💰 DEPOSIT BALANCE ║"); + println!("╚══════════════════════════════════════════════════════════════╝"); + println!(); + println!("User: {}", json.user_id); + println!("Wallet Address: {}", json.wallet_address); + println!(); + println!("💵 Deposit Balance: {:.4} PIPE (${:.2} USD)", + json.deposit_balance_pipe, + json.deposit_balance_pipe * json.storage_quota.pipe_price_usd); + println!("📊 PIPE Price: ${:.6} USD", json.storage_quota.pipe_price_usd); + println!(); + println!("📦 Available Storage:"); + println!("┌──────────────┬────────────────┬──────────────┬──────────────┐"); + println!("│ Tier │ Available GB │ PIPE/GB │ USD/GB │"); + println!("├──────────────┼────────────────┼──────────────┼──────────────┤"); + + for tier in &json.storage_quota.tier_estimates { + println!("│ {:<12} │ {:>12.2} GB│ {:>10.4} │ ${:>11.4} │", + tier.tier_name, + tier.available_gb, + tier.cost_per_gb_pipe, + tier.cost_per_gb_usd + ); + } + + println!("└──────────────┴────────────────┴──────────────┴──────────────┘"); + println!(); + println!("📊 Statistics:"); + println!(" Total Deposited: {:.4} PIPE", json.total_deposited as f64 / 1e9); + println!(" Total Burned: {:.4} PIPE", json.total_burned as f64 / 1e9); + + if let Some(last_deposit) = json.last_deposit_at { + println!(" Last Deposit: {}", last_deposit); + } + + println!(); + println!("💡 To deposit more PIPE:"); + println!(" Send PIPE tokens to: {}", json.wallet_address); + println!(" Then run: pipe sync-deposits"); + + } else { + return Err(anyhow!( + "Check deposit failed. Status = {}, Body = {}", + status, + text_body + )); + } + } + + Commands::EstimateCost { file_path, tier, user_id } => { + // Load credentials and check for JWT + let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { + anyhow!("No credentials found. Please create a user or login first.") + })?; + + // Ensure we have valid JWT token if available + ensure_valid_token(&client, base_url, &mut creds, config_path).await?; + + // Override with command-line args if provided + if let Some(uid) = user_id { + creds.user_id = uid; + } + + // Get file size + let metadata = tokio::fs::metadata(&file_path).await?; + let file_size = metadata.len(); + + let mut request = client.post(format!("{}/deposit/estimate", base_url)); + request = add_auth_headers(request, &creds, false); + + let req_body = EstimateUploadRequest { + file_size_bytes: file_size, + tier: Some(tier.clone()), + }; + request = request.json(&req_body); + + let resp = request.send().await?; + let status = resp.status(); + let text_body = resp.text().await?; + + if status.is_success() { + let estimate: UploadEstimate = serde_json::from_str(&text_body)?; + + println!("╔══════════════════════════════════════════════════════════════╗"); + println!("║ 📊 UPLOAD COST ESTIMATE ║"); + println!("╚══════════════════════════════════════════════════════════════╝"); + println!(); + println!("📁 File: {}", file_path); + println!("📏 Size: {:.2} GB ({} bytes)", file_size as f64 / 1_073_741_824.0, file_size); + println!("🎯 Tier: {}", estimate.tier_name); + println!(); + println!("💰 Estimated Cost:"); + println!(" {:.6} PIPE (${:.4} USD)", + estimate.estimated_cost_pipe, + estimate.estimated_cost_usd); + println!(); + println!("💳 Your Balance:"); + let current_balance = estimate.remaining_balance_pipe + estimate.estimated_cost_pipe; + println!(" Before Upload: {:.6} PIPE", current_balance); + println!(" After Upload: {:.6} PIPE", estimate.remaining_balance_pipe); + println!(); + + if estimate.can_afford { + println!("✅ Status: CAN AFFORD"); + println!(); + println!(" You have sufficient deposit balance for this upload."); + } else { + println!("❌ Status: INSUFFICIENT BALANCE"); + let needed = estimate.estimated_cost_pipe - current_balance; + println!(); + println!(" You need {:.6} more PIPE to upload this file.", needed); + println!(" Current balance: {:.6} PIPE", current_balance); + println!(" Required: {:.6} PIPE", estimate.estimated_cost_pipe); + } + + } else { + return Err(anyhow!( + "Estimate cost failed. Status = {}, Body = {}", + status, + text_body + )); + } + } + + Commands::SyncDeposits { user_id } => { + // Load credentials and check for JWT + let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { + anyhow!("No credentials found. Please create a user or login first.") + })?; + + // Ensure we have valid JWT token if available + ensure_valid_token(&client, base_url, &mut creds, config_path).await?; + + // Override with command-line args if provided + if let Some(uid) = user_id { + creds.user_id = uid; + } + + println!("🔄 Syncing deposits from wallet..."); + println!(); + + let mut request = client.post(format!("{}/deposit/sync", base_url)); + request = add_auth_headers(request, &creds, false); + + let resp = request.send().await?; + let status = resp.status(); + let text_body = resp.text().await?; + + if status.is_success() { + let result: SyncDepositsResponse = serde_json::from_str(&text_body)?; + + if result.success { + if result.deposited_pipe > 0.0 { + println!("✅ New deposit detected!"); + println!(); + println!(" Amount: {:.6} PIPE (${:.4} USD)", + result.deposited_pipe, + result.deposited_amount as f64 / 1e9 * 0.10); // Approximate USD + println!(); + + // Get updated balance + let balance_req = client.get(format!("{}/deposit/balance", base_url)); + let balance_req = add_auth_headers(balance_req, &creds, false); + + if let Ok(balance_resp) = balance_req.send().await { + if let Ok(balance_text) = balance_resp.text().await { + if let Ok(balance) = serde_json::from_str::(&balance_text) { + println!("💰 Updated Balance: {:.6} PIPE (${:.2} USD)", + balance.deposit_balance_pipe, + balance.deposit_balance_pipe * balance.storage_quota.pipe_price_usd); + + // Show storage for Normal tier + if let Some(normal) = balance.storage_quota.tier_estimates.first() { + println!("📦 Storage Available: {:.2} GB (Normal tier)", + normal.available_gb); + } + } + } + } + } else { + println!("ℹ️ No new deposits found"); + println!(); + println!(" {}", result.message); + } + + println!(); + println!("💡 To deposit PIPE:"); + println!(" 1. Run: pipe check-deposit"); + println!(" 2. Send PIPE to your wallet address"); + println!(" 3. Wait 30 seconds or run: pipe sync-deposits"); + + } else { + return Err(anyhow!("Sync failed: {}", result.message)); + } + + } else { + return Err(anyhow!( + "Sync deposits failed. Status = {}, Body = {}", + status, + text_body + )); + } + } Commands::EncryptLocal { input_file, @@ -6624,139 +6698,6 @@ pub async fn run_cli() -> Result<()> { println!(" The file may have been modified or signed with a different key"); } } - - Commands::Referral(subcmd) => { - // Load credentials - let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { - anyhow!("No credentials found. Please create a user or login first.") - })?; - - // Ensure we have valid JWT token - ensure_valid_token(&client, base_url, &mut creds, config_path).await?; - - // Get the JWT token - let jwt_token = creds.auth_tokens.as_ref() - .ok_or_else(|| anyhow!("No authentication tokens found."))? - .access_token.clone(); - - match subcmd { - ReferralCommands::Generate => { - let resp = client - .post(format!("{}/api/referral/generate", base_url)) - .header("Authorization", format!("Bearer {}", jwt_token)) - .send() - .await?; - - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - let response: serde_json::Value = serde_json::from_str(&text_body)?; - let code = response["code"].as_str().unwrap_or("Unknown"); - let existing = response["existing"].as_bool().unwrap_or(false); - - if existing { - println!("Your existing referral code: {}", code); - } else { - println!("🎉 Your new referral code: {}", code); - } - - println!("\n📋 Referral Program Rules:"); - println!(" • Share this code with friends who want to join Pipe Network"); - println!(" • They must swap at least 1 DevNet SOL to activate your reward"); - println!(" • You receive 100 PIPE tokens per successful referral"); - println!(" • Rewards are subject to fraud prevention checks"); - println!(" • Processing may take up to 24 hours"); - println!("\n💡 Get free DevNet SOL at: https://faucet.solana.com/"); - } else { - return Err(anyhow!( - "Failed to generate referral code. Status={}, Body={}", - status, - text_body - )); - } - } - - ReferralCommands::Show => { - // Get the code - let code_resp = client - .get(format!("{}/api/referral/my-code", base_url)) - .header("Authorization", format!("Bearer {}", jwt_token)) - .send() - .await; - - match code_resp { - Ok(resp) if resp.status().is_success() => { - let text_body = resp.text().await?; - let response: serde_json::Value = serde_json::from_str(&text_body)?; - let code = response["code"].as_str().unwrap_or("Unknown"); - println!("Your referral code: {}", code); - - // Get stats - let stats_resp = client - .get(format!("{}/api/referral/stats", base_url)) - .header("Authorization", format!("Bearer {}", jwt_token)) - .send() - .await?; - - if stats_resp.status().is_success() { - let stats_body = stats_resp.text().await?; - let stats: serde_json::Value = serde_json::from_str(&stats_body)?; - - println!("\n📊 Referral Statistics:"); - println!(" Total uses: {}", stats["total_uses"]); - println!(" Successful referrals: {}", stats["successful_referrals"]); - println!(" Pending referrals: {}", stats["pending_referrals"]); - println!(" Total PIPE earned: {}", stats["total_pipe_earned"]); - - println!("\n📋 Referral Program Rules:"); - println!(" • Referred user must swap at least 1 DevNet SOL to activate reward"); - println!(" • You receive 100 PIPE tokens per successful referral"); - println!(" • Rewards are subject to fraud prevention checks"); - println!(" • Processing may take up to 24 hours"); - println!("\n💡 Get free DevNet SOL at: https://faucet.solana.com/"); - } - } - _ => { - println!("You don't have a referral code yet. Generate one with 'pipe referral generate'"); - } - } - } - - ReferralCommands::Apply { code } => { - let req_body = serde_json::json!({ "code": code }); - let resp = client - .post(format!("{}/api/referral/apply", base_url)) - .header("Authorization", format!("Bearer {}", jwt_token)) - .json(&req_body) - .send() - .await?; - - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - let response: serde_json::Value = serde_json::from_str(&text_body)?; - if response["success"].as_bool().unwrap_or(false) { - println!("✅ {}", response["message"].as_str().unwrap_or("Referral code applied successfully!")); - println!("\nℹ️ Important: To activate the referral reward for your referrer:"); - println!(" • You must complete a swap of at least 1 DevNet SOL"); - println!(" • Your referrer will receive 100 PIPE tokens"); - println!(" • Use 'pipe swap-sol-for-pipe' to get started"); - println!("\n💡 Need DevNet SOL? Get it free at: https://faucet.solana.com/"); - } else { - println!("❌ {}", response["message"].as_str().unwrap_or("Failed to apply referral code")); - } - } else { - return Err(anyhow!( - "Failed to apply referral code. Status={}, Body={}", - status, - text_body - )); - } - } - } - } } Ok(())