1use std::sync::{Arc, Mutex};
10
11use cdk::wallet::WalletNwcHandler;
12use cdk_nwc::{NwcService as CdkNwcService, NwcServiceConfig};
13use nostr_sdk::{Keys, RelayUrl, SecretKey};
14use tokio::task::JoinHandle;
15use tokio_util::sync::CancellationToken;
16
17use crate::error::FfiError;
18use crate::wallet::Wallet;
19
20#[derive(uniffi::Object)]
27pub struct NwcService {
28 service: CdkNwcService,
29 handler: Arc<WalletNwcHandler>,
30 task: Mutex<Option<(JoinHandle<()>, CancellationToken)>>,
31}
32
33impl NwcService {
34 fn clear_finished_task(task: &mut Option<(JoinHandle<()>, CancellationToken)>) {
35 if task
36 .as_ref()
37 .is_some_and(|(handle, _)| handle.is_finished())
38 {
39 drop(task.take());
40 }
41 }
42
43 fn build(
45 wallet: &Arc<Wallet>,
46 relays: Vec<String>,
47 service_keys: Keys,
48 client_secret: SecretKey,
49 max_payment_msat: Option<u64>,
50 ) -> Result<Self, FfiError> {
51 if relays.is_empty() {
52 return Err(FfiError::internal("at least one relay is required"));
53 }
54
55 let relays = relays
56 .iter()
57 .map(|r| {
58 RelayUrl::parse(r)
59 .map_err(|e| FfiError::internal(format!("invalid relay {r}: {e}")))
60 })
61 .collect::<Result<Vec<_>, _>>()?;
62
63 let handler = Arc::new(WalletNwcHandler::new(
64 wallet.inner().clone(),
65 max_payment_msat,
66 ));
67
68 let service = CdkNwcService::new(NwcServiceConfig {
69 service_keys,
70 client_secret,
71 relays,
72 lud16: None,
73 })
74 .map_err(|e| FfiError::internal(e.to_string()))?;
75
76 Ok(Self {
77 service,
78 handler,
79 task: Mutex::new(None),
80 })
81 }
82}
83
84impl Drop for NwcService {
85 fn drop(&mut self) {
86 let Ok(mut guard) = self.task.lock() else {
87 return;
88 };
89
90 if let Some((handle, cancel)) = guard.take() {
91 cancel.cancel();
92 handle.abort();
93 }
94 }
95}
96
97#[uniffi::export(async_runtime = "tokio")]
98impl NwcService {
99 #[uniffi::constructor]
115 pub fn create(
116 wallet: Arc<Wallet>,
117 relays: Vec<String>,
118 service_secret_key: String,
119 max_payment_msat: Option<u64>,
120 ) -> Result<Self, FfiError> {
121 let service_keys = parse_keys(&service_secret_key)?;
122 let client_secret = SecretKey::generate();
123 Self::build(
124 &wallet,
125 relays,
126 service_keys,
127 client_secret,
128 max_payment_msat,
129 )
130 }
131
132 #[uniffi::constructor]
148 pub fn restore(
149 wallet: Arc<Wallet>,
150 relays: Vec<String>,
151 service_secret_key: String,
152 client_secret_key: String,
153 max_payment_msat: Option<u64>,
154 ) -> Result<Self, FfiError> {
155 let service_keys = parse_keys(&service_secret_key)?;
156 let client_secret = parse_secret_key(&client_secret_key)?;
157 Self::build(
158 &wallet,
159 relays,
160 service_keys,
161 client_secret,
162 max_payment_msat,
163 )
164 }
165
166 pub fn connection_uri(&self) -> String {
168 self.service.connection_uri().to_string()
169 }
170
171 pub fn service_pubkey(&self) -> String {
173 self.service.service_pubkey().to_hex()
174 }
175
176 pub fn client_pubkey(&self) -> String {
178 self.service.client_pubkey().to_hex()
179 }
180
181 #[allow(clippy::unused_async)]
194 pub async fn start(&self) -> Result<(), FfiError> {
195 let mut guard = self
196 .task
197 .lock()
198 .map_err(|_| FfiError::internal("nwc service lock poisoned"))?;
199
200 Self::clear_finished_task(&mut guard);
201
202 if guard.is_some() {
203 return Err(FfiError::internal("nwc service is already running"));
204 }
205
206 let cancel = CancellationToken::new();
207 let service = self.service.clone();
208 let handler = self.handler.clone();
209 let run_cancel = cancel.clone();
210
211 let handle = tokio::spawn(async move {
212 if let Err(e) = service.run(handler, run_cancel).await {
213 tracing::error!("NWC service stopped with error: {e}");
214 }
215 });
216
217 *guard = Some((handle, cancel));
218 Ok(())
219 }
220
221 pub async fn stop(&self) -> Result<(), FfiError> {
223 let task = {
224 let mut guard = self
225 .task
226 .lock()
227 .map_err(|_| FfiError::internal("nwc service lock poisoned"))?;
228 guard.take()
229 };
230
231 if let Some((handle, cancel)) = task {
232 cancel.cancel();
233 handle.abort();
234 let _ = handle.await;
235 }
236
237 Ok(())
238 }
239
240 pub fn is_running(&self) -> bool {
242 let Ok(mut guard) = self.task.lock() else {
243 return false;
244 };
245
246 Self::clear_finished_task(&mut guard);
247 guard.is_some()
248 }
249}
250
251#[uniffi::export]
261pub fn nwc_derive_service_secret_key_from_seed(seed: Vec<u8>) -> Result<String, FfiError> {
262 if seed.len() < 64 {
263 return Err(FfiError::internal("Seed must be at least 64 bytes"));
264 }
265
266 let seed: [u8; 64] = seed[..64]
267 .try_into()
268 .map_err(|_| FfiError::internal("Failed to read wallet seed bytes"))?;
269
270 let secret_key = cdk::wallet::derive_nwc_secret_key_from_seed(&seed)
271 .map_err(|e| FfiError::internal(format!("Failed to derive secret key: {e}")))?;
272
273 Ok(secret_key.to_secret_hex())
274}
275
276#[uniffi::export]
282pub fn nwc_get_pubkey(nostr_secret_key: String) -> Result<String, FfiError> {
283 Ok(parse_keys(&nostr_secret_key)?.public_key().to_hex())
284}
285
286fn parse_keys(key: &str) -> Result<Keys, FfiError> {
288 Ok(Keys::new(parse_secret_key(key)?))
289}
290
291fn parse_secret_key(key: &str) -> Result<SecretKey, FfiError> {
293 SecretKey::parse(key).map_err(|e| FfiError::internal(format!("invalid secret key: {e}")))
294}