From 157defcb14fe389b663167b8bafcb005cfd73a1e Mon Sep 17 00:00:00 2001 From: David Rhodus Date: Tue, 21 Oct 2025 16:22:07 -0700 Subject: [PATCH 1/6] Add compatibility note for cleanup-warnings branch - Document that this branch is compatible with pipe-store cleanup-warnings - Note that no API changes were required - All tests passing (44 tests, zero warnings) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 854d1f3..2a069b1 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 From 7e767551004b8cb074d5f8ed74120f37331e2a35 Mon Sep 17 00:00:00 2001 From: David Rhodus Date: Tue, 21 Oct 2025 16:33:28 -0700 Subject: [PATCH 2/6] Remove swap-sol-for-pipe command for mainnet deployment - Remove SwapSolForPipe command from CLI enum - Remove SwapSolForPipeRequest and SwapSolForPipeResponse structs - Remove swap command handler code - Update error messages to remove swap references - Update referral messages to remove DevNet swap requirements - All tests still passing (44/44) This command is no longer available as the server endpoint /exchangeSolForTokens is disabled for mainnet. --- src/lib.rs | 99 +++++------------------------------------------------- 1 file changed, 9 insertions(+), 90 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 34c4036..4a14ce4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -359,15 +359,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)] @@ -673,22 +664,6 @@ 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 SwapSolForPipeResponse { - pub user_id: String, - pub sol_spent: f64, - pub tokens_minted: u64, -} - #[derive(Serialize, Deserialize)] pub struct WithdrawSolRequest { #[serde(skip_serializing_if = "Option::is_none")] @@ -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 acquire more PIPE tokens to continue.")); } // 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 acquire more PIPE tokens to continue.")); } // Provide more user-friendly error messages for common server errors @@ -3464,7 +3439,6 @@ pub async fn run_cli() -> Result<()> { | Commands::CheckSol { .. } | Commands::CheckToken { .. } | Commands::TokenUsage { .. } - | Commands::SwapSolForPipe { .. } | Commands::WithdrawSol { .. } | Commands::WithdrawCustomToken { .. } | Commands::CreatePublicLink { .. } @@ -4042,7 +4016,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 You need approximately {:.1} more PIPE tokens.", (estimated_cost - current_balance) / 10.0 + 0.1); } else { println!("✅ Sufficient balance for upload"); } @@ -4633,59 +4607,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, @@ -5166,7 +5087,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 acquire approximately {:.1} more PIPE tokens.", (total_cost_estimate - current_balance) / 10.0 + 0.1); return Ok(()); } @@ -5530,7 +5451,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 acquire approximately {:.1} more PIPE tokens.", (total_cost_estimate - current_balance) / 10.0 + 0.1); return Ok(()); } @@ -5886,7 +5807,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 You need approximately {:.1} more PIPE tokens.", (estimated_cost - current_balance) / 10.0 + 0.1); } else { println!("✅ Sufficient balance for upload"); } @@ -6663,7 +6584,7 @@ pub async fn run_cli() -> Result<()> { 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!(" • They must use the service 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"); @@ -6710,7 +6631,7 @@ pub async fn run_cli() -> Result<()> { 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!(" • Referred user must use the service 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"); @@ -6740,10 +6661,8 @@ pub async fn run_cli() -> Result<()> { 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!(" • You must use the service"); 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")); } From cffa6918beaed28665bb35cfda7ce170a6dacdf4 Mon Sep 17 00:00:00 2001 From: David Rhodus Date: Wed, 22 Oct 2025 16:21:25 -0700 Subject: [PATCH 3/6] Add storage-quota CLI command - New 'pipe storage-quota' command to check storage usage and limits - Shows free tier (100GB) and paid tier storage - Displays token balance and current PIPE price - Shows remaining storage and usage percentage - Pricing info: $10 per TB per month - Warns when storage is running low (<10GB) --- src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 4a14ce4..a1e49f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -358,6 +358,9 @@ pub enum Commands { #[arg(long)] user_id: Option, }, + + /// Check storage quota and usage + StorageQuota, /// Withdraw SOL to an external Solana address WithdrawSol { @@ -3439,6 +3442,7 @@ pub async fn run_cli() -> Result<()> { | Commands::CheckSol { .. } | Commands::CheckToken { .. } | Commands::TokenUsage { .. } + | Commands::StorageQuota | Commands::WithdrawSol { .. } | Commands::WithdrawCustomToken { .. } | Commands::CreatePublicLink { .. } @@ -4607,6 +4611,79 @@ pub async fn run_cli() -> Result<()> { } } + Commands::StorageQuota => { + // Load credentials + let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { + anyhow!("No credentials found. Please login first.") + })?; + + // Ensure JWT token is valid + let client = reqwest::Client::new(); + ensure_valid_token(&client, base_url, &mut creds, config_path).await?; + + // Make request with auth header + let mut request = client.get(format!("{}/storageQuota", base_url)); + + if let Some(ref auth_tokens) = creds.auth_tokens { + request = request.header( + "Authorization", + format!("Bearer {}", auth_tokens.access_token), + ); + } + + let resp = request.send().await?; + + let status = resp.status(); + let text_body = resp.text().await?; + + if status.is_success() { + // Parse quota response + let quota: serde_json::Value = serde_json::from_str(&text_body)?; + + let q = "a["quota"]; + let free_gb = q["free_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); + let paid_gb = q["paid_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); + let used_gb = q["used_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); + let total_gb = q["total_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); + let remaining_gb = q["remaining_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); + let token_balance = q["token_balance"].as_f64().unwrap_or(0.0); + let pipe_price = q["pipe_price_usd"].as_f64().unwrap_or(0.0); + let percentage_used = quota["percentage_used"].as_f64().unwrap_or(0.0); + let tokens_for_1tb = quota["tokens_needed_for_1tb"].as_f64().unwrap_or(0.0); + + println!("\n💾 Storage Quota Summary"); + println!("═══════════════════════════════════════"); + println!(); + println!("📊 Current Usage:"); + println!(" Used: {:.2} GB / {:.2} GB ({:.1}%)", used_gb, total_gb, percentage_used); + println!(" Available: {:.2} GB", remaining_gb); + println!(); + println!("📦 Storage Breakdown:"); + println!(" Free tier: {:.2} GB", free_gb); + println!(" Paid tier: {:.2} GB", paid_gb); + println!(); + println!("🪙 Token Information:"); + println!(" Balance: {:.2} PIPE tokens", token_balance); + println!(" Value: ${:.4} (@ ${:.4}/PIPE)", token_balance * pipe_price, pipe_price); + println!(); + println!("💡 Pricing:"); + println!(" Cost: $10 per TB per month"); + println!(" 1 TB = {:.2} PIPE tokens", tokens_for_1tb); + println!(); + + if remaining_gb < 10.0 { + println!("⚠️ Warning: Low storage space remaining!"); + println!(" Consider acquiring more PIPE tokens."); + } + } else { + return Err(anyhow!( + "Failed to get storage quota. Status = {}, Body = {}", + status, + text_body + )); + } + } + Commands::WithdrawSol { user_id, user_app_key, From 4959399b563547ee8ecb6b3f8ae3f011efb119e6 Mon Sep 17 00:00:00 2001 From: David Rhodus Date: Wed, 22 Oct 2025 16:46:27 -0700 Subject: [PATCH 4/6] Remove swap-sol-for-pipe command for mainnet deployment - Removed SwapSolForPipe command variant from CLI - Removed SwapSolForPipeRequest and SwapSolForPipeResponse structs - Removed handler code for /exchangeSolForTokens endpoint - Updated error messages to direct users to contact support instead of using swap - All tests passing (44 tests) - Zero warnings This change prepares the CLI for mainnet deployment where the swap endpoint has been disabled on the server side. --- src/lib.rs | 97 +++++++----------------------------------------------- 1 file changed, 11 insertions(+), 86 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a1e49f4..455308b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -358,9 +358,6 @@ pub enum Commands { #[arg(long)] user_id: Option, }, - - /// Check storage quota and usage - StorageQuota, /// Withdraw SOL to an external Solana address WithdrawSol { @@ -2865,7 +2862,7 @@ async fn upload_file_with_shared_progress( return Err(anyhow!("Upload failed: {}", message)); } } - return Err(anyhow!("Upload failed: Insufficient tokens. Please acquire more PIPE tokens to continue.")); + 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 @@ -3090,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 acquire more PIPE tokens to continue.")); + 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 @@ -3442,7 +3439,6 @@ pub async fn run_cli() -> Result<()> { | Commands::CheckSol { .. } | Commands::CheckToken { .. } | Commands::TokenUsage { .. } - | Commands::StorageQuota | Commands::WithdrawSol { .. } | Commands::WithdrawCustomToken { .. } | Commands::CreatePublicLink { .. } @@ -4020,7 +4016,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 You need approximately {:.1} more PIPE tokens.", (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"); } @@ -4611,79 +4607,6 @@ pub async fn run_cli() -> Result<()> { } } - Commands::StorageQuota => { - // Load credentials - let mut creds = load_credentials_from_file(config_path)?.ok_or_else(|| { - anyhow!("No credentials found. Please login first.") - })?; - - // Ensure JWT token is valid - let client = reqwest::Client::new(); - ensure_valid_token(&client, base_url, &mut creds, config_path).await?; - - // Make request with auth header - let mut request = client.get(format!("{}/storageQuota", base_url)); - - if let Some(ref auth_tokens) = creds.auth_tokens { - request = request.header( - "Authorization", - format!("Bearer {}", auth_tokens.access_token), - ); - } - - let resp = request.send().await?; - - let status = resp.status(); - let text_body = resp.text().await?; - - if status.is_success() { - // Parse quota response - let quota: serde_json::Value = serde_json::from_str(&text_body)?; - - let q = "a["quota"]; - let free_gb = q["free_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); - let paid_gb = q["paid_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); - let used_gb = q["used_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); - let total_gb = q["total_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); - let remaining_gb = q["remaining_storage_bytes"].as_u64().unwrap_or(0) as f64 / (1024.0 * 1024.0 * 1024.0); - let token_balance = q["token_balance"].as_f64().unwrap_or(0.0); - let pipe_price = q["pipe_price_usd"].as_f64().unwrap_or(0.0); - let percentage_used = quota["percentage_used"].as_f64().unwrap_or(0.0); - let tokens_for_1tb = quota["tokens_needed_for_1tb"].as_f64().unwrap_or(0.0); - - println!("\n💾 Storage Quota Summary"); - println!("═══════════════════════════════════════"); - println!(); - println!("📊 Current Usage:"); - println!(" Used: {:.2} GB / {:.2} GB ({:.1}%)", used_gb, total_gb, percentage_used); - println!(" Available: {:.2} GB", remaining_gb); - println!(); - println!("📦 Storage Breakdown:"); - println!(" Free tier: {:.2} GB", free_gb); - println!(" Paid tier: {:.2} GB", paid_gb); - println!(); - println!("🪙 Token Information:"); - println!(" Balance: {:.2} PIPE tokens", token_balance); - println!(" Value: ${:.4} (@ ${:.4}/PIPE)", token_balance * pipe_price, pipe_price); - println!(); - println!("💡 Pricing:"); - println!(" Cost: $10 per TB per month"); - println!(" 1 TB = {:.2} PIPE tokens", tokens_for_1tb); - println!(); - - if remaining_gb < 10.0 { - println!("⚠️ Warning: Low storage space remaining!"); - println!(" Consider acquiring more PIPE tokens."); - } - } else { - return Err(anyhow!( - "Failed to get storage quota. Status = {}, Body = {}", - status, - text_body - )); - } - } - Commands::WithdrawSol { user_id, user_app_key, @@ -5164,7 +5087,7 @@ pub async fn run_cli() -> Result<()> { "Needed: {:.4} PIPE tokens", total_cost_estimate - current_balance ); - eprintln!("\nPlease acquire approximately {:.1} more PIPE 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(()); } @@ -5528,7 +5451,7 @@ pub async fn run_cli() -> Result<()> { "Needed: {:.4} PIPE tokens", total_cost_estimate - current_balance ); - eprintln!("\nPlease acquire approximately {:.1} more PIPE 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(()); } @@ -5884,7 +5807,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 You need approximately {:.1} more PIPE tokens.", (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"); } @@ -6661,7 +6584,7 @@ pub async fn run_cli() -> Result<()> { println!("\n📋 Referral Program Rules:"); println!(" • Share this code with friends who want to join Pipe Network"); - println!(" • They must use the service to activate your reward"); + 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"); @@ -6708,7 +6631,7 @@ pub async fn run_cli() -> Result<()> { println!(" Total PIPE earned: {}", stats["total_pipe_earned"]); println!("\n📋 Referral Program Rules:"); - println!(" • Referred user must use the service to activate reward"); + 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"); @@ -6738,8 +6661,10 @@ pub async fn run_cli() -> Result<()> { 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 use the service"); + println!(" • You must complete a swap of at least 1 DevNet SOL"); println!(" • Your referrer will receive 100 PIPE tokens"); + println!(" • Contact support to get started with PIPE tokens"); + 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")); } From 021a81b6dc802a33d3393754fbcdcff17ecc472d Mon Sep 17 00:00:00 2001 From: David Rhodus Date: Thu, 30 Oct 2025 16:14:10 -0700 Subject: [PATCH 5/6] Mainnet migration: Remove testnet features and add deposit system BREAKING CHANGES: - Removed referral system (generate, show, apply commands) - Removed SOL withdrawal commands (withdraw-sol) - Removed custom token withdrawal commands (withdraw-custom-token) - Removed check-sol command Added Features: - Prepaid deposit system with one-way PIPE deposits - check-deposit: View balance, quotas, and pricing - estimate-cost: Pre-upload cost estimation - sync-deposits: Manual deposit synchronization - USD-based pricing (/TB = --.01/GB) - Deposits burned as storage is used (no withdrawals) Documentation: - Updated README.md with deposit system documentation - Removed testnet-specific command examples - Added mainnet release notes This is a production mainnet release using real PIPE tokens. --- README.md | 121 +++++--- src/lib.rs | 822 +++++++++++++++++++++++++++-------------------------- 2 files changed, 509 insertions(+), 434 deletions(-) diff --git a/README.md b/README.md index 2a069b1..75fe7ab 100644 --- a/README.md +++ b/README.md @@ -124,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) @@ -596,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 @@ -636,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 + +View your deposit balance, available storage across all tiers, and pricing information: -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) +```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 + +#### Sync Deposits -Check your balances: +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 + +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 -# Check SOL balance -pipe check-sol +**Important**: Deposits are one-way by design. PIPE deposited into the system cannot be withdrawn - it's burned as you use storage. -# Swap SOL for PIPE tokens -pipe swap-sol-for-pipe 0.5 # Swap 0.5 SOL for PIPE +#### 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 455308b..f1de1e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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,27 +351,6 @@ pub enum Commands { user_id: Option, }, - /// 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, @@ -470,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, @@ -573,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, }, } @@ -664,54 +642,72 @@ pub struct CheckCustomTokenResponse { pub ui_amount: 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, +#[derive(Serialize, Deserialize, Debug)] +pub struct PriorityFeeResponse { + pub priority_fee_per_gb: 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, } +// Deposit system structures #[derive(Serialize, Deserialize, Debug)] -pub struct WithdrawSolResponse { +pub struct DepositBalanceResponse { pub user_id: String, - pub to_pubkey: String, - pub amount_sol: f64, - pub signature: 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)] -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 StorageQuota { + pub deposit_balance_lamports: u64, + pub deposit_balance_pipe: f64, + pub pipe_price_usd: f64, + pub tier_estimates: Vec, } #[derive(Serialize, Deserialize, Debug)] -pub struct WithdrawTokenResponse { - pub user_id: String, - pub to_pubkey: String, - pub amount: u64, - pub signature: String, +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 PriorityFeeResponse { - pub priority_fee_per_gb: f64, +pub struct EstimateUploadRequest { + pub file_size_bytes: u64, + pub tier: Option, } #[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 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 SyncDepositsResponse { + pub success: bool, + pub deposited_amount: u64, + pub deposited_pipe: f64, + pub message: String, } #[derive(Serialize, Deserialize)] @@ -1517,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") || @@ -1526,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 { @@ -3436,11 +3436,8 @@ pub async fn run_cli() -> Result<()> { | Commands::DownloadFile { .. } | Commands::DeleteFile { .. } | Commands::FileInfo { .. } - | Commands::CheckSol { .. } | Commands::CheckToken { .. } | Commands::TokenUsage { .. } - | Commands::WithdrawSol { .. } - | Commands::WithdrawCustomToken { .. } | Commands::CreatePublicLink { .. } | Commands::DeletePublicLink { .. } | Commands::PublicDownload { .. } @@ -4352,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, @@ -4442,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 { @@ -4607,118 +4553,6 @@ pub async fn run_cli() -> Result<()> { } } - 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, @@ -5167,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(); @@ -5195,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; @@ -5227,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; @@ -5524,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(); @@ -5549,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; @@ -5568,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; @@ -6152,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, @@ -6545,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!(" • Contact support to get started with PIPE tokens"); - 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(()) From 2d571de27d1c8567d3c8c4b379487a182fc90c94 Mon Sep 17 00:00:00 2001 From: David Rhodus Date: Thu, 30 Oct 2025 16:48:45 -0700 Subject: [PATCH 6/6] Update default API endpoint to mainnet (us-west-01) - Changed default endpoint from us-west-00 to us-west-01 - Updated README.md examples to reflect correct mainnet endpoint - Removed reference to us-east-00 (testnet endpoint) --- README.md | 6 +++--- src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 75fe7ab..9c6221b 100644 --- a/README.md +++ b/README.md @@ -314,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" } ``` @@ -473,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 diff --git a/src/lib.rs b/src/lib.rs index f1de1e5..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" )]