1use crate::error::OwsLibError;
6use std::process::Command;
7
8fn nano_rpc_call(
10 rpc_url: &str,
11 body: &serde_json::Value,
12) -> Result<serde_json::Value, OwsLibError> {
13 let body_str = body.to_string();
14 let output = Command::new("curl")
15 .args([
16 "-fsSL",
17 "-X",
18 "POST",
19 "-H",
20 "Content-Type: application/json",
21 "-d",
22 &body_str,
23 rpc_url,
24 ])
25 .output()
26 .map_err(|e| OwsLibError::BroadcastFailed(format!("failed to run curl: {e}")))?;
27
28 if !output.status.success() {
29 let stderr = String::from_utf8_lossy(&output.stderr);
30 return Err(OwsLibError::BroadcastFailed(format!(
31 "Nano RPC call failed: {stderr}"
32 )));
33 }
34
35 let resp_str = String::from_utf8_lossy(&output.stdout);
36 let parsed: serde_json::Value = serde_json::from_str(&resp_str)?;
37
38 if let Some(error) = parsed.get("error") {
40 let msg = error.as_str().unwrap_or("unknown error");
41 return Err(OwsLibError::BroadcastFailed(format!(
42 "Nano RPC error: {msg}"
43 )));
44 }
45
46 Ok(parsed)
47}
48
49#[derive(Debug, Clone)]
51pub struct NanoAccountInfo {
52 pub frontier: String,
54 pub balance: String,
56 pub representative: String,
58}
59
60pub fn account_info(rpc_url: &str, account: &str) -> Result<Option<NanoAccountInfo>, OwsLibError> {
64 let body = serde_json::json!({
65 "action": "account_info",
66 "account": account,
67 "representative": "true"
68 });
69
70 match nano_rpc_call(rpc_url, &body) {
71 Ok(resp) => {
72 let frontier = resp["frontier"]
73 .as_str()
74 .ok_or_else(|| {
75 OwsLibError::BroadcastFailed("no frontier in account_info response".into())
76 })?
77 .to_string();
78 let balance = resp["balance"]
79 .as_str()
80 .ok_or_else(|| {
81 OwsLibError::BroadcastFailed("no balance in account_info response".into())
82 })?
83 .to_string();
84 let representative = resp["representative"]
85 .as_str()
86 .ok_or_else(|| {
87 OwsLibError::BroadcastFailed(
88 "no representative in account_info response".into(),
89 )
90 })?
91 .to_string();
92
93 Ok(Some(NanoAccountInfo {
94 frontier,
95 balance,
96 representative,
97 }))
98 }
99 Err(OwsLibError::BroadcastFailed(msg)) if msg.contains("Account not found") => Ok(None),
100 Err(e) => Err(e),
101 }
102}
103
104fn work_generate_single(
106 rpc_url: &str,
107 hash: &str,
108 difficulty: &str,
109) -> Result<String, OwsLibError> {
110 let body = serde_json::json!({
111 "action": "work_generate",
112 "hash": hash,
113 "difficulty": difficulty
114 });
115
116 let resp = nano_rpc_call(rpc_url, &body)?;
117
118 resp["work"]
119 .as_str()
120 .map(|s| s.to_string())
121 .ok_or_else(|| OwsLibError::BroadcastFailed("no work in work_generate response".into()))
122}
123
124const FALLBACK_WORK_URL: &str = "https://rpc.nano.to";
126
127pub fn work_generate(rpc_url: &str, hash: &str, difficulty: &str) -> Result<String, OwsLibError> {
137 let mut endpoints: Vec<String> = vec![rpc_url.to_string()];
138
139 if let Ok(urls) = std::env::var("NANO_WORK_URL") {
140 for url in urls.split(';') {
141 let url = url.trim();
142 if !url.is_empty() && url != rpc_url {
143 endpoints.push(url.to_string());
144 }
145 }
146 }
147
148 if !endpoints.iter().any(|e| e == FALLBACK_WORK_URL) {
149 endpoints.push(FALLBACK_WORK_URL.to_string());
150 }
151
152 let mut last_error = None;
153
154 for endpoint in &endpoints {
155 match work_generate_single(endpoint, hash, difficulty) {
156 Ok(work) => return Ok(work),
157 Err(e) => {
158 eprintln!(" PoW failed on {endpoint}: {e}");
159 last_error = Some(e);
160 }
161 }
162 }
163
164 Err(last_error
165 .unwrap_or_else(|| OwsLibError::BroadcastFailed("no PoW endpoints available".into())))
166}
167
168pub fn process_block(
172 rpc_url: &str,
173 block_json: &serde_json::Value,
174 subtype: &str,
175) -> Result<String, OwsLibError> {
176 let body = serde_json::json!({
177 "action": "process",
178 "json_block": "true",
179 "subtype": subtype,
180 "block": block_json
181 });
182
183 let resp = nano_rpc_call(rpc_url, &body)?;
184
185 resp["hash"]
186 .as_str()
187 .map(|s| s.to_string())
188 .ok_or_else(|| OwsLibError::BroadcastFailed(format!("no hash in process response: {resp}")))
189}
190
191pub const SEND_DIFFICULTY: &str = "fffffff800000000";
193pub const RECEIVE_DIFFICULTY: &str = "fffffe0000000000";