use std::time::Duration;
use blockstore::EitherBlockstore;
use celestia_types::blob::BlobsAtHeight;
use celestia_types::nmt::Namespace;
use celestia_types::{Blob, ExtendedHeader, SharesAtHeight};
use js_sys::{Array, AsyncIterator};
use libp2p::Multiaddr;
use libp2p::identity::Keypair;
use lumina_node::blockstore::{InMemoryBlockstore, IndexedDbBlockstore};
use lumina_node::network;
use lumina_node::node::{DEFAULT_PRUNING_WINDOW_IN_MEMORY, NodeBuilder};
use lumina_node::store::{EitherStore, InMemoryStore, IndexedDbStore, SamplingMetadata};
use serde::{Deserialize, Serialize};
use tracing::{debug, error};
use wasm_bindgen::prelude::*;
use web_sys::BroadcastChannel;
use crate::commands::{
NodeCommand, SingleHeaderQuery, SubscriptionCommand, WorkerCommand, WorkerError, WorkerResponse,
};
use crate::error::{Context, Result};
use crate::subscriptions::into_async_iterator;
use crate::utils::{
Network, is_safari, js_value_from_display, request_storage_persistence, timeout,
};
use crate::worker::{WasmBlockstore, WasmStore};
use crate::worker_client::WorkerClient;
use crate::wrapper::libp2p::NetworkInfoSnapshot;
use crate::wrapper::node::{PeerTrackerInfoSnapshot, SyncingInfoSnapshot};
#[wasm_bindgen(inspectable, js_name = NodeConfig)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmNodeConfig {
pub network: Network,
#[wasm_bindgen(getter_with_clone)]
pub bootnodes: Vec<String>,
#[wasm_bindgen(getter_with_clone)]
pub identity_key: Option<Vec<u8>>,
#[wasm_bindgen(js_name = usePersistentMemory)]
pub use_persistent_memory: bool,
#[wasm_bindgen(js_name = customPruningWindowSecs)]
pub custom_pruning_window_secs: Option<u32>,
}
#[wasm_bindgen]
pub struct NodeClient {
worker: WorkerClient,
}
#[wasm_bindgen]
impl NodeClient {
#[wasm_bindgen(constructor)]
#[allow(deprecated)] pub async fn new(port: JsValue) -> Result<NodeClient> {
if !is_safari()?
&& let Err(e) = request_storage_persistence().await
{
error!("Error requesting storage persistence: {e}");
}
let worker = WorkerClient::new(port)?;
loop {
if timeout(100, worker.worker_exec(WorkerCommand::InternalPing))
.await
.is_ok()
{
break;
}
}
debug!("Connected to worker");
Ok(Self { worker })
}
#[wasm_bindgen(js_name = addConnectionToWorker)]
pub async fn add_connection_to_worker(&self, port: JsValue) -> Result<()> {
self.worker
.worker_exec(WorkerCommand::ConnectPort(Some(port.into())))
.await?;
Ok(())
}
#[wasm_bindgen(js_name = isRunning)]
pub async fn is_running(&self) -> Result<bool> {
let command = WorkerCommand::IsRunning;
let response = self.worker.worker_exec(command).await?;
Ok(response
.into_is_running()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
pub async fn start(&self, config: &WasmNodeConfig) -> Result<()> {
let command = WorkerCommand::StartNode(config.clone());
let response = self.worker.worker_exec(command).await?;
debug_assert!(matches!(response, WorkerResponse::Ok));
Ok(())
}
pub async fn stop(&self) -> Result<()> {
let command = WorkerCommand::StopNode;
let response = self.worker.worker_exec(command).await?;
debug_assert!(matches!(response, WorkerResponse::Ok));
Ok(())
}
#[wasm_bindgen(js_name = localPeerId)]
pub async fn local_peer_id(&self) -> Result<String> {
let command = NodeCommand::GetLocalPeerId;
let response = self.worker.node_exec(command).await?;
let peer_id = response
.into_local_peer_id()
.map_err(|_| WorkerError::InvalidResponseType)?;
Ok(peer_id)
}
#[wasm_bindgen(js_name = peerTrackerInfo)]
pub async fn peer_tracker_info(&self) -> Result<PeerTrackerInfoSnapshot> {
let command = NodeCommand::GetPeerTrackerInfo;
let response = self.worker.node_exec(command).await?;
let peer_info = response
.into_peer_tracker_info()
.map_err(|_| WorkerError::InvalidResponseType)?;
Ok(peer_info.into())
}
#[wasm_bindgen(js_name = waitConnected)]
pub async fn wait_connected(&self) -> Result<()> {
let command = NodeCommand::WaitConnected { trusted: false };
let response = self.worker.node_exec(command).await?;
debug_assert!(matches!(response, WorkerResponse::Ok));
Ok(())
}
#[wasm_bindgen(js_name = waitConnectedTrusted)]
pub async fn wait_connected_trusted(&self) -> Result<()> {
let command = NodeCommand::WaitConnected { trusted: true };
let response = self.worker.node_exec(command).await?;
debug_assert!(matches!(response, WorkerResponse::Ok));
Ok(())
}
#[wasm_bindgen(js_name = networkInfo)]
pub async fn network_info(&self) -> Result<NetworkInfoSnapshot> {
let command = NodeCommand::GetNetworkInfo;
let response = self.worker.node_exec(command).await?;
Ok(response
.into_network_info()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
pub async fn listeners(&self) -> Result<Array> {
let command = NodeCommand::GetListeners;
let response = self.worker.node_exec(command).await?;
let listeners = response
.into_listeners()
.map_err(|_| WorkerError::InvalidResponseType)?;
let result = listeners.iter().map(js_value_from_display).collect();
Ok(result)
}
#[wasm_bindgen(js_name = connectedPeers)]
pub async fn connected_peers(&self) -> Result<Array> {
let command = NodeCommand::GetConnectedPeers;
let response = self.worker.node_exec(command).await?;
let peers = response
.into_connected_peers()
.map_err(|_| WorkerError::InvalidResponseType)?;
let result = peers.iter().map(js_value_from_display).collect();
Ok(result)
}
#[wasm_bindgen(js_name = setPeerTrust)]
pub async fn set_peer_trust(&self, peer_id: &str, is_trusted: bool) -> Result<()> {
let command = NodeCommand::SetPeerTrust {
peer_id: peer_id.parse()?,
is_trusted,
};
let response = self.worker.node_exec(command).await?;
debug_assert!(matches!(response, WorkerResponse::Ok));
Ok(())
}
#[wasm_bindgen(js_name = requestHeadHeader)]
pub async fn request_head_header(&self) -> Result<ExtendedHeader> {
let command = NodeCommand::RequestHeader(SingleHeaderQuery::Head);
let response = self.worker.node_exec(command).await?;
Ok(response
.into_header()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = requestHeaderByHash)]
pub async fn request_header_by_hash(&self, hash: &str) -> Result<ExtendedHeader> {
let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHash(hash.parse()?));
let response = self.worker.node_exec(command).await?;
Ok(response
.into_header()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = requestHeaderByHeight)]
pub async fn request_header_by_height(&self, height: u64) -> Result<ExtendedHeader> {
let command = NodeCommand::RequestHeader(SingleHeaderQuery::ByHeight(height));
let response = self.worker.node_exec(command).await?;
Ok(response
.into_header()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = requestVerifiedHeaders)]
pub async fn request_verified_headers(
&self,
from: &ExtendedHeader,
amount: u64,
) -> Result<Vec<ExtendedHeader>> {
let command = NodeCommand::GetVerifiedHeaders {
from: from.clone(),
amount,
};
let response = self.worker.node_exec(command).await?;
Ok(response
.into_headers()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = requestAllBlobs)]
pub async fn request_all_blobs(
&self,
namespace: &Namespace,
block_height: u64,
timeout_secs: Option<f64>,
) -> Result<Vec<Blob>> {
let command = NodeCommand::RequestAllBlobs {
namespace: *namespace,
block_height,
timeout_secs,
};
let response = self.worker.node_exec(command).await?;
Ok(response
.into_blobs()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = syncerInfo)]
pub async fn syncer_info(&self) -> Result<SyncingInfoSnapshot> {
let command = NodeCommand::GetSyncerInfo;
let response = self.worker.node_exec(command).await?;
let syncer_info = response
.into_syncer_info()
.map_err(|_| WorkerError::InvalidResponseType)?;
Ok(syncer_info.into())
}
#[wasm_bindgen(js_name = getNetworkHeadHeader)]
pub async fn get_network_head_header(&self) -> Result<Option<ExtendedHeader>> {
let command = NodeCommand::LastSeenNetworkHead;
let response = self.worker.node_exec(command).await?;
Ok(response
.into_last_seen_network_head()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = getLocalHeadHeader)]
pub async fn get_local_head_header(&self) -> Result<ExtendedHeader> {
let command = NodeCommand::GetHeader(SingleHeaderQuery::Head);
let response = self.worker.node_exec(command).await?;
Ok(response
.into_header()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = getHeaderByHash)]
pub async fn get_header_by_hash(&self, hash: &str) -> Result<ExtendedHeader> {
let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHash(hash.parse()?));
let response = self.worker.node_exec(command).await?;
Ok(response
.into_header()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = getHeaderByHeight)]
pub async fn get_header_by_height(&self, height: u64) -> Result<ExtendedHeader> {
let command = NodeCommand::GetHeader(SingleHeaderQuery::ByHeight(height));
let response = self.worker.node_exec(command).await?;
Ok(response
.into_header()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = getHeaders)]
pub async fn get_headers(
&self,
start_height: Option<u64>,
end_height: Option<u64>,
) -> Result<Vec<ExtendedHeader>> {
let command = NodeCommand::GetHeadersRange {
start_height,
end_height,
};
let response = self.worker.node_exec(command).await?;
Ok(response
.into_headers()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = getSamplingMetadata)]
pub async fn get_sampling_metadata(&self, height: u64) -> Result<Option<SamplingMetadata>> {
let command = NodeCommand::GetSamplingMetadata { height };
let response = self.worker.node_exec(command).await?;
Ok(response
.into_sampling_metadata()
.map_err(|_| WorkerError::InvalidResponseType)?)
}
#[wasm_bindgen(js_name = eventsChannel)]
pub async fn events_channel(&self) -> Result<BroadcastChannel> {
let command = WorkerCommand::GetEventsChannelName;
let response = self.worker.worker_exec(command).await?;
let name = response
.into_events_channel_name()
.map_err(|_| WorkerError::InvalidResponseType)?;
Ok(BroadcastChannel::new(&name).unwrap())
}
#[wasm_bindgen(js_name = headerSubscribe, unchecked_return_type = "AsyncIterable<ExtendedHeader | SubscriptionError>")]
pub async fn header_subscribe(&self) -> Result<AsyncIterator> {
let command = SubscriptionCommand::Headers;
let port = self.worker.subscribe(command).await?;
into_async_iterator::<ExtendedHeader>(port)
}
#[wasm_bindgen(js_name = blobSubscribe, unchecked_return_type = "AsyncIterable<BlobsAtHeight | SubscriptionError>")]
pub async fn blob_subscribe(&self, namespace: Namespace) -> Result<AsyncIterator> {
let command = SubscriptionCommand::Blobs(namespace);
let port = self.worker.subscribe(command).await?;
into_async_iterator::<BlobsAtHeight>(port)
}
#[wasm_bindgen(js_name = namespaceSubscribe, unchecked_return_type = "AsyncIterable<SharesAtHeight | SubscriptionError>")]
pub async fn namespace_subscribe(&self, namespace: Namespace) -> Result<AsyncIterator> {
let command = SubscriptionCommand::Shares(namespace);
let port = self.worker.subscribe(command).await?;
into_async_iterator::<SharesAtHeight>(port)
}
}
#[wasm_bindgen(js_class = NodeConfig)]
impl WasmNodeConfig {
pub fn default(network: Network) -> WasmNodeConfig {
let bootnodes = network::Network::from(network)
.canonical_bootnodes()
.map(|addr| addr.to_string())
.collect::<Vec<_>>();
WasmNodeConfig {
network,
bootnodes,
identity_key: None,
use_persistent_memory: true,
custom_pruning_window_secs: None,
}
}
pub(crate) async fn into_node_builder(self) -> Result<NodeBuilder<WasmBlockstore, WasmStore>> {
let network = network::Network::from(self.network);
let network_id = network.id();
let mut builder = if self.use_persistent_memory {
let store_name = format!("lumina-{network_id}");
let blockstore_name = format!("lumina-{network_id}-blockstore");
let store = IndexedDbStore::new(&store_name)
.await
.context("Failed to open the store")?;
let blockstore = IndexedDbBlockstore::new(&blockstore_name)
.await
.context("Failed to open the blockstore")?;
NodeBuilder::new()
.store(EitherStore::Right(store))
.blockstore(EitherBlockstore::Right(blockstore))
} else {
NodeBuilder::new()
.store(EitherStore::Left(InMemoryStore::new()))
.blockstore(EitherBlockstore::Left(InMemoryBlockstore::new()))
.pruning_window(DEFAULT_PRUNING_WINDOW_IN_MEMORY)
};
if let Some(key_bytes) = self.identity_key {
let keypair = Keypair::ed25519_from_bytes(key_bytes).context("could not decode key")?;
builder = builder.keypair(keypair);
}
let bootnodes = self
.bootnodes
.into_iter()
.map(|addr| {
addr.parse()
.with_context(|| format!("invalid multiaddr: {addr}"))
})
.collect::<Result<Vec<Multiaddr>, _>>()?;
builder = builder
.network(network)
.sync_batch_size(128)
.bootnodes(bootnodes);
if let Some(secs) = self.custom_pruning_window_secs {
let dur = Duration::from_secs(secs.into());
builder = builder.pruning_window(dur);
}
Ok(builder)
}
}