use std::sync::{Arc, Mutex};
use cdk::wallet::WalletNwcHandler;
use cdk_nwc::{NwcService as CdkNwcService, NwcServiceConfig};
use nostr_sdk::{Keys, RelayUrl, SecretKey};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::error::FfiError;
use crate::wallet::Wallet;
#[derive(uniffi::Object)]
pub struct NwcService {
service: CdkNwcService,
handler: Arc<WalletNwcHandler>,
task: Mutex<Option<(JoinHandle<()>, CancellationToken)>>,
}
impl NwcService {
fn clear_finished_task(task: &mut Option<(JoinHandle<()>, CancellationToken)>) {
if task
.as_ref()
.is_some_and(|(handle, _)| handle.is_finished())
{
drop(task.take());
}
}
fn build(
wallet: &Arc<Wallet>,
relays: Vec<String>,
service_keys: Keys,
client_secret: SecretKey,
max_payment_msat: Option<u64>,
) -> Result<Self, FfiError> {
if relays.is_empty() {
return Err(FfiError::internal("at least one relay is required"));
}
let relays = relays
.iter()
.map(|r| {
RelayUrl::parse(r)
.map_err(|e| FfiError::internal(format!("invalid relay {r}: {e}")))
})
.collect::<Result<Vec<_>, _>>()?;
let handler = Arc::new(WalletNwcHandler::new(
wallet.inner().clone(),
max_payment_msat,
));
let service = CdkNwcService::new(NwcServiceConfig {
service_keys,
client_secret,
relays,
lud16: None,
})
.map_err(|e| FfiError::internal(e.to_string()))?;
Ok(Self {
service,
handler,
task: Mutex::new(None),
})
}
}
impl Drop for NwcService {
fn drop(&mut self) {
let Ok(mut guard) = self.task.lock() else {
return;
};
if let Some((handle, cancel)) = guard.take() {
cancel.cancel();
handle.abort();
}
}
}
#[uniffi::export(async_runtime = "tokio")]
impl NwcService {
#[uniffi::constructor]
pub fn create(
wallet: Arc<Wallet>,
relays: Vec<String>,
service_secret_key: String,
max_payment_msat: Option<u64>,
) -> Result<Self, FfiError> {
let service_keys = parse_keys(&service_secret_key)?;
let client_secret = SecretKey::generate();
Self::build(
&wallet,
relays,
service_keys,
client_secret,
max_payment_msat,
)
}
#[uniffi::constructor]
pub fn restore(
wallet: Arc<Wallet>,
relays: Vec<String>,
service_secret_key: String,
client_secret_key: String,
max_payment_msat: Option<u64>,
) -> Result<Self, FfiError> {
let service_keys = parse_keys(&service_secret_key)?;
let client_secret = parse_secret_key(&client_secret_key)?;
Self::build(
&wallet,
relays,
service_keys,
client_secret,
max_payment_msat,
)
}
pub fn connection_uri(&self) -> String {
self.service.connection_uri().to_string()
}
pub fn service_pubkey(&self) -> String {
self.service.service_pubkey().to_hex()
}
pub fn client_pubkey(&self) -> String {
self.service.client_pubkey().to_hex()
}
#[allow(clippy::unused_async)]
pub async fn start(&self) -> Result<(), FfiError> {
let mut guard = self
.task
.lock()
.map_err(|_| FfiError::internal("nwc service lock poisoned"))?;
Self::clear_finished_task(&mut guard);
if guard.is_some() {
return Err(FfiError::internal("nwc service is already running"));
}
let cancel = CancellationToken::new();
let service = self.service.clone();
let handler = self.handler.clone();
let run_cancel = cancel.clone();
let handle = tokio::spawn(async move {
if let Err(e) = service.run(handler, run_cancel).await {
tracing::error!("NWC service stopped with error: {e}");
}
});
*guard = Some((handle, cancel));
Ok(())
}
pub async fn stop(&self) -> Result<(), FfiError> {
let task = {
let mut guard = self
.task
.lock()
.map_err(|_| FfiError::internal("nwc service lock poisoned"))?;
guard.take()
};
if let Some((handle, cancel)) = task {
cancel.cancel();
handle.abort();
let _ = handle.await;
}
Ok(())
}
pub fn is_running(&self) -> bool {
let Ok(mut guard) = self.task.lock() else {
return false;
};
Self::clear_finished_task(&mut guard);
guard.is_some()
}
}
#[uniffi::export]
pub fn nwc_derive_service_secret_key_from_seed(seed: Vec<u8>) -> Result<String, FfiError> {
if seed.len() < 64 {
return Err(FfiError::internal("Seed must be at least 64 bytes"));
}
let seed: [u8; 64] = seed[..64]
.try_into()
.map_err(|_| FfiError::internal("Failed to read wallet seed bytes"))?;
let secret_key = cdk::wallet::derive_nwc_secret_key_from_seed(&seed)
.map_err(|e| FfiError::internal(format!("Failed to derive secret key: {e}")))?;
Ok(secret_key.to_secret_hex())
}
#[uniffi::export]
pub fn nwc_get_pubkey(nostr_secret_key: String) -> Result<String, FfiError> {
Ok(parse_keys(&nostr_secret_key)?.public_key().to_hex())
}
fn parse_keys(key: &str) -> Result<Keys, FfiError> {
Ok(Keys::new(parse_secret_key(key)?))
}
fn parse_secret_key(key: &str) -> Result<SecretKey, FfiError> {
SecretKey::parse(key).map_err(|e| FfiError::internal(format!("invalid secret key: {e}")))
}