Skip to main content

outlayer_cli/commands/
earnings.rs

1use anyhow::Result;
2use serde_json::json;
3
4use crate::api::ApiClient;
5use crate::config::{self, NetworkConfig};
6use crate::near::{ContractCaller, NearClient};
7
8/// `outlayer earnings` — show developer earnings
9pub async fn show(network: &NetworkConfig) -> Result<()> {
10    let creds = config::load_credentials(network)?;
11    let api = ApiClient::new(network);
12    let near = NearClient::new(network);
13
14    // Get HTTPS earnings from coordinator
15    let https_earnings = api
16        .get_project_owner_earnings(&creds.account_id)
17        .await
18        .ok();
19
20    // Get blockchain earnings from contract
21    let blockchain_earnings = near.get_developer_earnings(&creds.account_id).await.ok();
22
23    let https_balance = https_earnings
24        .as_ref()
25        .map(|e| format_usd(&e.balance))
26        .unwrap_or_else(|| "$0.00".to_string());
27
28    let https_total = https_earnings
29        .as_ref()
30        .map(|e| format_usd(&e.total_earned))
31        .unwrap_or_else(|| "$0.00".to_string());
32
33    let blockchain_balance = blockchain_earnings
34        .as_ref()
35        .map(|e| format_usd(e))
36        .unwrap_or_else(|| "$0.00".to_string());
37
38    println!("Blockchain earnings:  {blockchain_balance}");
39    println!("HTTPS API earnings:   {https_balance} (total: {https_total})");
40
41    Ok(())
42}
43
44/// `outlayer earnings withdraw` — withdraw blockchain earnings
45pub async fn withdraw(network: &NetworkConfig) -> Result<()> {
46    let creds = config::load_credentials(network)?;
47
48    let caller = ContractCaller::from_credentials(&creds, network)?;
49    let gas = 100_000_000_000_000u64; // 100 TGas
50
51    caller
52        .call_contract("withdraw_developer_earnings", json!({}), gas, 1) // 1 yoctoNEAR
53        .await?;
54
55    eprintln!("Earnings withdrawn to {}", creds.account_id);
56    Ok(())
57}
58
59/// `outlayer earnings history` — view earnings history
60pub async fn history(
61    network: &NetworkConfig,
62    source: Option<String>,
63    limit: i64,
64) -> Result<()> {
65    let creds = config::load_credentials(network)?;
66    let api = ApiClient::new(network);
67
68    let resp = api
69        .get_earnings_history(&creds.account_id, source.as_deref(), limit, 0)
70        .await?;
71
72    if resp.earnings.is_empty() {
73        eprintln!("No earnings history.");
74        return Ok(());
75    }
76
77    println!(
78        "{:<12} {:<10} {:<25} {:>10}",
79        "DATE", "SOURCE", "PROJECT", "AMOUNT"
80    );
81
82    for e in &resp.earnings {
83        let date = format_timestamp(e.created_at);
84        let amount = format_usd(&e.amount);
85        println!(
86            "{:<12} {:<10} {:<25} {:>10}",
87            date, e.source, e.project_id, amount
88        );
89    }
90
91    if resp.total_count > limit {
92        eprintln!(
93            "\nShowing {}/{} entries. Use --limit to see more.",
94            resp.earnings.len(),
95            resp.total_count
96        );
97    }
98
99    Ok(())
100}
101
102/// Format minimal USD units (6 decimals) to human-readable
103fn format_usd(minimal_units: &str) -> String {
104    let units: u64 = minimal_units.parse().unwrap_or(0);
105    let dollars = units as f64 / 1_000_000.0;
106    format!("${:.2}", dollars)
107}
108
109fn format_timestamp(ts: i64) -> String {
110    // ts is unix seconds or milliseconds — normalize
111    let secs = if ts > 1_000_000_000_000 {
112        ts / 1000
113    } else {
114        ts
115    };
116    // Simple date format without chrono
117    let days_since_epoch = secs / 86400;
118    // Approximate: good enough for display
119    let year = 1970 + (days_since_epoch / 365);
120    let day_in_year = days_since_epoch % 365;
121    let month = day_in_year / 30 + 1;
122    let day = day_in_year % 30 + 1;
123    format!("{:04}-{:02}-{:02}", year, month, day)
124}