1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Serialize, Deserialize)]
8pub struct Credentials {
9 pub account_id: String,
10 pub public_key: String,
11 pub private_key: Option<String>,
13 pub contract_id: String,
14 #[serde(default = "default_auth_type")]
16 pub auth_type: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub wallet_key: Option<String>,
20}
21
22fn default_auth_type() -> String {
23 "near_key".to_string()
24}
25
26impl Credentials {
27 pub fn is_wallet_key(&self) -> bool {
28 self.auth_type == "wallet_key"
29 }
30}
31
32#[derive(Debug, Serialize, Deserialize)]
35pub struct ProjectConfig {
36 pub project: ProjectSection,
37 pub build: Option<BuildSection>,
38 pub deploy: Option<DeploySection>,
39 pub run: Option<RunSection>,
40 pub network: Option<String>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44pub struct ProjectSection {
45 pub name: String,
46 pub owner: String,
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub struct BuildSection {
51 #[serde(default = "default_target")]
52 pub target: String,
53 #[serde(default = "default_source")]
54 pub source: String,
55}
56
57fn default_target() -> String {
58 "wasm32-wasip2".to_string()
59}
60fn default_source() -> String {
61 "github".to_string()
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65pub struct DeploySection {
66 pub repo: Option<String>,
67 pub wasm_path: Option<String>,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct RunSection {
72 pub max_instructions: Option<u64>,
73 pub max_memory_mb: Option<u32>,
74 pub max_execution_seconds: Option<u32>,
75 pub secrets_profile: Option<String>,
76 pub payment_key_nonce: Option<u32>,
77}
78
79#[derive(Debug, Clone)]
82pub struct NetworkConfig {
83 pub network_id: String,
84 pub rpc_url: String,
85 pub contract_id: String,
86 #[allow(dead_code)]
87 pub wallet_url: String,
88 pub api_base_url: String,
89}
90
91impl NetworkConfig {
92 pub fn mainnet() -> Self {
93 Self {
94 network_id: "mainnet".to_string(),
95 rpc_url: "https://rpc.mainnet.near.org".to_string(),
96 contract_id: "outlayer.near".to_string(),
97 wallet_url: "https://app.mynearwallet.com".to_string(),
98 api_base_url: "https://api.outlayer.fastnear.com".to_string(),
99 }
100 }
101
102 pub fn testnet() -> Self {
103 Self {
104 network_id: "testnet".to_string(),
105 rpc_url: "https://test.rpc.fastnear.com".to_string(),
106 contract_id: "outlayer.testnet".to_string(),
107 wallet_url: "https://testnet.mynearwallet.com".to_string(),
108 api_base_url: "https://testnet-api.outlayer.fastnear.com".to_string(),
109 }
110 }
111}
112
113pub fn resolve_network(flag: Option<&str>, project: Option<&str>) -> Result<NetworkConfig> {
115 let network = flag
116 .or(project)
117 .map(|s| s.to_string())
118 .or_else(load_default_network)
119 .or_else(|| detect_logged_in_network())
120 .unwrap_or_else(|| "mainnet".to_string());
121
122 match network.as_str() {
123 "mainnet" => Ok(NetworkConfig::mainnet()),
124 "testnet" => Ok(NetworkConfig::testnet()),
125 other => anyhow::bail!("Unknown network: {other}. Use 'mainnet' or 'testnet'."),
126 }
127}
128
129pub fn save_default_network(network: &str) {
130 if let Ok(home) = outlayer_home() {
131 let _ = std::fs::create_dir_all(&home);
132 let _ = std::fs::write(home.join("default-network"), network);
133 }
134}
135
136fn load_default_network() -> Option<String> {
137 let home = outlayer_home().ok()?;
138 std::fs::read_to_string(home.join("default-network"))
139 .ok()
140 .map(|s| s.trim().to_string())
141 .filter(|s| !s.is_empty())
142}
143
144fn detect_logged_in_network() -> Option<String> {
146 let home = outlayer_home().ok()?;
147 let has_mainnet = home.join("mainnet/credentials.json").exists();
148 let has_testnet = home.join("testnet/credentials.json").exists();
149 match (has_mainnet, has_testnet) {
150 (true, false) => Some("mainnet".to_string()),
151 (false, true) => Some("testnet".to_string()),
152 _ => None, }
154}
155
156fn outlayer_home() -> Result<PathBuf> {
159 if let Ok(home) = std::env::var("OUTLAYER_HOME") {
160 return Ok(PathBuf::from(home));
161 }
162 let home = dirs::home_dir().context("Cannot determine home directory")?;
163 Ok(home.join(".outlayer"))
164}
165
166fn credentials_path(network: &str) -> Result<PathBuf> {
167 let home = outlayer_home()?;
168 Ok(home.join(network).join("credentials.json"))
169}
170
171const KEYRING_SERVICE: &str = "outlayer-cli";
174
175fn keyring_key(network: &str, account_id: &str) -> String {
176 format!("{network}:{account_id}")
177}
178
179pub fn save_private_key(network: &str, account_id: &str, key: &str) -> bool {
180 let entry = match keyring::Entry::new(KEYRING_SERVICE, &keyring_key(network, account_id)) {
181 Ok(e) => e,
182 Err(_) => return false,
183 };
184 if entry.set_password(key).is_err() {
185 return false;
186 }
187 entry.get_password().is_ok()
189}
190
191pub fn load_private_key(network: &str, account_id: &str, creds: &Credentials) -> Result<String> {
192 if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, &keyring_key(network, account_id)) {
194 if let Ok(key) = entry.get_password() {
195 return Ok(key);
196 }
197 }
198 creds
200 .private_key
201 .clone()
202 .context("Private key not found in credentials or keychain")
203}
204
205fn delete_private_key(network: &str, account_id: &str) {
206 if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, &keyring_key(network, account_id)) {
207 let _ = entry.delete_credential();
208 }
209}
210
211pub fn load_credentials(network: &NetworkConfig) -> Result<Credentials> {
214 let path = credentials_path(&network.network_id)?;
215 let data = std::fs::read_to_string(&path)
216 .with_context(|| format!("Not logged in. Run: outlayer login --network {}", network.network_id))?;
217 serde_json::from_str(&data).context("Invalid credentials file")
218}
219
220pub fn save_credentials(network: &NetworkConfig, creds: &Credentials) -> Result<()> {
221 let path = credentials_path(&network.network_id)?;
222 if let Some(parent) = path.parent() {
223 std::fs::create_dir_all(parent)?;
224 }
225 let data = serde_json::to_string_pretty(creds)?;
226 std::fs::write(&path, data)?;
227 Ok(())
228}
229
230pub fn delete_credentials(network: &NetworkConfig) -> Result<()> {
231 let path = credentials_path(&network.network_id)?;
232 if path.exists() {
233 if let Ok(data) = std::fs::read_to_string(&path) {
235 if let Ok(creds) = serde_json::from_str::<Credentials>(&data) {
236 delete_private_key(&network.network_id, &creds.account_id);
237 }
238 }
239 std::fs::remove_file(&path)?;
240 }
241 Ok(())
242}
243
244pub fn load_project_config() -> Result<ProjectConfig> {
247 let path = std::env::current_dir()?.join("outlayer.toml");
248 let data = std::fs::read_to_string(&path)
249 .context("outlayer.toml not found. Run 'outlayer create <name>' first.")?;
250 toml::from_str(&data).context("Invalid outlayer.toml")
251}
252