Add sync cancellation via AtomicBool flag for FFI consumers

Allows Android/FFI callers to interrupt an in-progress wallet sync
  cleanly between batches. Adds salvium_wallet_stop_sync FFI export,
  WalletError::Cancelled, SyncEvent::Cancelled, and WalletHandle
  wrapper that pairs each wallet with its cancellation flag.
This commit is contained in:
Matt Hess
2026-02-26 02:23:12 +00:00
parent 0cec53f868
commit 65d596a6ea
19 changed files with 1075 additions and 51 deletions
+5 -1
View File
@@ -203,11 +203,15 @@ pub async fn sync_wallet(ctx: &AppContext) -> Result {
error
);
}
SyncEvent::Cancelled { height } => {
println!("Sync cancelled at height {}", height);
}
}
}
});
let _final_height = wallet.sync(&daemon, Some(&tx)).await?;
let no_cancel = std::sync::atomic::AtomicBool::new(false);
let _final_height = wallet.sync(&daemon, Some(&tx), &no_cancel).await?;
drop(tx);
let _ = progress_task.await;
+2
View File
@@ -29,6 +29,8 @@ pub mod storage;
#[cfg(not(target_arch = "wasm32"))]
mod ffi;
mod wasm_ffi;
/// Keccak-256 hash (CryptoNote variant with 0x01 padding, NOT SHA3)
/// Matches Salvium C++ cn_fast_hash / keccak()
#[cfg_attr(feature = "wasm-exports", wasm_bindgen)]
+76
View File
@@ -0,0 +1,76 @@
//! C ABI wrappers for WASM static library consumption.
//!
//! These functions use the caller-provided buffer pattern: the caller passes
//! `out_buf` / `out_len`, and the function writes a UTF-8 result string into
//! the buffer and returns the number of bytes written (0 on error or null
//! pointers).
//!
//! Intended consumer: Cloudflare Worker linking `libsalvium_crypto.a` directly
//! (no wasm-bindgen / JS glue).
use core::slice;
/// Helper: write `src` into the caller's buffer, truncating if necessary.
/// Returns bytes actually written, or 0 if pointers are null.
unsafe fn write_to_buf(src: &[u8], out_buf: *mut u8, out_len: usize) -> usize {
if out_buf.is_null() || out_len == 0 {
return 0;
}
let n = src.len().min(out_len);
unsafe {
core::ptr::copy_nonoverlapping(src.as_ptr(), out_buf, n);
}
n
}
/// Parse a complete transaction from raw bytes to JSON.
///
/// Writes the JSON string into `out_buf` (up to `out_len` bytes) and returns
/// the number of bytes written. Returns 0 on error or null pointers.
#[no_mangle]
pub unsafe extern "C" fn salvium_parse_transaction_bytes(
data: *const u8,
data_len: usize,
out_buf: *mut u8,
out_len: usize,
) -> usize {
if data.is_null() || data_len == 0 {
return 0;
}
let input = unsafe { slice::from_raw_parts(data, data_len) };
let json = crate::parse_transaction_bytes(input);
unsafe { write_to_buf(json.as_bytes(), out_buf, out_len) }
}
/// Parse a complete block from raw bytes to JSON.
///
/// Writes the JSON string into `out_buf` (up to `out_len` bytes) and returns
/// the number of bytes written. Returns 0 on error or null pointers.
#[no_mangle]
pub unsafe extern "C" fn salvium_parse_block_bytes(
data: *const u8,
data_len: usize,
out_buf: *mut u8,
out_len: usize,
) -> usize {
if data.is_null() || data_len == 0 {
return 0;
}
let input = unsafe { slice::from_raw_parts(data, data_len) };
let json = crate::parse_block_bytes(input);
unsafe { write_to_buf(json.as_bytes(), out_buf, out_len) }
}
/// Get the human-readable name for a transaction type code.
///
/// Writes the name string into `out_buf` (up to `out_len` bytes) and returns
/// the number of bytes written. Returns 0 on null pointer.
#[no_mangle]
pub unsafe extern "C" fn salvium_tx_type_name(
tx_type: u32,
out_buf: *mut u8,
out_len: usize,
) -> usize {
let name = crate::wasm_tx_type_name(tx_type as u8);
unsafe { write_to_buf(name.as_bytes(), out_buf, out_len) }
}
+89 -32
View File
@@ -1,6 +1,8 @@
//! Wallet lifecycle, key/address queries, balance, sync, and data operations.
use std::ffi::{c_char, c_void};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::error::{ffi_try, ffi_try_ptr, ffi_try_string};
use crate::handles::{borrow_handle, borrow_handle_mut, drop_handle};
@@ -8,6 +10,21 @@ use crate::strings::c_str_to_str;
use salvium_wallet::Wallet;
/// Wrapper that pairs a Wallet with its cancellation flag.
pub(crate) struct WalletHandle {
pub wallet: Wallet,
pub sync_cancel: Arc<AtomicBool>,
}
impl WalletHandle {
fn new(wallet: Wallet) -> Self {
Self {
wallet,
sync_cancel: Arc::new(AtomicBool::new(false)),
}
}
}
// =============================================================================
// Wallet Lifecycle
// =============================================================================
@@ -36,7 +53,9 @@ pub unsafe extern "C" fn salvium_wallet_create(
let network = int_to_network(network)?;
let path = unsafe { c_str_to_str(db_path) }?;
let key = unsafe { crate::strings::c_buf_to_slice(db_key, db_key_len) }?;
Wallet::create(seed_arr, network, path, key).map_err(|e| e.to_string())
Wallet::create(seed_arr, network, path, key)
.map(WalletHandle::new)
.map_err(|e| e.to_string())
})
}
@@ -56,7 +75,9 @@ pub unsafe extern "C" fn salvium_wallet_from_mnemonic(
let network = int_to_network(network)?;
let path = unsafe { c_str_to_str(db_path) }?;
let key = unsafe { crate::strings::c_buf_to_slice(db_key, db_key_len) }?;
Wallet::from_mnemonic(words, network, path, key).map_err(|e| e.to_string())
Wallet::from_mnemonic(words, network, path, key)
.map(WalletHandle::new)
.map_err(|e| e.to_string())
})
}
@@ -79,14 +100,16 @@ pub unsafe extern "C" fn salvium_wallet_open(
let key = unsafe { crate::strings::c_buf_to_slice(db_key, db_key_len) }?;
let keys = wallet_keys_from_json(json_str)?;
Wallet::open(keys, path, key).map_err(|e| e.to_string())
Wallet::open(keys, path, key)
.map(WalletHandle::new)
.map_err(|e| e.to_string())
})
}
/// Close a wallet handle, releasing all resources.
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_close(handle: *mut c_void) {
drop_handle::<Wallet>(handle);
drop_handle::<WalletHandle>(handle);
}
// =============================================================================
@@ -104,7 +127,7 @@ pub unsafe extern "C" fn salvium_wallet_get_address(
addr_type: i32,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
match addr_type {
0 => wallet.cn_address().map_err(|e| e.to_string()),
1 => wallet.carrot_address().map_err(|e| e.to_string()),
@@ -122,7 +145,7 @@ pub unsafe extern "C" fn salvium_wallet_get_address(
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_get_mnemonic(handle: *mut c_void) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
match wallet.mnemonic() {
Some(Ok(words)) => Ok(words),
Some(Err(e)) => Err(e.to_string()),
@@ -138,7 +161,7 @@ pub unsafe extern "C" fn salvium_wallet_get_mnemonic(handle: *mut c_void) -> *mu
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_get_keys_json(handle: *mut c_void) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let keys = wallet.keys();
let json = serde_json::json!({
"wallet_type": format!("{:?}", keys.wallet_type),
@@ -159,7 +182,9 @@ pub unsafe extern "C" fn salvium_wallet_get_keys_json(handle: *mut c_void) -> *m
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_can_spend(handle: *mut c_void) -> i32 {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }.ok()?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }
.ok()?
.wallet;
Some(wallet.can_spend())
}));
match result {
@@ -175,7 +200,9 @@ pub unsafe extern "C" fn salvium_wallet_can_spend(handle: *mut c_void) -> i32 {
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_network(handle: *mut c_void) -> i32 {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }.ok()?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }
.ok()?
.wallet;
Some(wallet.network())
}));
match result {
@@ -192,7 +219,9 @@ pub unsafe extern "C" fn salvium_wallet_network(handle: *mut c_void) -> i32 {
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_sync_height(handle: *mut c_void) -> u64 {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }.ok()?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }
.ok()?
.wallet;
wallet.sync_height().ok()
}));
match result {
@@ -216,7 +245,7 @@ pub unsafe extern "C" fn salvium_wallet_get_balance(
account_index: i32,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let asset = unsafe { c_str_to_str(asset_type) }?;
let result = wallet
.get_balance(asset, account_index)
@@ -235,7 +264,7 @@ pub unsafe extern "C" fn salvium_wallet_get_all_balances(
account_index: i32,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let result = wallet
.get_all_balances(account_index)
.map_err(|e| e.to_string())?;
@@ -255,7 +284,7 @@ pub unsafe extern "C" fn salvium_wallet_set_attribute(
value: *const c_char,
) -> i32 {
ffi_try(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let k = unsafe { c_str_to_str(key) }?;
let v = unsafe { c_str_to_str(value) }?;
wallet.set_attribute(k, v).map_err(|e| e.to_string())
@@ -272,7 +301,7 @@ pub unsafe extern "C" fn salvium_wallet_get_attribute(
key: *const c_char,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let k = unsafe { c_str_to_str(key) }?;
match wallet.get_attribute(k).map_err(|e| e.to_string())? {
Some(v) => Ok(v),
@@ -285,7 +314,7 @@ pub unsafe extern "C" fn salvium_wallet_get_attribute(
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_reset_sync_height(handle: *mut c_void, height: u64) -> i32 {
ffi_try(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
wallet.reset_sync_height(height).map_err(|e| e.to_string())
})
}
@@ -296,7 +325,7 @@ pub unsafe extern "C" fn salvium_wallet_reset_sync_height(handle: *mut c_void, h
/// Sync callback function pointer type.
///
/// - `event_type`: 0=started, 1=progress, 2=complete, 3=reorg, 4=error
/// - `event_type`: 0=started, 1=progress, 2=complete, 3=reorg, 4=error, 5=parse_error, 6=cancelled
/// - `current_height`: current scan height
/// - `target_height`: target chain height
/// - `outputs_found`: number of outputs found so far
@@ -324,10 +353,13 @@ pub unsafe extern "C" fn salvium_wallet_sync(
callback: Option<SyncCallbackFn>,
) -> i32 {
ffi_try(|| {
let wallet_ref = unsafe { borrow_handle_mut::<Wallet>(wallet) }?;
let handle = unsafe { borrow_handle_mut::<WalletHandle>(wallet) }?;
let daemon_ref = unsafe { borrow_handle::<salvium_rpc::DaemonRpc>(daemon) }?;
let rt = crate::runtime();
// Reset cancel flag before starting.
handle.sync_cancel.store(false, Ordering::Relaxed);
rt.block_on(async {
if let Some(cb) = callback {
let (tx, mut rx) = tokio::sync::mpsc::channel::<salvium_wallet::SyncEvent>(256);
@@ -339,14 +371,18 @@ pub unsafe extern "C" fn salvium_wallet_sync(
}
});
let result = wallet_ref.sync(daemon_ref, Some(&tx)).await;
let result = handle
.wallet
.sync(daemon_ref, Some(&tx), &handle.sync_cancel)
.await;
drop(tx); // Close channel so forwarder exits.
let _ = forwarder.await;
result.map(|_| ()).map_err(|e| e.to_string())
} else {
wallet_ref
.sync(daemon_ref, None)
handle
.wallet
.sync(daemon_ref, None, &handle.sync_cancel)
.await
.map(|_| ())
.map_err(|e| e.to_string())
@@ -355,6 +391,22 @@ pub unsafe extern "C" fn salvium_wallet_sync(
})
}
/// Cancel an in-progress sync.
///
/// Sets the cancellation flag. The sync loop will stop before the next
/// batch and return `WalletError::Cancelled`. Safe to call from any thread.
/// No-op if no sync is running.
///
/// Returns 0 on success, -1 on error.
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_stop_sync(wallet: *mut c_void) -> i32 {
ffi_try(|| {
let handle = unsafe { borrow_handle::<WalletHandle>(wallet) }?;
handle.sync_cancel.store(true, Ordering::Relaxed);
Ok(())
})
}
/// Dispatch a SyncEvent to the C callback.
fn dispatch_sync_event(event: &salvium_wallet::SyncEvent, cb: SyncCallbackFn) {
use salvium_wallet::SyncEvent;
@@ -412,6 +464,9 @@ fn dispatch_sync_event(event: &salvium_wallet::SyncEvent, cb: SyncCallbackFn) {
}
}
}
SyncEvent::Cancelled { height } => unsafe {
cb(6, *height, 0, 0, std::ptr::null());
},
}
}
@@ -433,7 +488,7 @@ pub unsafe extern "C" fn salvium_wallet_get_transfers(
filters_json: *const c_char,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let json_str = unsafe { c_str_to_str(filters_json) }?;
let query: salvium_wallet::TxQuery =
serde_json::from_str(json_str).map_err(|e| format!("invalid query JSON: {e}"))?;
@@ -456,7 +511,7 @@ pub unsafe extern "C" fn salvium_wallet_get_outputs(
query_json: *const c_char,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let json_str = unsafe { c_str_to_str(query_json) }?;
let query: salvium_wallet::OutputQuery =
serde_json::from_str(json_str).map_err(|e| format!("invalid query JSON: {e}"))?;
@@ -476,7 +531,7 @@ pub unsafe extern "C" fn salvium_wallet_get_stakes(
status: *const c_char,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let status_opt = if status.is_null() {
None
} else {
@@ -497,7 +552,7 @@ pub unsafe extern "C" fn salvium_wallet_get_stakes(
#[no_mangle]
pub unsafe extern "C" fn salvium_wallet_address_book_list(handle: *mut c_void) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let entries = wallet.get_address_book().map_err(|e| e.to_string())?;
serde_json::to_string(&entries).map_err(|e| e.to_string())
})
@@ -514,7 +569,7 @@ pub unsafe extern "C" fn salvium_wallet_address_book_add(
description: *const c_char,
) -> i64 {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let addr = unsafe { c_str_to_str(address) }?;
let lbl = unsafe { c_str_to_str(label) }?;
let desc = unsafe { c_str_to_str(description) }?;
@@ -544,7 +599,7 @@ pub unsafe extern "C" fn salvium_wallet_address_book_delete(
row_id: i64,
) -> i32 {
ffi_try(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
wallet
.delete_address_book_entry(row_id)
.map(|_| ())
@@ -564,7 +619,7 @@ pub unsafe extern "C" fn salvium_wallet_set_tx_note(
note: *const c_char,
) -> i32 {
ffi_try(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let hash = unsafe { c_str_to_str(tx_hash) }?;
let n = unsafe { c_str_to_str(note) }?;
wallet.set_tx_note(hash, n).map_err(|e| e.to_string())
@@ -582,7 +637,7 @@ pub unsafe extern "C" fn salvium_wallet_get_tx_notes(
tx_hashes_json: *const c_char,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let json_str = unsafe { c_str_to_str(tx_hashes_json) }?;
let hashes: Vec<String> =
serde_json::from_str(json_str).map_err(|e| format!("invalid JSON array: {e}"))?;
@@ -603,7 +658,7 @@ pub unsafe extern "C" fn salvium_wallet_freeze_output(
key_image: *const c_char,
) -> i32 {
ffi_try(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let ki = unsafe { c_str_to_str(key_image) }?;
wallet.freeze_output(ki).map_err(|e| e.to_string())
})
@@ -616,7 +671,7 @@ pub unsafe extern "C" fn salvium_wallet_thaw_output(
key_image: *const c_char,
) -> i32 {
ffi_try(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let ki = unsafe { c_str_to_str(key_image) }?;
wallet.thaw_output(ki).map_err(|e| e.to_string())
})
@@ -641,7 +696,7 @@ pub unsafe extern "C" fn salvium_wallet_export_blob(
pin: *const c_char,
) -> *mut c_char {
ffi_try_string(|| {
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
let pin_str = unsafe { c_str_to_str(pin) }?;
let keys = wallet.keys();
@@ -714,7 +769,9 @@ pub unsafe extern "C" fn salvium_wallet_import_blob(
return Err("blob contains neither seed nor keys".into());
};
Wallet::open(keys, path, &data_key).map_err(|e| e.to_string())
Wallet::open(keys, path, &data_key)
.map(WalletHandle::new)
.map_err(|e| e.to_string())
})
}
+6 -2
View File
@@ -274,12 +274,16 @@ async fn wallet_sync_and_balance_check() {
height, blob_len, error
);
}
SyncEvent::Cancelled { height } => {
println!(" ** Sync cancelled at height {} **", height);
}
}
}
});
let no_cancel = std::sync::atomic::AtomicBool::new(false);
let sync_height = wallet
.sync(&daemon, Some(&event_tx))
.sync(&daemon, Some(&event_tx), &no_cancel)
.await
.expect("wallet sync failed");
@@ -442,7 +446,7 @@ async fn wallet_sync_and_balance_check() {
// ── Sync idempotency check ──────────────────────────────────────────
println!("\n Verifying sync idempotency...");
let height2 = wallet
.sync(&daemon, None)
.sync(&daemon, None, &no_cancel)
.await
.expect("second sync failed");
let all_balances_2 = wallet.get_all_balances(0).unwrap();
+5 -1
View File
@@ -317,13 +317,17 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
);
}
SyncEvent::Started { .. } => {}
SyncEvent::Cancelled { height } => {
println!(" ** Sync cancelled at height {} **", height);
}
}
}
(final_parse_errors, final_empty_blobs)
});
let sync_result = wallet.sync(&daemon, Some(&tx)).await;
let no_cancel = std::sync::atomic::AtomicBool::new(false);
let sync_result = wallet.sync(&daemon, Some(&tx), &no_cancel).await;
drop(tx); // close channel so progress task finishes
let (final_parse_errors, final_empty_blobs) = progress_handle.await.unwrap_or((0, 0));
+3
View File
@@ -52,6 +52,9 @@ pub enum WalletError {
#[error("hardware device error: {0}")]
Device(String),
#[error("sync cancelled by caller")]
Cancelled,
#[error("{0}")]
Other(String),
}
+10
View File
@@ -37,6 +37,8 @@ pub enum SyncEvent {
blob_len: usize,
error: String,
},
/// Sync cancelled by caller.
Cancelled { height: u64 },
}
/// Blockchain sync engine.
@@ -119,6 +121,7 @@ impl SyncEngine {
scan_ctx: &mut ScanContext,
stake_lock_period: u64,
event_tx: Option<&tokio::sync::mpsc::Sender<SyncEvent>>,
cancel: &std::sync::atomic::AtomicBool,
) -> Result<u64, WalletError> {
let daemon_height = daemon
.get_height()
@@ -167,6 +170,13 @@ impl SyncEngine {
let mut controller = BatchController::new();
while current < top_block {
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
if let Some(tx) = event_tx {
let _ = tx.send(SyncEvent::Cancelled { height: current }).await;
}
return Err(WalletError::Cancelled);
}
let remaining = top_block - current;
let batch_size = controller.next_batch_size(remaining);
let batch_start = current + 1;
+2
View File
@@ -225,6 +225,7 @@ impl Wallet {
&mut self,
daemon: &salvium_rpc::DaemonRpc,
event_tx: Option<&tokio::sync::mpsc::Sender<SyncEvent>>,
cancel: &std::sync::atomic::AtomicBool,
) -> Result<u64, WalletError> {
let lock_period =
salvium_types::constants::network_config(self.network()).stake_lock_period;
@@ -234,6 +235,7 @@ impl Wallet {
&mut self.scan_context,
lock_period,
event_tx,
cancel,
)
.await
}
+2 -1
View File
@@ -1145,8 +1145,9 @@ async fn sync_wallet_checked(wallet: &mut Wallet, daemon: &DaemonRpc, label: &st
});
let t0 = Instant::now();
let no_cancel = std::sync::atomic::AtomicBool::new(false);
let sync_height = wallet
.sync(daemon, Some(&event_tx))
.sync(daemon, Some(&event_tx), &no_cancel)
.await
.expect("sync failed");
drop(event_tx);
+8 -2
View File
@@ -74,7 +74,10 @@ async fn test_burn_transaction_build() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
let hf_info = d.hard_fork_info().await.unwrap();
@@ -138,7 +141,10 @@ async fn test_burn_submit_testnet() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
let hf_info = d.hard_fork_info().await.unwrap();
@@ -74,7 +74,10 @@ async fn test_convert_transaction_build() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
let hf_info = d.hard_fork_info().await.unwrap();
@@ -136,7 +139,10 @@ async fn test_convert_expected_rejection() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
let hf_info = d.hard_fork_info().await.unwrap();
+12 -3
View File
@@ -76,7 +76,10 @@ async fn test_stake_transaction_build() {
// Sync
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
// Get hardfork info for asset type
@@ -190,7 +193,10 @@ async fn test_stake_submit_testnet() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
let hf_info = d.hard_fork_info().await.unwrap();
@@ -467,7 +473,10 @@ async fn test_stake_return_detection() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
// Look for protocol TX outputs (stake returns).
@@ -336,7 +336,10 @@ async fn test_self_transfer_to_subaddress() {
.expect("create wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height {}", sync_height);
let hf_info = d.hard_fork_info().await.unwrap();
+18 -5
View File
@@ -57,7 +57,10 @@ async fn test_full_sync_balance() {
println!("Daemon height: {}", info.height);
println!("Network: testnet");
let sync_height = wallet.sync(&d, None).await.expect("sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("sync failed");
println!("Synced to height: {}", sync_height);
// Sync height should be close to daemon height
@@ -140,7 +143,10 @@ async fn test_view_only_sync() {
.expect("open view-only wallet");
let d = daemon();
let sync_height = wallet.sync(&d, None).await.expect("view-only sync failed");
let sync_height = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("view-only sync failed");
println!("View-only synced to height: {}", sync_height);
// View-only wallet should detect CN outputs
@@ -204,8 +210,9 @@ async fn test_carrot_view_only_sync() {
.expect("open CARROT view-only wallet");
let d = daemon();
let no_cancel = std::sync::atomic::AtomicBool::new(false);
let sync_height = wallet
.sync(&d, None)
.sync(&d, None, &no_cancel)
.await
.expect("CARROT view-only sync failed");
println!("CARROT view-only synced to height: {}", sync_height);
@@ -259,7 +266,10 @@ async fn test_sync_idempotent() {
let d = daemon();
// First sync
let height_1 = wallet.sync(&d, None).await.expect("first sync failed");
let height_1 = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("first sync failed");
let bal_1 = wallet.get_balance("SAL", 0).unwrap();
let total_1: u64 = bal_1.balance.parse().unwrap_or(0);
let unlocked_1: u64 = bal_1.unlocked_balance.parse().unwrap_or(0);
@@ -271,7 +281,10 @@ async fn test_sync_idempotent() {
);
// Second sync (should be a no-op or near-instant)
let height_2 = wallet.sync(&d, None).await.expect("second sync failed");
let height_2 = wallet
.sync(&d, None, &std::sync::atomic::AtomicBool::new(false))
.await
.expect("second sync failed");
let bal_2 = wallet.get_balance("SAL", 0).unwrap();
let total_2: u64 = bal_2.balance.parse().unwrap_or(0);
let unlocked_2: u64 = bal_2.unlocked_balance.parse().unwrap_or(0);
@@ -135,8 +135,9 @@ async fn test_real_testnet_transfer() {
}
});
let no_cancel = std::sync::atomic::AtomicBool::new(false);
let sync_height = wallet
.sync(&daemon, Some(&event_tx))
.sync(&daemon, Some(&event_tx), &no_cancel)
.await
.expect("sync failed");
drop(event_tx);
+477
View File
@@ -0,0 +1,477 @@
# Explorer WASM Integration Spec
How to use the `salvium-explorer` WASM module in a Cloudflare Worker or browser to parse blocks, analyze transactions, and scan outputs.
## 1. Build
```bash
# Build the wasm-bindgen package (JS glue + .wasm binary)
wasm-pack build crates/salvium-explorer --release --target bundler --out-dir pkg
# Output:
# crates/salvium-explorer/pkg/salvium_explorer.js (JS glue)
# crates/salvium-explorer/pkg/salvium_explorer_bg.wasm (WASM binary)
# crates/salvium-explorer/pkg/salvium_explorer.d.ts (TypeScript types)
# crates/salvium-explorer/pkg/package.json
```
Or use the build script which also produces the C ABI static library:
```bash
scripts/build-wasm.sh
# Output in prebuilt/wasm/
```
## 2. Initialize the Module
### Cloudflare Worker (bundler target)
```typescript
import init, { initSync } from 'salvium-explorer';
import wasmModule from 'salvium-explorer/salvium_explorer_bg.wasm';
// Synchronous init (preferred for Workers)
initSync({ module: wasmModule });
// Or async init
await init({ module_or_path: wasmModule });
```
### Browser
```typescript
import init from 'salvium-explorer';
await init(); // Fetches .wasm from same directory
```
After initialization, all exported functions are available as direct imports.
## 3. Explorer-Specific APIs
These are the 3 high-level functions unique to the explorer crate. They combine multiple salvium-crypto primitives into single calls.
### 3a. `parse_and_analyze_tx`
Parse a raw transaction binary and return enriched JSON.
```typescript
import { parse_and_analyze_tx } from 'salvium-explorer';
const result: string = parse_and_analyze_tx(txBytes: Uint8Array);
const tx = JSON.parse(result);
```
**Returns** all fields from `parse_transaction_bytes()` plus these analysis fields:
| Field | Type | Description |
|-------|------|-------------|
| `tx_type_name` | string | Human-readable TX type (e.g. `"Transfer"`, `"Miner"`, `"Protocol"`) |
| `rct_type_name` | string | Human-readable RCT type (e.g. `"RctBulletproofPlus"`) |
| `input_count` | number | Number of inputs |
| `output_count` | number | Number of outputs |
| `is_coinbase` | boolean | Whether this is a miner/coinbase TX |
| `is_carrot` | boolean | Whether any output uses CARROT format (type `0x04`) |
| `key_images` | string[] | Key image hex strings from inputs |
| `output_keys` | string[] | Output public key hex strings |
| `fee` | string | Transaction fee in atomic units (decimal string) |
**Error:** Returns `{"error": "..."}` on parse failure.
**Example response (trimmed):**
```json
{
"prefix": {
"version": 3,
"txType": 3,
"unlockTime": "0",
"vin": [...],
"vout": [...],
"extra": { "pubkey": "ab12...", ... }
},
"rct": {
"type": 6,
"txnFee": "24960000",
...
},
"tx_type_name": "Transfer",
"rct_type_name": "RctBulletproofPlus",
"input_count": 2,
"output_count": 2,
"is_coinbase": false,
"is_carrot": true,
"key_images": ["aabb...", "ccdd..."],
"output_keys": ["eeff...", "1122..."],
"fee": "24960000"
}
```
### 3b. `parse_and_analyze_block`
Parse a raw block binary and return enriched JSON.
```typescript
import { parse_and_analyze_block } from 'salvium-explorer';
const result: string = parse_and_analyze_block(blockBytes: Uint8Array);
const block = JSON.parse(result);
```
**Returns** all fields from `parse_block_bytes()` plus:
| Field | Type | Description |
|-------|------|-------------|
| `tx_count` | number | Number of transaction hashes (excluding miner tx) |
**Base block fields:**
| Field | Type | Description |
|-------|------|-------------|
| `majorVersion` | number | Block major version |
| `minorVersion` | number | Block minor version |
| `timestamp` | number | Block timestamp (Unix) |
| `prevId` | string | Previous block hash (hex) |
| `nonce` | number | Mining nonce |
| `minerTx` | object | Miner transaction (full parsed TX) |
| `txHashes` | string[] | Transaction hashes in this block (hex) |
### 3c. `decode_outputs_for_view_key`
Scan a transaction for owned outputs using a view key pair.
```typescript
import { decode_outputs_for_view_key } from 'salvium-explorer';
const result: string = decode_outputs_for_view_key(
txBytes: Uint8Array, // raw transaction binary
viewSecret: Uint8Array, // 32-byte view secret key
spendPub: Uint8Array, // 32-byte spend public key
);
const outputs = JSON.parse(result);
```
**Parameters:**
- `txBytes`: Raw transaction blob (binary, NOT hex)
- `viewSecret`: 32-byte view secret key (the private view key)
- `spendPub`: 32-byte spend public key (the public spend key)
**Returns** a JSON array of owned outputs:
```json
[
{
"output_index": 0,
"amount": "1000000000",
"output_key": "aabbccdd...",
"subaddress_major": 0,
"subaddress_minor": 0
}
]
```
| Field | Type | Description |
|-------|------|-------------|
| `output_index` | number | Index within the transaction's outputs |
| `amount` | string | Amount in atomic units (1 SAL = 100,000,000) |
| `output_key` | string | Output public key (hex) |
| `subaddress_major` | number | Subaddress major index (0 = main) |
| `subaddress_minor` | number | Subaddress minor index (0 = main) |
Returns `[]` if no outputs match. Returns `{"error": "..."}` on failure.
**How it works internally:**
1. Parses the transaction binary to extract `vout` and `extra.pubkey`
2. Computes key derivation: `D = 8 * view_secret * tx_pub_key`
3. For each output at index `i`: derives expected key `P' = H(D, i)*G + spend_pub`
4. Compares `P'` against the actual output key — match means the output is owned
**Note:** This is a CryptoNote (legacy) scan only. CARROT output scanning requires the full CARROT key set — use the low-level CARROT helpers for that (see Section 5).
## 4. Core Parsing Functions
These do raw binary parsing without the analysis enrichment.
### `parse_transaction_bytes`
```typescript
import { parse_transaction_bytes } from 'salvium-explorer';
const json: string = parse_transaction_bytes(data: Uint8Array);
```
Returns the full parsed transaction as JSON. All binary fields are hex-encoded, amounts are decimal strings.
### `parse_block_bytes`
```typescript
import { parse_block_bytes } from 'salvium-explorer';
const json: string = parse_block_bytes(data: Uint8Array);
```
### `parse_extra`
```typescript
import { parse_extra } from 'salvium-explorer';
const json: string = parse_extra(extraBytes: Uint8Array);
```
Parses just the TX extra field. Returns JSON with:
- `pubkey`: TX public key (hex)
- `nonces`: payment IDs, extra nonces
- `additionalPubkeys`: additional TX public keys (for subaddresses)
### `compute_tx_prefix_hash`
```typescript
import { compute_tx_prefix_hash } from 'salvium-explorer';
const hash: Uint8Array = compute_tx_prefix_hash(data: Uint8Array);
// Returns 32-byte keccak256 of the TX prefix
```
## 5. Crypto Primitives Available
The explorer re-exports the full salvium-crypto function set. These are the ones most relevant for explorer use:
### Hashing
```typescript
keccak256(data: Uint8Array): Uint8Array // 32-byte CryptoNote hash
blake2b_hash(data: Uint8Array, outLen: number): Uint8Array
sha256(data: Uint8Array): Uint8Array
```
### Key Operations
```typescript
// CryptoNote key derivation (used by decode_outputs_for_view_key internally)
generate_key_derivation(pubKey: Uint8Array, secKey: Uint8Array): Uint8Array
derive_public_key(derivation: Uint8Array, outputIndex: number, basePub: Uint8Array): Uint8Array
// Key image
generate_key_image(pubKey: Uint8Array, secKey: Uint8Array): Uint8Array
is_valid_key_image(keyImage: Uint8Array): boolean
// Point operations
scalar_mult_base(s: Uint8Array): Uint8Array // s*G
hash_to_point(data: Uint8Array): Uint8Array // H_p(data)
```
### Address Utilities
```typescript
wasm_parse_address(address: string): string // Address → JSON
wasm_is_valid_address(address: string): boolean // Validate
wasm_describe_address(address: string): string // Human-readable description
wasm_create_address( // Create from components
network: number, // 0=mainnet, 1=testnet, 2=stagenet
format: number, // 0=CryptoNote, 1=CARROT
addrType: number, // 0=standard, 1=subaddress, 2=integrated
spendKey: Uint8Array,
viewKey: Uint8Array
): string
wasm_to_integrated_address(address: string, paymentId: Uint8Array): string
```
**`wasm_parse_address` returns:**
```json
{
"network": "mainnet",
"format": "CryptoNote",
"address_type": "Standard",
"spend_public_key": "hex64",
"view_public_key": "hex64"
}
```
### TX Type / RCT Type Names
```typescript
wasm_tx_type_name(txType: number): string // e.g. "Transfer", "Miner", "Protocol"
wasm_rct_type_name(rctType: number): string // e.g. "RctBulletproofPlus"
```
### CARROT Output Scanning (Low-Level)
For full CARROT scanning (beyond what `decode_outputs_for_view_key` provides):
```typescript
// Build input context for a TX
make_input_context_rct(firstKeyImage: Uint8Array): Uint8Array // Regular TX
make_input_context_coinbase(blockHeight: bigint): Uint8Array // Coinbase TX
// View tag check (fast rejection)
compute_carrot_view_tag(sSrUnctx: Uint8Array, inputContext: Uint8Array, ko: Uint8Array): Uint8Array
// After view tag matches — full decryption
decrypt_carrot_amount(encAmount: Uint8Array, sSrCtx: Uint8Array, ko: Uint8Array): bigint
recover_carrot_address_spend_pubkey(ko: Uint8Array, sSrCtx: Uint8Array, commitment: Uint8Array): Uint8Array
derive_carrot_commitment_mask(sSrCtx: Uint8Array, amount: bigint, addressSpendPubkey: Uint8Array, enoteType: number): Uint8Array
// CARROT key derivation (9 keys from master secret)
derive_carrot_keys_batch(masterSecret: Uint8Array): Uint8Array // Returns 288 bytes (9 × 32)
derive_carrot_view_only_keys_batch(viewBalanceSecret: Uint8Array, accountSpendPubkey: Uint8Array): Uint8Array
// Subaddress map generation
carrot_subaddress_map_batch(
accountSpendPubkey: Uint8Array,
accountViewPubkey: Uint8Array,
generateAddressSecret: Uint8Array,
majorCount: number,
minorCount: number
): Uint8Array // Returns majorCount * minorCount * 40 bytes (32-byte key + 4-byte major + 4-byte minor each)
```
### Verification
```typescript
// Full RCT signature verification (all ring sigs + bulletproofs in one call)
verify_rct_signatures_wasm(
rctType: number,
inputCount: number,
ringSize: number,
txPrefixHash: Uint8Array,
rctBaseBytes: Uint8Array,
bpComponents: Uint8Array,
keyImagesFlat: Uint8Array,
pseudoOutsFlat: Uint8Array,
sigsFlat: Uint8Array,
ringPubkeysFlat: Uint8Array,
ringCommitmentsFlat: Uint8Array
): Uint8Array // Returns JSON with verification result
```
## 6. Typical Explorer Workflow
### Block Page
```typescript
import { parse_and_analyze_block, parse_and_analyze_tx } from 'salvium-explorer';
// 1. Fetch block from daemon RPC (get_block endpoint returns binary blob)
const blockBlob: Uint8Array = await fetchBlockBlob(height);
// 2. Parse + analyze the block
const block = JSON.parse(parse_and_analyze_block(blockBlob));
// block.majorVersion, block.timestamp, block.prevId, block.tx_count, ...
// 3. The miner TX is embedded in the block
const minerTx = block.minerTx;
// minerTx.prefix.vout → miner reward outputs
// 4. Fetch and parse each transaction
for (const txHash of block.txHashes) {
const txBlob: Uint8Array = await fetchTxBlob(txHash);
const tx = JSON.parse(parse_and_analyze_tx(txBlob));
// tx.tx_type_name, tx.input_count, tx.output_count, tx.fee, ...
}
```
### Transaction Page
```typescript
import { parse_and_analyze_tx, wasm_tx_type_name } from 'salvium-explorer';
const tx = JSON.parse(parse_and_analyze_tx(txBlob));
// Display summary
console.log(`Type: ${tx.tx_type_name}`); // "Transfer"
console.log(`Fee: ${tx.fee} atomic`); // "24960000"
console.log(`Inputs: ${tx.input_count}`);
console.log(`Outputs: ${tx.output_count}`);
console.log(`CARROT: ${tx.is_carrot}`);
console.log(`Coinbase: ${tx.is_coinbase}`);
// Key images (for double-spend checking)
for (const ki of tx.key_images) {
console.log(`Key image: ${ki}`);
}
// Output keys (for output lookup)
for (const ok of tx.output_keys) {
console.log(`Output key: ${ok}`);
}
```
### Output Decoding (View Key Search)
```typescript
import { decode_outputs_for_view_key } from 'salvium-explorer';
// User provides their view key + spend public key
const viewSecret = hexToBytes(viewSecretHex); // 32 bytes
const spendPub = hexToBytes(spendPubHex); // 32 bytes
const owned = JSON.parse(decode_outputs_for_view_key(txBlob, viewSecret, spendPub));
for (const out of owned) {
console.log(`Output #${out.output_index}: ${out.amount} atomic SAL`);
console.log(` Key: ${out.output_key}`);
}
```
### Address Validation
```typescript
import { wasm_is_valid_address, wasm_parse_address } from 'salvium-explorer';
if (wasm_is_valid_address(userAddress)) {
const info = JSON.parse(wasm_parse_address(userAddress));
console.log(`Network: ${info.network}, Type: ${info.address_type}`);
}
```
## 7. Data Types — All Inputs are Raw Bytes
Every function that takes transaction or block data expects **raw binary** (`Uint8Array`), not hex strings. If your daemon returns hex, decode first:
```typescript
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
// Daemon RPC returns hex → convert before passing to WASM
const txHex: string = await daemon.getTransaction(txHash);
const txBytes: Uint8Array = hexToBytes(txHex);
const result = parse_and_analyze_tx(txBytes);
```
## 8. Error Handling
All string-returning functions follow the same pattern:
```typescript
const result = JSON.parse(parse_and_analyze_tx(txBytes));
if (result.error) {
// Parse failed
console.error(result.error);
} else {
// Success — use result fields
}
```
For functions returning `Uint8Array`: an empty array (`length === 0`) indicates an error (invalid input, failed point decompression, etc).
For functions returning `boolean`: they return `false` on invalid input.
## 9. Memory / Performance Notes
- The WASM module is ~4MB (uncompressed). Cloudflare Workers supports this.
- All functions are synchronous — no async/await needed after `initSync()`.
- The module manages its own WASM linear memory. `Uint8Array` inputs are copied into WASM memory and results are copied out — no manual memory management needed from JS.
- `parse_and_analyze_tx` and `parse_and_analyze_block` do JSON serialization internally (serde_json). For hot paths parsing thousands of transactions, prefer `parse_transaction_bytes` and extract only the fields you need.
## Source Files
| File | What |
|------|------|
| `crates/salvium-explorer/src/lib.rs` | Explorer WASM APIs (3 custom + 61 re-exports) |
| `crates/salvium-explorer/Cargo.toml` | Crate config |
| `crates/salvium-explorer/pkg/` | Built wasm-pack output (JS glue, .wasm, .d.ts) |
| `crates/salvium-crypto/src/lib.rs` | Underlying crypto implementations |
| `crates/salvium-crypto/src/wasm_ffi.rs` | C ABI static lib (alternative to wasm-bindgen) |
| `crates/salvium-crypto/src/tx_parse.rs` | Transaction/block binary parser |
| `scripts/build-wasm.sh` | Build script (wasm-pack + staticlib) |
+336
View File
@@ -0,0 +1,336 @@
# Wallet Sync Integration Spec
How to use the salvium-rs FFI library to create a wallet, sync it, and read data.
## 1. Initialization
```c
// Optional — runtime is created lazily on first FFI call.
salvium_ffi_init();
```
## 2. Connect to Daemon
```c
void* daemon = salvium_daemon_connect("http://seed01.salvium.io:19081");
if (!daemon) {
const char* err = salvium_last_error(); // DO NOT free this pointer
// handle error...
}
```
**Seed nodes:**
| Network | URL |
|----------|----------------------------------------|
| Mainnet | `http://seed01.salvium.io:19081` |
| Testnet | `http://seed01.salvium.io:29081` |
| Stagenet | `http://seed01.salvium.io:39081` |
Verify daemon is ready before syncing:
```c
int synced = salvium_daemon_is_synchronized(daemon);
// 1 = synced, 0 = still syncing, -1 = error
```
## 3. Create / Open Wallet
Three options — all return an opaque `void*` handle (null on error).
### 3a. From 25-word mnemonic
```c
void* wallet = salvium_wallet_from_mnemonic(
"word1 word2 ... word25", // null-terminated C string
0, // network: 0=Mainnet, 1=Testnet, 2=Stagenet
"/path/to/wallet.db", // database file path
db_key, // uint8_t* encryption key
db_key_len // size_t key length
);
```
### 3b. From 32-byte seed
```c
void* wallet = salvium_wallet_create(
seed_bytes, // const uint8_t[32]
0, // network
"/path/to/wallet.db",
db_key,
db_key_len
);
```
### 3c. From JSON keys (view-only supported)
```c
// Full wallet: {"seed": "hex64", "network": "mainnet"}
// View-only: {"view_secret_key": "hex64", "spend_public_key": "hex64", "network": "mainnet"}
void* wallet = salvium_wallet_open(keys_json, "/path/to/wallet.db", db_key, db_key_len);
```
### 3d. From PIN-encrypted blob
```c
void* wallet = salvium_wallet_import_blob(blob_json, pin, "/path/to/wallet.db");
```
## 4. Sync
### The call
```c
int rc = salvium_wallet_sync(wallet, daemon, my_callback);
// rc: 0 = success, -1 = error
```
**This function blocks until sync is complete.** It handles everything internally:
- Fetches blocks from the daemon in adaptive batches (2-1000 blocks per HTTP request)
- Parses each block and scans all transaction outputs for owned funds
- Detects chain reorganizations and rolls back automatically
- Stores matched outputs and transactions in the wallet database
- Updates sync height per-block for crash safety
### Callback (optional, may be NULL)
```c
typedef void (*SyncCallbackFn)(
int event_type, // event code (see below)
uint64_t current_height, // current scan position
uint64_t target_height, // chain tip
uint32_t outputs_found, // cumulative owned outputs found
const char* error_msg // null unless event_type=4 or 5
);
void my_callback(int type, uint64_t cur, uint64_t target, uint32_t outs, const char* msg) {
switch (type) {
case 0: printf("Started: target=%llu\n", target); break;
case 1: printf("Progress: %llu/%llu (%u outputs)\n", cur, target, outs); break;
case 2: printf("Complete: height=%llu\n", cur); break;
case 3: printf("Reorg: %llu -> %llu\n", cur, target); break;
case 4: printf("Error: %s\n", msg); break;
case 5: printf("Parse error at %llu: %s\n", cur, msg); break;
}
}
```
| Event | Code | Frequency | Fields |
|-------|------|-----------|--------|
| Started | 0 | Once at start | `target_height` |
| Progress | 1 | Per batch (~every 2000 blocks) | `current_height`, `target_height`, `outputs_found` |
| Complete | 2 | Once at end | `current_height` = final height |
| Reorg | 3 | When chain fork detected | `current_height` = old tip, `target_height` = fork point |
| Error | 4 | On RPC/network error | `error_msg` = description |
| ParseError | 5 | On block parse failure | `current_height` = block height, `error_msg` = details |
### Incremental sync
`salvium_wallet_sync` is always incremental. It starts from the last persisted height and only fetches new blocks. Calling it again after completion returns immediately if no new blocks exist.
### Rescan
```c
salvium_wallet_reset_sync_height(wallet, 0); // reset to genesis
salvium_wallet_sync(wallet, daemon, callback); // full rescan
```
## 5. Query Balance
```c
// Single asset
char* json = salvium_wallet_get_balance(wallet, "SAL", 0);
// Returns: {"balance":"123456789","unlocked_balance":"100000000","locked_balance":"23456789"}
// Amounts are atomic units (1 SAL = 100,000,000 atomic)
// CALLER MUST FREE:
salvium_string_free(json);
// All assets
char* all = salvium_wallet_get_all_balances(wallet, 0);
// Returns: {"SAL":{"balance":"...","unlocked_balance":"...","locked_balance":"..."}, ...}
salvium_string_free(all);
```
**You MUST sync before querying balances.** Without sync, the balance will be 0.
## 6. Query Transactions
```c
// All confirmed transfers
char* txs = salvium_wallet_get_transfers(wallet, "{\"is_confirmed\":true}");
// Only incoming
char* txs = salvium_wallet_get_transfers(wallet, "{\"is_incoming\":true}");
// Height range
char* txs = salvium_wallet_get_transfers(wallet, "{\"min_height\":100000,\"max_height\":200000}");
// Specific tx hash
char* txs = salvium_wallet_get_transfers(wallet, "{\"tx_hash\":\"abc123...\"}");
// Returns JSON array. ALWAYS free:
salvium_string_free(txs);
```
**TxQuery fields (all optional):**
| Field | Type | Description |
|-------|------|-------------|
| `is_incoming` | bool | Filter incoming transfers |
| `is_outgoing` | bool | Filter outgoing transfers |
| `is_confirmed` | bool | Filter confirmed (in-block) |
| `in_pool` | bool | Filter mempool transactions |
| `tx_type` | i64 | 1=miner, 2=protocol, 3=transfer |
| `min_height` | i64 | Minimum block height |
| `max_height` | i64 | Maximum block height |
| `tx_hash` | string | Exact transaction hash |
**TransactionRow fields in response:**
| Field | Type | Description |
|-------|------|-------------|
| `tx_hash` | string | Transaction hash (hex) |
| `block_height` | i64/null | Block height (null if in pool) |
| `block_timestamp` | i64 | Unix timestamp |
| `is_incoming` | bool | Received funds |
| `is_outgoing` | bool | Sent funds |
| `is_confirmed` | bool | In a block |
| `incoming_amount` | string | Atomic units received |
| `outgoing_amount` | string | Atomic units sent |
| `fee` | string | Transaction fee (atomic) |
| `asset_type` | string | e.g. "SAL" |
| `tx_type` | i64 | Transaction type code |
## 7. Query Outputs (UTXOs)
```c
// All unspent outputs
char* outs = salvium_wallet_get_outputs(wallet, "{\"is_spent\":false}");
// Unspent SAL only
char* outs = salvium_wallet_get_outputs(wallet, "{\"is_spent\":false,\"asset_type\":\"SAL\"}");
salvium_string_free(outs);
```
## 8. Other Wallet Queries
```c
// Addresses
char* cn_addr = salvium_wallet_get_address(wallet, 0); // CryptoNote
char* carr_addr = salvium_wallet_get_address(wallet, 1); // CARROT
salvium_string_free(cn_addr);
salvium_string_free(carr_addr);
// Mnemonic (null if view-only)
char* words = salvium_wallet_get_mnemonic(wallet);
if (words) salvium_string_free(words);
// Key material as JSON
char* keys = salvium_wallet_get_keys_json(wallet);
salvium_string_free(keys);
// Can this wallet spend?
int can = salvium_wallet_can_spend(wallet); // 1=yes, 0=no
// Current sync height
uint64_t h = salvium_wallet_sync_height(wallet);
// Network
int net = salvium_wallet_network(wallet); // 0=main, 1=test, 2=stage
```
## 9. Staking
```c
char* stakes = salvium_wallet_get_stakes(wallet, "locked"); // or "returned" or NULL for all
salvium_string_free(stakes);
```
## 10. Cleanup
**Close in reverse order.** Always close handles when done.
```c
salvium_wallet_close(wallet); // wallet first
salvium_daemon_close(daemon); // daemon second
```
## Error Handling
Every FFI function follows one of these patterns:
| Return type | Success | Error |
|-------------|---------|-------|
| `i32` | `0` | `-1` |
| `*mut c_char` | non-null string | `NULL` |
| `*mut c_void` | non-null handle | `NULL` |
| `u64` | value | `u64::MAX` |
On error, call `salvium_last_error()` to get the error message:
```c
const char* err = salvium_last_error();
// This pointer is valid until the NEXT FFI call on the same thread.
// DO NOT free it. Copy it if you need to keep it.
```
## Memory Rules
1. **Strings** returned by FFI functions (`*mut c_char`) MUST be freed with `salvium_string_free()`.
2. **Handles** (`*mut c_void`) MUST be closed with the matching `_close()` function.
3. **Error strings** from `salvium_last_error()` must NOT be freed — they are owned by the library.
4. **Input strings** (parameters you pass in) must be valid null-terminated UTF-8.
## Complete Example (Pseudocode)
```c
salvium_ffi_init();
// Connect
void* daemon = salvium_daemon_connect("http://seed01.salvium.io:19081");
assert(daemon != NULL);
// Wait for daemon to sync
while (salvium_daemon_is_synchronized(daemon) != 1) {
sleep(5);
}
// Create wallet
uint8_t db_key[32];
generate_random_bytes(db_key, 32);
void* wallet = salvium_wallet_from_mnemonic(mnemonic, 0, "wallet.db", db_key, 32);
assert(wallet != NULL);
// Sync blockchain (blocks until complete)
int rc = salvium_wallet_sync(wallet, daemon, progress_callback);
assert(rc == 0);
// Read balance
char* bal = salvium_wallet_get_balance(wallet, "SAL", 0);
printf("Balance: %s\n", bal);
salvium_string_free(bal);
// Read transactions
char* txs = salvium_wallet_get_transfers(wallet, "{\"is_confirmed\":true}");
printf("Transfers: %s\n", txs);
salvium_string_free(txs);
// Cleanup
salvium_wallet_close(wallet);
salvium_daemon_close(daemon);
```
## Source Files
| File | What |
|------|------|
| `crates/salvium-ffi/src/lib.rs` | FFI entry point, runtime singleton |
| `crates/salvium-ffi/src/wallet.rs` | Wallet lifecycle, sync, queries |
| `crates/salvium-ffi/src/daemon.rs` | Daemon RPC handle |
| `crates/salvium-ffi/src/error.rs` | Error storage, `ffi_try` helpers |
| `crates/salvium-ffi/src/strings.rs` | String marshalling, `salvium_string_free` |
| `crates/salvium-wallet/src/wallet.rs` | Rust wallet implementation |
| `crates/salvium-wallet/src/sync.rs` | Sync engine internals |
| `crates/salvium-rpc/src/daemon.rs` | RPC client |
| `crates/salvium-sync-bench/src/main.rs` | Working Rust sync example |
+10
View File
@@ -36,6 +36,16 @@ wasm-pack build \
# Remove wasm-pack's generated .gitignore (we want to commit/ship this)
rm -f "$OUT_DIR/.gitignore"
# Build WASM static library (C ABI, no wasm-bindgen glue).
# Produces libsalvium_crypto.a for direct linking (e.g. Cloudflare Worker).
echo ""
echo "==> Building WASM static library (C ABI)..."
cargo build -p salvium-crypto \
--target wasm32-unknown-unknown \
--release \
--no-default-features
cp "$ROOT_DIR/target/wasm32-unknown-unknown/release/libsalvium_crypto.a" "$OUT_DIR/"
echo ""
echo "==> Done:"
ls -lh "$OUT_DIR/"*