use anyhow::{Context, Result};
use serde_json::json;
use crate::api::{ApiClient, GetPubkeyRequest};
use crate::config::{self, NetworkConfig};
use crate::crypto;
use crate::near::{ContractCaller, NearClient};
pub async fn create(network: &NetworkConfig) -> Result<()> {
let creds = config::load_credentials(network)?;
let near = NearClient::new(network);
let caller = ContractCaller::from_credentials(&creds, network)?;
let api = ApiClient::new(network);
let nonce = near
.get_next_payment_key_nonce(&creds.account_id)
.await
.context("Failed to get next payment key nonce")?;
eprintln!("Creating payment key (nonce: {nonce})...");
let secret = crypto::generate_payment_key_secret();
let secrets_json = json!({
"key": secret,
"project_ids": [],
"max_per_call": null,
"initial_balance": null
})
.to_string();
let pubkey = api
.get_secrets_pubkey(
&GetPubkeyRequest {
accessor: json!({ "type": "System", "PaymentKey": {} }),
owner: creds.account_id.clone(),
profile: Some(nonce.to_string()),
secrets_json: secrets_json.clone(),
},
None,
)
.await
.context("Failed to get keystore pubkey")?;
let encrypted = crypto::encrypt_secrets(&pubkey, &secrets_json)?;
let deposit = 100_000_000_000_000_000_000_000u128; let gas = 100_000_000_000_000u64;
caller
.call_contract(
"store_secrets",
json!({
"accessor": { "System": "PaymentKey" },
"profile": nonce.to_string(),
"encrypted_secrets_base64": encrypted,
"access": "AllowAll",
"vault_id": null,
}),
gas,
deposit,
)
.await
.context("Failed to store payment key")?;
let api_key = format!("{}:{}:{}", creds.account_id, nonce, secret);
eprintln!("Payment key created (nonce: {nonce})");
println!("{api_key}");
eprintln!("\nSave this key — it cannot be recovered.");
eprintln!("Top up: outlayer keys topup --nonce {nonce} --amount 1");
Ok(())
}
pub async fn list(network: &NetworkConfig) -> Result<()> {
let creds = config::load_credentials(network)?;
let near = NearClient::new(network);
let api = ApiClient::new(network);
let secrets = near.list_user_secrets(&creds.account_id).await?;
let payment_keys: Vec<_> = secrets
.iter()
.filter(|s| s.accessor.to_string().contains("System"))
.collect();
if payment_keys.is_empty() {
eprintln!("No payment keys. Create one: outlayer keys create");
return Ok(());
}
println!(
"{:<8} {:>12} {:>12} {:>12}",
"NONCE", "AVAILABLE", "SPENT", "INITIAL"
);
for pk in &payment_keys {
let nonce: u32 = pk.profile.parse().unwrap_or(0);
match api
.get_payment_key_balance(&creds.account_id, nonce)
.await
{
Ok(balance) => {
println!(
"{:<8} {:>12} {:>12} {:>12}",
nonce,
format_usd(&balance.available),
format_usd(&balance.spent),
format_usd(&balance.initial_balance),
);
}
Err(_) => {
println!(
"{:<8} {:>12} {:>12} {:>12}",
nonce, "---", "---", "---"
);
}
}
}
Ok(())
}
pub async fn balance(network: &NetworkConfig, nonce: u32) -> Result<()> {
let creds = config::load_credentials(network)?;
let api = ApiClient::new(network);
let balance = api
.get_payment_key_balance(&creds.account_id, nonce)
.await?;
println!("Balance: {}", format_usd(&balance.available));
println!("Spent: {}", format_usd(&balance.spent));
println!("Reserved: {}", format_usd(&balance.reserved));
println!("Initial: {}", format_usd(&balance.initial_balance));
if let Some(last_used) = &balance.last_used_at {
println!("Last used: {last_used}");
}
Ok(())
}
pub async fn topup(network: &NetworkConfig, nonce: u32, amount_near: f64) -> Result<()> {
let creds = config::load_credentials(network)?;
if network.network_id != "mainnet" {
anyhow::bail!("Top-up with NEAR is only available on mainnet.");
}
let deposit = (amount_near * 1e24) as u128;
let min_deposit = 35_000_000_000_000_000_000_000u128; if deposit < min_deposit {
anyhow::bail!("Minimum top-up is 0.035 NEAR (0.01 deposit + 0.025 execution fees).");
}
let caller = ContractCaller::from_credentials(&creds, network)?;
let gas = 200_000_000_000_000u64;
eprintln!("Topping up key nonce {nonce} with {amount_near} NEAR...");
caller
.call_contract(
"top_up_payment_key_with_near",
json!({
"nonce": nonce,
"swap_contract_id": "intents.near"
}),
gas,
deposit,
)
.await
.context("Top-up failed")?;
eprintln!("Top-up successful. NEAR will be swapped to USDC via Intents.");
eprintln!("Check balance: outlayer keys balance --nonce {nonce}");
Ok(())
}
pub async fn delete(network: &NetworkConfig, nonce: u32) -> Result<()> {
let creds = config::load_credentials(network)?;
let caller = ContractCaller::from_credentials(&creds, network)?;
let gas = 100_000_000_000_000u64;
eprintln!("Deleting payment key nonce {nonce}...");
caller
.call_contract(
"delete_payment_key",
json!({ "nonce": nonce }),
gas,
1, )
.await
.context("Failed to delete payment key")?;
eprintln!("Payment key deleted. Storage deposit refunded.");
Ok(())
}
fn format_usd(minimal_units: &str) -> String {
let units: u64 = minimal_units.parse().unwrap_or(0);
let dollars = units as f64 / 1_000_000.0;
format!("${:.2}", dollars)
}