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:
@@ -203,11 +203,15 @@ pub async fn sync_wallet(ctx: &AppContext) -> Result {
|
|||||||
error
|
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);
|
drop(tx);
|
||||||
let _ = progress_task.await;
|
let _ = progress_task.await;
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub mod storage;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
mod ffi;
|
mod ffi;
|
||||||
|
|
||||||
|
mod wasm_ffi;
|
||||||
|
|
||||||
/// Keccak-256 hash (CryptoNote variant with 0x01 padding, NOT SHA3)
|
/// Keccak-256 hash (CryptoNote variant with 0x01 padding, NOT SHA3)
|
||||||
/// Matches Salvium C++ cn_fast_hash / keccak()
|
/// Matches Salvium C++ cn_fast_hash / keccak()
|
||||||
#[cfg_attr(feature = "wasm-exports", wasm_bindgen)]
|
#[cfg_attr(feature = "wasm-exports", wasm_bindgen)]
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Wallet lifecycle, key/address queries, balance, sync, and data operations.
|
//! Wallet lifecycle, key/address queries, balance, sync, and data operations.
|
||||||
|
|
||||||
use std::ffi::{c_char, c_void};
|
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::error::{ffi_try, ffi_try_ptr, ffi_try_string};
|
||||||
use crate::handles::{borrow_handle, borrow_handle_mut, drop_handle};
|
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;
|
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
|
// Wallet Lifecycle
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -36,7 +53,9 @@ pub unsafe extern "C" fn salvium_wallet_create(
|
|||||||
let network = int_to_network(network)?;
|
let network = int_to_network(network)?;
|
||||||
let path = unsafe { c_str_to_str(db_path) }?;
|
let path = unsafe { c_str_to_str(db_path) }?;
|
||||||
let key = unsafe { crate::strings::c_buf_to_slice(db_key, db_key_len) }?;
|
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 network = int_to_network(network)?;
|
||||||
let path = unsafe { c_str_to_str(db_path) }?;
|
let path = unsafe { c_str_to_str(db_path) }?;
|
||||||
let key = unsafe { crate::strings::c_buf_to_slice(db_key, db_key_len) }?;
|
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 key = unsafe { crate::strings::c_buf_to_slice(db_key, db_key_len) }?;
|
||||||
|
|
||||||
let keys = wallet_keys_from_json(json_str)?;
|
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.
|
/// Close a wallet handle, releasing all resources.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_close(handle: *mut c_void) {
|
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,
|
addr_type: i32,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
ffi_try_string(|| {
|
||||||
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
|
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
|
||||||
match addr_type {
|
match addr_type {
|
||||||
0 => wallet.cn_address().map_err(|e| e.to_string()),
|
0 => wallet.cn_address().map_err(|e| e.to_string()),
|
||||||
1 => wallet.carrot_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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_get_mnemonic(handle: *mut c_void) -> *mut c_char {
|
pub unsafe extern "C" fn salvium_wallet_get_mnemonic(handle: *mut c_void) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
ffi_try_string(|| {
|
||||||
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
|
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
|
||||||
match wallet.mnemonic() {
|
match wallet.mnemonic() {
|
||||||
Some(Ok(words)) => Ok(words),
|
Some(Ok(words)) => Ok(words),
|
||||||
Some(Err(e)) => Err(e.to_string()),
|
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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_get_keys_json(handle: *mut c_void) -> *mut c_char {
|
pub unsafe extern "C" fn salvium_wallet_get_keys_json(handle: *mut c_void) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
ffi_try_string(|| {
|
||||||
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
|
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
|
||||||
let keys = wallet.keys();
|
let keys = wallet.keys();
|
||||||
let json = serde_json::json!({
|
let json = serde_json::json!({
|
||||||
"wallet_type": format!("{:?}", keys.wallet_type),
|
"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]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_can_spend(handle: *mut c_void) -> i32 {
|
pub unsafe extern "C" fn salvium_wallet_can_spend(handle: *mut c_void) -> i32 {
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
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())
|
Some(wallet.can_spend())
|
||||||
}));
|
}));
|
||||||
match result {
|
match result {
|
||||||
@@ -175,7 +200,9 @@ pub unsafe extern "C" fn salvium_wallet_can_spend(handle: *mut c_void) -> i32 {
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_network(handle: *mut c_void) -> i32 {
|
pub unsafe extern "C" fn salvium_wallet_network(handle: *mut c_void) -> i32 {
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
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())
|
Some(wallet.network())
|
||||||
}));
|
}));
|
||||||
match result {
|
match result {
|
||||||
@@ -192,7 +219,9 @@ pub unsafe extern "C" fn salvium_wallet_network(handle: *mut c_void) -> i32 {
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_sync_height(handle: *mut c_void) -> u64 {
|
pub unsafe extern "C" fn salvium_wallet_sync_height(handle: *mut c_void) -> u64 {
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
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()
|
wallet.sync_height().ok()
|
||||||
}));
|
}));
|
||||||
match result {
|
match result {
|
||||||
@@ -216,7 +245,7 @@ pub unsafe extern "C" fn salvium_wallet_get_balance(
|
|||||||
account_index: i32,
|
account_index: i32,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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 asset = unsafe { c_str_to_str(asset_type) }?;
|
||||||
let result = wallet
|
let result = wallet
|
||||||
.get_balance(asset, account_index)
|
.get_balance(asset, account_index)
|
||||||
@@ -235,7 +264,7 @@ pub unsafe extern "C" fn salvium_wallet_get_all_balances(
|
|||||||
account_index: i32,
|
account_index: i32,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
ffi_try_string(|| {
|
||||||
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
|
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
|
||||||
let result = wallet
|
let result = wallet
|
||||||
.get_all_balances(account_index)
|
.get_all_balances(account_index)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
@@ -255,7 +284,7 @@ pub unsafe extern "C" fn salvium_wallet_set_attribute(
|
|||||||
value: *const c_char,
|
value: *const c_char,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
ffi_try(|| {
|
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 k = unsafe { c_str_to_str(key) }?;
|
||||||
let v = unsafe { c_str_to_str(value) }?;
|
let v = unsafe { c_str_to_str(value) }?;
|
||||||
wallet.set_attribute(k, v).map_err(|e| e.to_string())
|
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,
|
key: *const c_char,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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) }?;
|
let k = unsafe { c_str_to_str(key) }?;
|
||||||
match wallet.get_attribute(k).map_err(|e| e.to_string())? {
|
match wallet.get_attribute(k).map_err(|e| e.to_string())? {
|
||||||
Some(v) => Ok(v),
|
Some(v) => Ok(v),
|
||||||
@@ -285,7 +314,7 @@ pub unsafe extern "C" fn salvium_wallet_get_attribute(
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_reset_sync_height(handle: *mut c_void, height: u64) -> i32 {
|
pub unsafe extern "C" fn salvium_wallet_reset_sync_height(handle: *mut c_void, height: u64) -> i32 {
|
||||||
ffi_try(|| {
|
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())
|
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.
|
/// 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
|
/// - `current_height`: current scan height
|
||||||
/// - `target_height`: target chain height
|
/// - `target_height`: target chain height
|
||||||
/// - `outputs_found`: number of outputs found so far
|
/// - `outputs_found`: number of outputs found so far
|
||||||
@@ -324,10 +353,13 @@ pub unsafe extern "C" fn salvium_wallet_sync(
|
|||||||
callback: Option<SyncCallbackFn>,
|
callback: Option<SyncCallbackFn>,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
ffi_try(|| {
|
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 daemon_ref = unsafe { borrow_handle::<salvium_rpc::DaemonRpc>(daemon) }?;
|
||||||
let rt = crate::runtime();
|
let rt = crate::runtime();
|
||||||
|
|
||||||
|
// Reset cancel flag before starting.
|
||||||
|
handle.sync_cancel.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
if let Some(cb) = callback {
|
if let Some(cb) = callback {
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<salvium_wallet::SyncEvent>(256);
|
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.
|
drop(tx); // Close channel so forwarder exits.
|
||||||
let _ = forwarder.await;
|
let _ = forwarder.await;
|
||||||
|
|
||||||
result.map(|_| ()).map_err(|e| e.to_string())
|
result.map(|_| ()).map_err(|e| e.to_string())
|
||||||
} else {
|
} else {
|
||||||
wallet_ref
|
handle
|
||||||
.sync(daemon_ref, None)
|
.wallet
|
||||||
|
.sync(daemon_ref, None, &handle.sync_cancel)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
.map_err(|e| e.to_string())
|
.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.
|
/// Dispatch a SyncEvent to the C callback.
|
||||||
fn dispatch_sync_event(event: &salvium_wallet::SyncEvent, cb: SyncCallbackFn) {
|
fn dispatch_sync_event(event: &salvium_wallet::SyncEvent, cb: SyncCallbackFn) {
|
||||||
use salvium_wallet::SyncEvent;
|
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,
|
filters_json: *const c_char,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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 json_str = unsafe { c_str_to_str(filters_json) }?;
|
||||||
let query: salvium_wallet::TxQuery =
|
let query: salvium_wallet::TxQuery =
|
||||||
serde_json::from_str(json_str).map_err(|e| format!("invalid query JSON: {e}"))?;
|
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,
|
query_json: *const c_char,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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 json_str = unsafe { c_str_to_str(query_json) }?;
|
||||||
let query: salvium_wallet::OutputQuery =
|
let query: salvium_wallet::OutputQuery =
|
||||||
serde_json::from_str(json_str).map_err(|e| format!("invalid query JSON: {e}"))?;
|
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,
|
status: *const c_char,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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() {
|
let status_opt = if status.is_null() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -497,7 +552,7 @@ pub unsafe extern "C" fn salvium_wallet_get_stakes(
|
|||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn salvium_wallet_address_book_list(handle: *mut c_void) -> *mut c_char {
|
pub unsafe extern "C" fn salvium_wallet_address_book_list(handle: *mut c_void) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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())?;
|
let entries = wallet.get_address_book().map_err(|e| e.to_string())?;
|
||||||
serde_json::to_string(&entries).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,
|
description: *const c_char,
|
||||||
) -> i64 {
|
) -> i64 {
|
||||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
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 addr = unsafe { c_str_to_str(address) }?;
|
||||||
let lbl = unsafe { c_str_to_str(label) }?;
|
let lbl = unsafe { c_str_to_str(label) }?;
|
||||||
let desc = unsafe { c_str_to_str(description) }?;
|
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,
|
row_id: i64,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
ffi_try(|| {
|
ffi_try(|| {
|
||||||
let wallet = unsafe { borrow_handle::<Wallet>(handle) }?;
|
let wallet = &unsafe { borrow_handle::<WalletHandle>(handle) }?.wallet;
|
||||||
wallet
|
wallet
|
||||||
.delete_address_book_entry(row_id)
|
.delete_address_book_entry(row_id)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
@@ -564,7 +619,7 @@ pub unsafe extern "C" fn salvium_wallet_set_tx_note(
|
|||||||
note: *const c_char,
|
note: *const c_char,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
ffi_try(|| {
|
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 hash = unsafe { c_str_to_str(tx_hash) }?;
|
||||||
let n = unsafe { c_str_to_str(note) }?;
|
let n = unsafe { c_str_to_str(note) }?;
|
||||||
wallet.set_tx_note(hash, n).map_err(|e| e.to_string())
|
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,
|
tx_hashes_json: *const c_char,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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 json_str = unsafe { c_str_to_str(tx_hashes_json) }?;
|
||||||
let hashes: Vec<String> =
|
let hashes: Vec<String> =
|
||||||
serde_json::from_str(json_str).map_err(|e| format!("invalid JSON array: {e}"))?;
|
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,
|
key_image: *const c_char,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
ffi_try(|| {
|
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) }?;
|
let ki = unsafe { c_str_to_str(key_image) }?;
|
||||||
wallet.freeze_output(ki).map_err(|e| e.to_string())
|
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,
|
key_image: *const c_char,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
ffi_try(|| {
|
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) }?;
|
let ki = unsafe { c_str_to_str(key_image) }?;
|
||||||
wallet.thaw_output(ki).map_err(|e| e.to_string())
|
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,
|
pin: *const c_char,
|
||||||
) -> *mut c_char {
|
) -> *mut c_char {
|
||||||
ffi_try_string(|| {
|
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 pin_str = unsafe { c_str_to_str(pin) }?;
|
||||||
|
|
||||||
let keys = wallet.keys();
|
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());
|
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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -274,12 +274,16 @@ async fn wallet_sync_and_balance_check() {
|
|||||||
height, blob_len, error
|
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
|
let sync_height = wallet
|
||||||
.sync(&daemon, Some(&event_tx))
|
.sync(&daemon, Some(&event_tx), &no_cancel)
|
||||||
.await
|
.await
|
||||||
.expect("wallet sync failed");
|
.expect("wallet sync failed");
|
||||||
|
|
||||||
@@ -442,7 +446,7 @@ async fn wallet_sync_and_balance_check() {
|
|||||||
// ── Sync idempotency check ──────────────────────────────────────────
|
// ── Sync idempotency check ──────────────────────────────────────────
|
||||||
println!("\n Verifying sync idempotency...");
|
println!("\n Verifying sync idempotency...");
|
||||||
let height2 = wallet
|
let height2 = wallet
|
||||||
.sync(&daemon, None)
|
.sync(&daemon, None, &no_cancel)
|
||||||
.await
|
.await
|
||||||
.expect("second sync failed");
|
.expect("second sync failed");
|
||||||
let all_balances_2 = wallet.get_all_balances(0).unwrap();
|
let all_balances_2 = wallet.get_all_balances(0).unwrap();
|
||||||
|
|||||||
@@ -317,13 +317,17 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
SyncEvent::Started { .. } => {}
|
SyncEvent::Started { .. } => {}
|
||||||
|
SyncEvent::Cancelled { height } => {
|
||||||
|
println!(" ** Sync cancelled at height {} **", height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(final_parse_errors, final_empty_blobs)
|
(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
|
drop(tx); // close channel so progress task finishes
|
||||||
let (final_parse_errors, final_empty_blobs) = progress_handle.await.unwrap_or((0, 0));
|
let (final_parse_errors, final_empty_blobs) = progress_handle.await.unwrap_or((0, 0));
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ pub enum WalletError {
|
|||||||
#[error("hardware device error: {0}")]
|
#[error("hardware device error: {0}")]
|
||||||
Device(String),
|
Device(String),
|
||||||
|
|
||||||
|
#[error("sync cancelled by caller")]
|
||||||
|
Cancelled,
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ pub enum SyncEvent {
|
|||||||
blob_len: usize,
|
blob_len: usize,
|
||||||
error: String,
|
error: String,
|
||||||
},
|
},
|
||||||
|
/// Sync cancelled by caller.
|
||||||
|
Cancelled { height: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blockchain sync engine.
|
/// Blockchain sync engine.
|
||||||
@@ -119,6 +121,7 @@ impl SyncEngine {
|
|||||||
scan_ctx: &mut ScanContext,
|
scan_ctx: &mut ScanContext,
|
||||||
stake_lock_period: u64,
|
stake_lock_period: u64,
|
||||||
event_tx: Option<&tokio::sync::mpsc::Sender<SyncEvent>>,
|
event_tx: Option<&tokio::sync::mpsc::Sender<SyncEvent>>,
|
||||||
|
cancel: &std::sync::atomic::AtomicBool,
|
||||||
) -> Result<u64, WalletError> {
|
) -> Result<u64, WalletError> {
|
||||||
let daemon_height = daemon
|
let daemon_height = daemon
|
||||||
.get_height()
|
.get_height()
|
||||||
@@ -167,6 +170,13 @@ impl SyncEngine {
|
|||||||
let mut controller = BatchController::new();
|
let mut controller = BatchController::new();
|
||||||
|
|
||||||
while current < top_block {
|
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 remaining = top_block - current;
|
||||||
let batch_size = controller.next_batch_size(remaining);
|
let batch_size = controller.next_batch_size(remaining);
|
||||||
let batch_start = current + 1;
|
let batch_start = current + 1;
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ impl Wallet {
|
|||||||
&mut self,
|
&mut self,
|
||||||
daemon: &salvium_rpc::DaemonRpc,
|
daemon: &salvium_rpc::DaemonRpc,
|
||||||
event_tx: Option<&tokio::sync::mpsc::Sender<SyncEvent>>,
|
event_tx: Option<&tokio::sync::mpsc::Sender<SyncEvent>>,
|
||||||
|
cancel: &std::sync::atomic::AtomicBool,
|
||||||
) -> Result<u64, WalletError> {
|
) -> Result<u64, WalletError> {
|
||||||
let lock_period =
|
let lock_period =
|
||||||
salvium_types::constants::network_config(self.network()).stake_lock_period;
|
salvium_types::constants::network_config(self.network()).stake_lock_period;
|
||||||
@@ -234,6 +235,7 @@ impl Wallet {
|
|||||||
&mut self.scan_context,
|
&mut self.scan_context,
|
||||||
lock_period,
|
lock_period,
|
||||||
event_tx,
|
event_tx,
|
||||||
|
cancel,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1145,8 +1145,9 @@ async fn sync_wallet_checked(wallet: &mut Wallet, daemon: &DaemonRpc, label: &st
|
|||||||
});
|
});
|
||||||
|
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
|
let no_cancel = std::sync::atomic::AtomicBool::new(false);
|
||||||
let sync_height = wallet
|
let sync_height = wallet
|
||||||
.sync(daemon, Some(&event_tx))
|
.sync(daemon, Some(&event_tx), &no_cancel)
|
||||||
.await
|
.await
|
||||||
.expect("sync failed");
|
.expect("sync failed");
|
||||||
drop(event_tx);
|
drop(event_tx);
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ async fn test_burn_transaction_build() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
let hf_info = d.hard_fork_info().await.unwrap();
|
let hf_info = d.hard_fork_info().await.unwrap();
|
||||||
@@ -138,7 +141,10 @@ async fn test_burn_submit_testnet() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
let hf_info = d.hard_fork_info().await.unwrap();
|
let hf_info = d.hard_fork_info().await.unwrap();
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ async fn test_convert_transaction_build() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
let hf_info = d.hard_fork_info().await.unwrap();
|
let hf_info = d.hard_fork_info().await.unwrap();
|
||||||
@@ -136,7 +139,10 @@ async fn test_convert_expected_rejection() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
let hf_info = d.hard_fork_info().await.unwrap();
|
let hf_info = d.hard_fork_info().await.unwrap();
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ async fn test_stake_transaction_build() {
|
|||||||
|
|
||||||
// Sync
|
// Sync
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
// Get hardfork info for asset type
|
// Get hardfork info for asset type
|
||||||
@@ -190,7 +193,10 @@ async fn test_stake_submit_testnet() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
let hf_info = d.hard_fork_info().await.unwrap();
|
let hf_info = d.hard_fork_info().await.unwrap();
|
||||||
@@ -467,7 +473,10 @@ async fn test_stake_return_detection() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
// Look for protocol TX outputs (stake returns).
|
// Look for protocol TX outputs (stake returns).
|
||||||
|
|||||||
@@ -336,7 +336,10 @@ async fn test_self_transfer_to_subaddress() {
|
|||||||
.expect("create wallet");
|
.expect("create wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("Synced to height {}", sync_height);
|
||||||
|
|
||||||
let hf_info = d.hard_fork_info().await.unwrap();
|
let hf_info = d.hard_fork_info().await.unwrap();
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ async fn test_full_sync_balance() {
|
|||||||
println!("Daemon height: {}", info.height);
|
println!("Daemon height: {}", info.height);
|
||||||
println!("Network: testnet");
|
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);
|
println!("Synced to height: {}", sync_height);
|
||||||
|
|
||||||
// Sync height should be close to daemon height
|
// Sync height should be close to daemon height
|
||||||
@@ -140,7 +143,10 @@ async fn test_view_only_sync() {
|
|||||||
.expect("open view-only wallet");
|
.expect("open view-only wallet");
|
||||||
|
|
||||||
let d = daemon();
|
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);
|
println!("View-only synced to height: {}", sync_height);
|
||||||
|
|
||||||
// View-only wallet should detect CN outputs
|
// View-only wallet should detect CN outputs
|
||||||
@@ -204,8 +210,9 @@ async fn test_carrot_view_only_sync() {
|
|||||||
.expect("open CARROT view-only wallet");
|
.expect("open CARROT view-only wallet");
|
||||||
|
|
||||||
let d = daemon();
|
let d = daemon();
|
||||||
|
let no_cancel = std::sync::atomic::AtomicBool::new(false);
|
||||||
let sync_height = wallet
|
let sync_height = wallet
|
||||||
.sync(&d, None)
|
.sync(&d, None, &no_cancel)
|
||||||
.await
|
.await
|
||||||
.expect("CARROT view-only sync failed");
|
.expect("CARROT view-only sync failed");
|
||||||
println!("CARROT view-only synced to height: {}", sync_height);
|
println!("CARROT view-only synced to height: {}", sync_height);
|
||||||
@@ -259,7 +266,10 @@ async fn test_sync_idempotent() {
|
|||||||
let d = daemon();
|
let d = daemon();
|
||||||
|
|
||||||
// First sync
|
// 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 bal_1 = wallet.get_balance("SAL", 0).unwrap();
|
||||||
let total_1: u64 = bal_1.balance.parse().unwrap_or(0);
|
let total_1: u64 = bal_1.balance.parse().unwrap_or(0);
|
||||||
let unlocked_1: u64 = bal_1.unlocked_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)
|
// 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 bal_2 = wallet.get_balance("SAL", 0).unwrap();
|
||||||
let total_2: u64 = bal_2.balance.parse().unwrap_or(0);
|
let total_2: u64 = bal_2.balance.parse().unwrap_or(0);
|
||||||
let unlocked_2: u64 = bal_2.unlocked_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
|
let sync_height = wallet
|
||||||
.sync(&daemon, Some(&event_tx))
|
.sync(&daemon, Some(&event_tx), &no_cancel)
|
||||||
.await
|
.await
|
||||||
.expect("sync failed");
|
.expect("sync failed");
|
||||||
drop(event_tx);
|
drop(event_tx);
|
||||||
|
|||||||
@@ -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) |
|
||||||
@@ -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 |
|
||||||
@@ -36,6 +36,16 @@ wasm-pack build \
|
|||||||
# Remove wasm-pack's generated .gitignore (we want to commit/ship this)
|
# Remove wasm-pack's generated .gitignore (we want to commit/ship this)
|
||||||
rm -f "$OUT_DIR/.gitignore"
|
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 ""
|
||||||
echo "==> Done:"
|
echo "==> Done:"
|
||||||
ls -lh "$OUT_DIR/"*
|
ls -lh "$OUT_DIR/"*
|
||||||
|
|||||||
Reference in New Issue
Block a user