outlayer_cli/commands/
keys.rs1use 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
9pub 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 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 let secret = crypto::generate_payment_key_secret();
27
28 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 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 let encrypted = crypto::encrypt_secrets(&pubkey, &secrets_json)?;
50
51 let deposit = 100_000_000_000_000_000_000_000u128; let gas = 100_000_000_000_000u64; 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
80pub 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 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 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 println!(
124 "{:<8} {:>12} {:>12} {:>12}",
125 nonce, "---", "---", "---"
126 );
127 }
128 }
129 }
130
131 Ok(())
132}
133
134pub 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
154pub 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 let deposit = (amount_near * 1e24) as u128;
164 let min_deposit = 35_000_000_000_000_000_000_000u128; 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; 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
193pub 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; eprintln!("Deleting payment key nonce {nonce}...");
201
202 caller
203 .call_contract(
204 "delete_payment_key",
205 json!({ "nonce": nonce }),
206 gas,
207 1, )
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}