use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::sync::Mutex;
use once_cell::sync::Lazy;
use serde_json;
use tokio::runtime::Runtime;
use crate::sdk::identity::{AgentIdentity, AgentIdentityState};
use crate::sdk::storage::EncryptedFileStorage;
use crate::sdk::nostr_client::NostrClient;
use crate::sdk::enrollment::EnrollmentBootstrap;
static TOKIO_RUNTIME: Lazy<Runtime> = Lazy::new(|| {
Runtime::new().expect("Failed to create tokio runtime")
});
pub const FFI_SUCCESS: c_int = 0;
pub const FFI_ERROR_NULL_POINTER: c_int = -1;
pub const FFI_ERROR_INVALID_UTF8: c_int = -2;
pub const FFI_ERROR_NOT_INITIALIZED: c_int = -3;
pub const FFI_ERROR_ALREADY_INITIALIZED: c_int = -4;
pub const FFI_ERROR_STORAGE_FAILED: c_int = -5;
pub const FFI_ERROR_ENROLLMENT_FAILED: c_int = -6;
pub const FFI_ERROR_AUTH_FAILED: c_int = -7;
pub const FFI_ERROR_DELEGATION_INVALID: c_int = -8;
pub const FFI_ERROR_WALLET_FAILED: c_int = -9;
pub const FFI_ERROR_INTERNAL: c_int = -100;
static AGENT_STATE: Lazy<Mutex<Option<AgentState>>> = Lazy::new(|| Mutex::new(None));
struct AgentState {
identity: AgentIdentityState,
storage_path: String,
nwc_uri: Option<String>,
email_mapping: std::collections::HashMap<String, String>,
pending_client_id: Option<String>,
pending_email: Option<String>,
}
#[no_mangle]
pub extern "C" fn agent_initialize() -> c_int {
agent_initialize_with_path(std::ptr::null())
}
#[no_mangle]
pub extern "C" fn agent_initialize_with_path(storage_path: *const c_char) -> c_int {
let mut state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
if state.is_some() {
return FFI_ERROR_ALREADY_INITIALIZED;
}
let path = if storage_path.is_null() {
match dirs::home_dir() {
Some(home) => home.join(".signedby"),
None => return FFI_ERROR_STORAGE_FAILED,
}
} else {
let path_str = match unsafe { CStr::from_ptr(storage_path) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
std::path::PathBuf::from(path_str)
};
let storage = match EncryptedFileStorage::new(path.clone()) {
Ok(s) => s,
Err(_) => return FFI_ERROR_STORAGE_FAILED,
};
let identity = AgentIdentity::new(storage);
let identity_state = if identity.is_initialized() {
match identity.load() {
Ok(s) => s,
Err(_) => return FFI_ERROR_STORAGE_FAILED,
}
} else {
match identity.initialize() {
Ok(s) => s,
Err(_) => return FFI_ERROR_STORAGE_FAILED,
}
};
*state = Some(AgentState {
identity: identity_state,
storage_path: path.to_string_lossy().to_string(),
nwc_uri: None,
email_mapping: std::collections::HashMap::new(),
pending_client_id: None,
pending_email: None,
});
FFI_SUCCESS
}
#[no_mangle]
pub extern "C" fn agent_get_npub() -> *mut c_char {
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let agent_state = match state.as_ref() {
Some(s) => s,
None => return std::ptr::null_mut(),
};
match CString::new(agent_state.identity.agent_npub.clone()) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn agent_get_did() -> *mut c_char {
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let agent_state = match state.as_ref() {
Some(s) => s,
None => return std::ptr::null_mut(),
};
match CString::new(agent_state.identity.did.clone()) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn agent_get_leaf_commitment() -> *mut c_char {
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let agent_state = match state.as_ref() {
Some(s) => s,
None => return std::ptr::null_mut(),
};
match CString::new(agent_state.identity.leaf_commitment.clone()) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn agent_enroll(enterprise_domain: *const c_char) -> c_int {
if enterprise_domain.is_null() {
return FFI_ERROR_NULL_POINTER;
}
let _domain = match unsafe { CStr::from_ptr(enterprise_domain) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
if state.is_none() {
return FFI_ERROR_NOT_INITIALIZED;
}
FFI_SUCCESS
}
#[no_mangle]
pub extern "C" fn agent_set_email_mapping(mappings_json: *const c_char) -> c_int {
if mappings_json.is_null() {
return FFI_ERROR_NULL_POINTER;
}
let json_str = match unsafe { CStr::from_ptr(mappings_json) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
let mapping: std::collections::HashMap<String, String> = match serde_json::from_str(json_str) {
Ok(m) => m,
Err(_) => return FFI_ERROR_INTERNAL,
};
let mut state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
let agent_state = match state.as_mut() {
Some(s) => s,
None => return FFI_ERROR_NOT_INITIALIZED,
};
agent_state.email_mapping = mapping;
FFI_SUCCESS
}
pub type EnrollmentCallback = extern "C" fn(event_type: c_int, event_json: *const c_char);
#[no_mangle]
pub extern "C" fn agent_start_enrollment_watcher(callback: EnrollmentCallback) -> c_int {
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
if state.is_none() {
return FFI_ERROR_NOT_INITIALIZED;
}
let agent_state = state.as_ref().unwrap();
if agent_state.email_mapping.is_empty() {
return FFI_ERROR_ENROLLMENT_FAILED;
}
let email_mapping = agent_state.email_mapping.clone();
let storage_path = agent_state.storage_path.clone();
drop(state);
TOKIO_RUNTIME.spawn(async move {
let storage = match EncryptedFileStorage::new(std::path::PathBuf::from(&storage_path)) {
Ok(s) => s,
Err(e) => {
eprintln!("[ffi] Failed to create storage: {}", e);
return;
}
};
let identity = AgentIdentity::new(storage);
let nostr_client = match NostrClient::new(&identity).await {
Ok(c) => c,
Err(e) => {
eprintln!("[ffi] Failed to create NostrClient: {}", e);
return;
}
};
let mut enrollment = EnrollmentBootstrap::new(
nostr_client,
"https://api.signedbyme.com".to_string(),
email_mapping,
);
let callback_gate = callback;
let callback_complete = callback;
let _ = enrollment.start_enrollment_watcher(
&identity,
move |gate, msg| {
if let Ok(msg_cstr) = CString::new(msg) {
callback_gate(gate as c_int, msg_cstr.as_ptr());
}
},
move |result| {
if let Ok(json) = serde_json::to_string(&result) {
if let Ok(json_cstr) = CString::new(json) {
callback_complete(3, json_cstr.as_ptr());
}
}
},
).await;
});
FFI_SUCCESS
}
#[no_mangle]
pub extern "C" fn agent_submit_challenge_code(
client_id: *const c_char,
email: *const c_char,
challenge: *const c_char,
) -> c_int {
if client_id.is_null() || email.is_null() || challenge.is_null() {
return FFI_ERROR_NULL_POINTER;
}
let client_id_str = match unsafe { CStr::from_ptr(client_id) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
let email_str = match unsafe { CStr::from_ptr(email) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
let challenge_str = match unsafe { CStr::from_ptr(challenge) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
if state.is_none() {
return FFI_ERROR_NOT_INITIALIZED;
}
let agent_state = state.as_ref().unwrap();
let storage_path = agent_state.storage_path.clone();
let email_mapping = agent_state.email_mapping.clone();
drop(state);
let result = TOKIO_RUNTIME.block_on(async {
let storage = match EncryptedFileStorage::new(std::path::PathBuf::from(&storage_path)) {
Ok(s) => s,
Err(e) => {
eprintln!("[ffi] Failed to create storage: {}", e);
return Err(());
}
};
let identity = AgentIdentity::new(storage);
let nostr_client = match NostrClient::new(&identity).await {
Ok(c) => c,
Err(e) => {
eprintln!("[ffi] Failed to create NostrClient: {}", e);
return Err(());
}
};
let enrollment = EnrollmentBootstrap::new(
nostr_client,
"https://api.signedbyme.com".to_string(),
email_mapping,
);
match enrollment.submit_challenge_code(client_id_str, email_str, challenge_str).await {
Ok(event_id) => {
eprintln!("[ffi] Gate 1 complete: Published kind 28202: {}", event_id);
Ok(())
}
Err(e) => {
eprintln!("[ffi] Failed to submit challenge: {}", e);
Err(())
}
}
});
match result {
Ok(()) => FFI_SUCCESS,
Err(()) => FFI_ERROR_ENROLLMENT_FAILED,
}
}
#[no_mangle]
pub extern "C" fn agent_authenticate(enterprise_domain: *const c_char) -> *mut c_char {
if enterprise_domain.is_null() {
return std::ptr::null_mut();
}
let _domain = match unsafe { CStr::from_ptr(enterprise_domain) }.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
if state.is_none() {
return std::ptr::null_mut();
}
std::ptr::null_mut()
}
#[no_mangle]
pub extern "C" fn agent_check_delegation(enterprise_domain: *const c_char) -> c_int {
if enterprise_domain.is_null() {
return FFI_ERROR_NULL_POINTER;
}
let _domain = match unsafe { CStr::from_ptr(enterprise_domain) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
if state.is_none() {
return FFI_ERROR_NOT_INITIALIZED;
}
1 }
#[no_mangle]
pub extern "C" fn agent_setup_wallet(nwc_uri: *const c_char) -> c_int {
if nwc_uri.is_null() {
return FFI_ERROR_NULL_POINTER;
}
let uri = match unsafe { CStr::from_ptr(nwc_uri) }.to_str() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INVALID_UTF8,
};
if !uri.starts_with("nostr+walletconnect://") {
return FFI_ERROR_WALLET_FAILED;
}
let mut state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
let agent_state = match state.as_mut() {
Some(s) => s,
None => return FFI_ERROR_NOT_INITIALIZED,
};
agent_state.nwc_uri = Some(uri.to_string());
FFI_SUCCESS
}
#[no_mangle]
pub extern "C" fn agent_get_lightning_address() -> *mut c_char {
std::ptr::null_mut()
}
#[no_mangle]
pub extern "C" fn agent_create_invoice(
amount_sats: u64,
description: *const c_char,
) -> *mut c_char {
if description.is_null() {
return std::ptr::null_mut();
}
let _desc = match unsafe { CStr::from_ptr(description) }.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let agent_state = match state.as_ref() {
Some(s) => s,
None => return std::ptr::null_mut(),
};
if agent_state.nwc_uri.is_none() {
return std::ptr::null_mut();
}
let _ = amount_sats;
std::ptr::null_mut()
}
#[no_mangle]
pub extern "C" fn agent_pay_invoice(bolt11: *const c_char) -> *mut c_char {
if bolt11.is_null() {
return std::ptr::null_mut();
}
let _invoice = match unsafe { CStr::from_ptr(bolt11) }.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let agent_state = match state.as_ref() {
Some(s) => s,
None => return std::ptr::null_mut(),
};
if agent_state.nwc_uri.is_none() {
return std::ptr::null_mut();
}
std::ptr::null_mut()
}
#[no_mangle]
pub extern "C" fn agent_get_balance() -> i64 {
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return -1,
};
let agent_state = match state.as_ref() {
Some(s) => s,
None => return -1,
};
if agent_state.nwc_uri.is_none() {
return -1;
}
-1
}
#[no_mangle]
pub extern "C" fn agent_string_free(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
let _ = CString::from_raw(ptr);
}
}
}
#[no_mangle]
pub extern "C" fn agent_sdk_version() -> *mut c_char {
match CString::new(env!("CARGO_PKG_VERSION")) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn agent_is_initialized() -> c_int {
let state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return 0,
};
if state.is_some() { 1 } else { 0 }
}
#[no_mangle]
pub extern "C" fn agent_shutdown() -> c_int {
let mut state = match AGENT_STATE.lock() {
Ok(s) => s,
Err(_) => return FFI_ERROR_INTERNAL,
};
*state = None;
FFI_SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sdk_version() {
let version = agent_sdk_version();
assert!(!version.is_null());
unsafe {
let _ = CString::from_raw(version);
}
}
#[test]
fn test_not_initialized() {
{
let mut state = AGENT_STATE.lock().unwrap();
*state = None;
}
assert_eq!(agent_is_initialized(), 0);
assert!(agent_get_npub().is_null());
}
}