Skip to main content

outlayer_cli/commands/
keys.rs

1use anyhow::{Context, Result};
2use serde_json::json;
3
4use crate::api::{ApiClient, GetPubkeyRequest};
5use crate::config::{self, NetworkConfig};
6use crate::crypto;
7use crate::near::{ContractCaller, NearClient};
8
9/// `outlayer keys create` — create a new payment key
10pub async fn create(network: &NetworkConfig) -> Result<()> {
11    let creds = config::load_credentials(network)?;
12
13    let near = NearClient::new(network);
14    let caller = ContractCaller::from_credentials(&creds, network)?;
15    let api = ApiClient::new(network);
16
17    // Get next nonce
18    let nonce = near
19        .get_next_payment_key_nonce(&creds.account_id)
20        .await
21        .context("Failed to get next payment key nonce")?;
22
23    eprintln!("Creating payment key (nonce: {nonce})...");
24
25    // Generate secret
26    let secret = crypto::generate_payment_key_secret();
27
28    // Build secrets JSON
29    let secrets_json = json!({
30        "key": secret,
31        "project_ids": [],
32        "max_per_call": null,
33        "initial_balance": null
34    })
35    .to_string();
36
37    // Get pubkey for encryption
38    let pubkey = api
39        .get_secrets_pubkey(&GetPubkeyRequest {
40            accessor: json!({ "type": "System", "PaymentKey": {} }),
41            owner: creds.account_id.clone(),
42            profile: Some(nonce.to_string()),
43            secrets_json: secrets_json.clone(),
44        })
45        .await
46        .context("Failed to get keystore pubkey")?;
47
48    // Encrypt
49    let encrypted = crypto::encrypt_secrets(&pubkey, &secrets_json)?;
50
51    // Store on contract
52    let deposit = 100_000_000_000_000_000_000_000u128; // 0.1 NEAR
53    let gas = 100_000_000_000_000u64; // 100 TGas
54
55    caller
56        .call_contract(
57            "store_secrets",
58            json!({
59                "accessor": { "System": "PaymentKey" },
60                "profile": nonce.to_string(),
61                "encrypted_secrets_base64": encrypted,
62                "access": "AllowAll"
63            }),
64            gas,
65            deposit,
66        )
67        .await
68        .context("Failed to store payment key")?;
69
70    let api_key = format!("{}:{}:{}", creds.account_id, nonce, secret);
71
72    eprintln!("Payment key created (nonce: {nonce})");
73    println!("{api_key}");
74    eprintln!("\nSave this key — it cannot be recovered.");
75    eprintln!("Top up: outlayer keys topup --nonce {nonce} --amount 1");
76
77    Ok(())
78}
79
80/// `outlayer keys list` — list payment keys with balances
81pub async fn list(network: &NetworkConfig) -> Result<()> {
82    let creds = config::load_credentials(network)?;
83    let near = NearClient::new(network);
84    let api = ApiClient::new(network);
85
86    // Get all user secrets, filter for System(PaymentKey) entries
87    let secrets = near.list_user_secrets(&creds.account_id).await?;
88
89    let payment_keys: Vec<_> = secrets
90        .iter()
91        .filter(|s| s.accessor.to_string().contains("System"))
92        .collect();
93
94    if payment_keys.is_empty() {
95        eprintln!("No payment keys. Create one: outlayer keys create");
96        return Ok(());
97    }
98
99    println!(
100        "{:<8} {:>12} {:>12} {:>12}",
101        "NONCE", "AVAILABLE", "SPENT", "INITIAL"
102    );
103
104    for pk in &payment_keys {
105        let nonce: u32 = pk.profile.parse().unwrap_or(0);
106
107        // Try to get balance from coordinator
108        match api
109            .get_payment_key_balance(&creds.account_id, nonce)
110            .await
111        {
112            Ok(balance) => {
113                println!(
114                    "{:<8} {:>12} {:>12} {:>12}",
115                    nonce,
116                    format_usd(&balance.available),
117                    format_usd(&balance.spent),
118                    format_usd(&balance.initial_balance),
119                );
120            }
121            Err(_) => {
122                // Key exists on contract but not yet initialized in coordinator
123                println!(
124                    "{:<8} {:>12} {:>12} {:>12}",
125                    nonce, "---", "---", "---"
126                );
127            }
128        }
129    }
130
131    Ok(())
132}
133
134/// `outlayer keys balance --nonce N` — check specific key balance
135pub async fn balance(network: &NetworkConfig, nonce: u32) -> Result<()> {
136    let creds = config::load_credentials(network)?;
137    let api = ApiClient::new(network);
138
139    let balance = api
140        .get_payment_key_balance(&creds.account_id, nonce)
141        .await?;
142
143    println!("Balance:    {}", format_usd(&balance.available));
144    println!("Spent:      {}", format_usd(&balance.spent));
145    println!("Reserved:   {}", format_usd(&balance.reserved));
146    println!("Initial:    {}", format_usd(&balance.initial_balance));
147    if let Some(last_used) = &balance.last_used_at {
148        println!("Last used:  {last_used}");
149    }
150
151    Ok(())
152}
153
154/// `outlayer keys topup --nonce N --amount X` — top up with NEAR
155pub async fn topup(network: &NetworkConfig, nonce: u32, amount_near: f64) -> Result<()> {
156    let creds = config::load_credentials(network)?;
157
158    if network.network_id != "mainnet" {
159        anyhow::bail!("Top-up with NEAR is only available on mainnet.");
160    }
161
162    // Convert NEAR to yoctoNEAR
163    let deposit = (amount_near * 1e24) as u128;
164    let min_deposit = 35_000_000_000_000_000_000_000u128; // 0.035 NEAR minimum
165    if deposit < min_deposit {
166        anyhow::bail!("Minimum top-up is 0.035 NEAR (0.01 deposit + 0.025 execution fees).");
167    }
168
169    let caller = ContractCaller::from_credentials(&creds, network)?;
170    let gas = 200_000_000_000_000u64; // 200 TGas (cross-contract calls)
171
172    eprintln!("Topping up key nonce {nonce} with {amount_near} NEAR...");
173
174    caller
175        .call_contract(
176            "top_up_payment_key_with_near",
177            json!({
178                "nonce": nonce,
179                "swap_contract_id": "intents.near"
180            }),
181            gas,
182            deposit,
183        )
184        .await
185        .context("Top-up failed")?;
186
187    eprintln!("Top-up successful. NEAR will be swapped to USDC via Intents.");
188    eprintln!("Check balance: outlayer keys balance --nonce {nonce}");
189
190    Ok(())
191}
192
193/// `outlayer keys delete --nonce N` — delete payment key
194pub async fn delete(network: &NetworkConfig, nonce: u32) -> Result<()> {
195    let creds = config::load_credentials(network)?;
196
197    let caller = ContractCaller::from_credentials(&creds, network)?;
198    let gas = 100_000_000_000_000u64; // 100 TGas
199
200    eprintln!("Deleting payment key nonce {nonce}...");
201
202    caller
203        .call_contract(
204            "delete_payment_key",
205            json!({ "nonce": nonce }),
206            gas,
207            1, // 1 yoctoNEAR
208        )
209        .await
210        .context("Failed to delete payment key")?;
211
212    eprintln!("Payment key deleted. Storage deposit refunded.");
213    Ok(())
214}
215
216fn format_usd(minimal_units: &str) -> String {
217    let units: u64 = minimal_units.parse().unwrap_or(0);
218    let dollars = units as f64 / 1_000_000.0;
219    format!("${:.2}", dollars)
220}