use std::{fs::File, io::Read, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use freenet::{dev_tool::OperationMode, server::WebApp};
use freenet_stdlib::prelude::{
ContractCode, ContractContainer, ContractWasmAPIVersion, Parameters, WrappedContract,
};
use freenet_stdlib::{
client_api::{
ClientRequest, ContractRequest, ContractResponse, DelegateRequest, HostResponse, WebApi,
},
prelude::*,
};
use xz2::read::XzDecoder;
use crate::config::{BaseConfig, GetConfig, PutConfig, SubscribeConfig, UpdateConfig};
mod v1;
pub(crate) const RESPONSE_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Debug, Clone, clap::Subcommand)]
pub(crate) enum PutType {
Contract(PutContract),
Delegate(PutDelegate),
}
#[derive(clap::Parser, Clone, Debug)]
pub(crate) struct PutContract {
#[arg(long)]
pub(crate) related_contracts: Option<PathBuf>,
#[arg(long)]
pub(crate) state: Option<PathBuf>,
#[arg(long)]
pub(crate) webapp_archive: Option<PathBuf>,
#[arg(long)]
pub(crate) webapp_metadata: Option<PathBuf>,
}
#[derive(clap::Parser, Clone, Debug)]
pub(crate) struct PutDelegate {
#[arg(long, env = "DELEGATE_NONCE", default_value_t = String::new())]
pub(crate) nonce: String,
#[arg(long, env = "DELEGATE_CIPHER", default_value_t = String::new())]
pub(crate) cipher: String,
}
pub async fn put(config: PutConfig, other: BaseConfig) -> anyhow::Result<()> {
if config.release {
anyhow::bail!("Cannot publish contracts in the network yet");
}
let params = if let Some(params) = &config.parameters {
let mut buf = vec![];
File::open(params)?.read_to_end(&mut buf)?;
Parameters::from(buf)
} else {
Parameters::from(&[] as &[u8])
};
match &config.package_type {
PutType::Contract(contract) => put_contract(&config, contract, other, params).await,
PutType::Delegate(delegate) => put_delegate(&config, delegate, other, params).await,
}
}
async fn put_contract(
config: &PutConfig,
contract_config: &PutContract,
other: BaseConfig,
params: Parameters<'static>,
) -> anyhow::Result<()> {
let contract = if let Ok(raw_code) = ContractCode::load_raw(&config.code) {
let code = ContractCode::from(raw_code.data().to_vec());
let wrapped = WrappedContract::new(Arc::new(code), params);
let api_version = ContractWasmAPIVersion::V1(wrapped);
ContractContainer::from(api_version)
} else {
ContractContainer::try_from((config.code.as_path(), params))?
};
let state = if let Some(ref webapp_archive) = contract_config.webapp_archive {
let mut archive = vec![];
File::open(webapp_archive)?.read_to_end(&mut archive)?;
let metadata = if let Some(ref metadata_path) = contract_config.webapp_metadata {
let mut buf = vec![];
File::open(metadata_path)?.read_to_end(&mut buf)?;
buf
} else {
vec![]
};
use std::io::Cursor;
use tar::Archive;
let mut found_index = false;
let decoder = XzDecoder::new(Cursor::new(&archive));
let mut tar = Archive::new(decoder);
let entries = tar.entries()?;
for entry in entries {
let entry = entry?;
let path = entry.path()?;
tracing::debug!("Found file in archive: {}", path.display());
if path.file_name().map(|f| f.to_string_lossy()) == Some("index.html".into()) {
tracing::debug!("Found index.html at path: {}", path.display());
found_index = true;
break;
}
}
if !found_index {
tracing::warn!("Warning: No index.html found at root of webapp archive");
}
let webapp = WebApp::from_compressed(metadata.clone(), archive)?;
tracing::info!(
metadata_len = metadata.len(),
"Metadata being packed into WebApp state"
);
if !metadata.is_empty() {
tracing::info!(
first_32_bytes = format!("{:02x?}", &metadata[..metadata.len().min(32)]),
"First 32 bytes of metadata"
);
}
let packed = webapp.pack()?;
tracing::info!(packed_len = packed.len(), "WebApp state after packing");
if !packed.is_empty() {
tracing::info!(
first_32_bytes = format!("{:02x?}", &packed[..packed.len().min(32)]),
"First 32 bytes of packed state"
);
}
packed.into()
} else if let Some(ref state_path) = contract_config.state {
let mut buf = vec![];
File::open(state_path)?.read_to_end(&mut buf)?;
buf.into()
} else {
tracing::warn!(
"no state provided for contract, if your contract cannot handle empty state correctly, this will always cause an error."
);
freenet_stdlib::prelude::State::from(vec![])
};
let related_contracts: freenet_stdlib::prelude::RelatedContracts =
if let Some(_related) = &contract_config.related_contracts {
todo!("use `related` contracts")
} else {
Default::default()
};
let key = contract.key();
println!("Publishing contract {key}");
tracing::debug!(
state_size = state.as_ref().len(),
has_related = related_contracts.states().next().is_some(), "Contract details"
);
let request = ContractRequest::Put {
contract,
state: state.to_vec().into(),
related_contracts,
subscribe: config.subscribe,
blocking_subscribe: false,
}
.into();
tracing::debug!("Starting WebSocket client connection");
let mut client = start_api_client(other).await?;
tracing::debug!("WebSocket client connected successfully");
execute_command(request, &mut client).await?;
tracing::info!(
%key,
"Request submitted, waiting for network response (timeout: {}s)...",
RESPONSE_TIMEOUT.as_secs()
);
let result = match tokio::time::timeout(RESPONSE_TIMEOUT, client.recv()).await {
Ok(Ok(HostResponse::ContractResponse(ContractResponse::PutResponse {
key: response_key,
}))) => {
tracing::info!(%response_key, "Contract published successfully");
Ok(())
}
Ok(Ok(HostResponse::ContractResponse(ContractResponse::UpdateResponse {
key: response_key,
..
}))) => {
tracing::info!(%response_key, "Contract updated successfully");
Ok(())
}
Ok(Ok(HostResponse::ContractResponse(other))) => {
Err(anyhow::anyhow!("Unexpected contract response: {:?}", other))
}
Ok(Ok(other)) => Err(anyhow::anyhow!("Unexpected response type: {:?}", other)),
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {e}")),
Err(_) => Err(anyhow::anyhow!(
"Timeout waiting for server response for contract {key} after {} seconds.\n\
The operation may have succeeded on the network - check server logs.\n\
If this timeout is consistently too short, consider:\n\
- Network latency between you and the gateway\n\
- Contract size and propagation time through the network",
RESPONSE_TIMEOUT.as_secs()
)),
};
close_api_client(&mut client).await;
result
}
async fn put_delegate(
config: &PutConfig,
delegate_config: &PutDelegate,
other: BaseConfig,
params: Parameters<'static>,
) -> anyhow::Result<()> {
let delegate = DelegateContainer::try_from((config.code.as_path(), params))?;
let (cipher, nonce) = if delegate_config.cipher.is_empty() && delegate_config.nonce.is_empty() {
println!(
"Using default cipher and nonce.
For additional hardening is recommended to use a different cipher and nonce to encrypt secrets in storage.");
(
::freenet_stdlib::client_api::DelegateRequest::DEFAULT_CIPHER,
::freenet_stdlib::client_api::DelegateRequest::DEFAULT_NONCE,
)
} else {
let mut cipher = [0; 32];
bs58::decode(delegate_config.cipher.as_bytes())
.with_alphabet(bs58::Alphabet::BITCOIN)
.onto(&mut cipher)?;
let mut nonce = [0; 24];
bs58::decode(delegate_config.nonce.as_bytes())
.with_alphabet(bs58::Alphabet::BITCOIN)
.onto(&mut nonce)?;
(cipher, nonce)
};
let delegate_key = delegate.key().clone();
println!("Putting delegate {} ", delegate_key.encode());
let request = DelegateRequest::RegisterDelegate {
delegate,
cipher,
nonce,
}
.into();
let mut client = start_api_client(other).await?;
execute_command(request, &mut client).await?;
tracing::info!(
delegate = %delegate_key.encode(),
"Request submitted, waiting for network response (timeout: {}s)...",
RESPONSE_TIMEOUT.as_secs()
);
let result = match tokio::time::timeout(RESPONSE_TIMEOUT, client.recv()).await {
Ok(Ok(HostResponse::DelegateResponse { key, values })) => {
tracing::info!(%key, response_count = values.len(), "Delegate registered successfully");
Ok(())
}
Ok(Ok(other)) => Err(anyhow::anyhow!("Unexpected response type: {:?}", other)),
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {e}")),
Err(_) => Err(anyhow::anyhow!(
"Timeout waiting for server response for delegate {} after {} seconds.\n\
The operation may have succeeded on the network - check server logs.\n\
If this timeout is consistently too short, consider:\n\
- Network latency between you and the gateway\n\
- Delegate size and propagation time through the network",
delegate_key.encode(),
RESPONSE_TIMEOUT.as_secs()
)),
};
close_api_client(&mut client).await;
result
}
#[derive(clap::Parser, Clone, Debug)]
pub(crate) struct GetContractIdConfig {
#[arg(long)]
pub(crate) code: PathBuf,
#[arg(long)]
pub(crate) parameters: Option<PathBuf>,
}
pub async fn get_contract_id(config: GetContractIdConfig) -> anyhow::Result<()> {
let params = if let Some(params) = &config.parameters {
let mut buf = vec![];
File::open(params)?.read_to_end(&mut buf)?;
Parameters::from(buf)
} else {
Parameters::from(&[] as &[u8])
};
let contract = if let Ok(raw_code) = ContractCode::load_raw(&config.code) {
let code = ContractCode::from(raw_code.data().to_vec());
let wrapped = WrappedContract::new(Arc::new(code), params);
let api_version = ContractWasmAPIVersion::V1(wrapped);
ContractContainer::from(api_version)
} else {
ContractContainer::try_from((config.code.as_path(), params))?
};
let key = contract.key();
println!("{key}");
Ok(())
}
pub async fn update(config: UpdateConfig, other: BaseConfig) -> anyhow::Result<()> {
if config.release {
anyhow::bail!("Cannot publish contracts in the network yet");
}
let instance_id = ContractInstanceId::try_from(config.key)?;
let key = ContractKey::from_id_and_code(instance_id, CodeHash::new([0u8; 32]));
println!("Updating contract {key}");
let data = {
let mut buf = vec![];
File::open(&config.delta)?.read_to_end(&mut buf)?;
StateDelta::from(buf).into()
};
let request = ContractRequest::Update { key, data }.into();
let mut client = start_api_client(other).await?;
execute_command(request, &mut client).await?;
tracing::info!(
%key,
"Request submitted, waiting for network response (timeout: {}s)...",
RESPONSE_TIMEOUT.as_secs()
);
let result = match tokio::time::timeout(RESPONSE_TIMEOUT, client.recv()).await {
Ok(Ok(HostResponse::ContractResponse(ContractResponse::UpdateResponse {
key: response_key,
summary,
}))) => {
tracing::info!(%response_key, ?summary, "Contract updated successfully");
Ok(())
}
Ok(Ok(HostResponse::ContractResponse(other))) => {
Err(anyhow::anyhow!("Unexpected contract response: {:?}", other))
}
Ok(Ok(other)) => Err(anyhow::anyhow!("Unexpected response type: {:?}", other)),
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {e}")),
Err(_) => Err(anyhow::anyhow!(
"Timeout waiting for server response for contract {key} after {} seconds.\n\
The operation may have succeeded on the network - check server logs.\n\
If this timeout is consistently too short, consider:\n\
- Network latency between you and the gateway\n\
- State delta size and propagation time through the network",
RESPONSE_TIMEOUT.as_secs()
)),
};
close_api_client(&mut client).await;
result
}
pub async fn get(config: GetConfig, other: BaseConfig) -> anyhow::Result<()> {
let instance_id = ContractInstanceId::try_from(config.key)?;
let key = ContractKey::from_id_and_code(instance_id, CodeHash::new([0u8; 32]));
eprintln!("Getting contract {key}");
let request = ContractRequest::Get {
key: *key.id(),
return_contract_code: config.return_code,
subscribe: false,
blocking_subscribe: false,
}
.into();
let mut client = start_api_client(other).await?;
execute_command(request, &mut client).await?;
let result = match tokio::time::timeout(RESPONSE_TIMEOUT, client.recv()).await {
Ok(Ok(HostResponse::ContractResponse(ContractResponse::GetResponse {
key: response_key,
state,
..
}))) => {
let state_bytes: &[u8] = state.as_ref();
eprintln!("Contract {response_key}: {} bytes", state_bytes.len());
if let Some(output_path) = &config.output {
std::fs::write(output_path, state_bytes)?;
eprintln!("State written to {}", output_path.display());
} else {
use std::io::Write;
std::io::stdout().write_all(state_bytes)?;
std::io::stdout().flush()?;
}
Ok(())
}
Ok(Ok(HostResponse::ContractResponse(ContractResponse::NotFound { instance_id }))) => {
Err(anyhow::anyhow!("Contract not found: {instance_id}"))
}
Ok(Ok(HostResponse::ContractResponse(other))) => {
Err(anyhow::anyhow!("Unexpected contract response: {:?}", other))
}
Ok(Ok(other)) => Err(anyhow::anyhow!("Unexpected response type: {:?}", other)),
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {e}")),
Err(_) => Err(anyhow::anyhow!(
"Timeout waiting for get response for contract {key} after {} seconds",
RESPONSE_TIMEOUT.as_secs()
)),
};
close_api_client(&mut client).await;
result
}
pub async fn subscribe(config: SubscribeConfig, other: BaseConfig) -> anyhow::Result<()> {
let instance_id = ContractInstanceId::try_from(config.key)?;
let key = ContractKey::from_id_and_code(instance_id, CodeHash::new([0u8; 32]));
eprintln!("Subscribing to contract {key}");
let request = ContractRequest::Subscribe {
key: *key.id(),
summary: None,
}
.into();
let mut client = start_api_client(other).await?;
execute_command(request, &mut client).await?;
match tokio::time::timeout(RESPONSE_TIMEOUT, client.recv()).await {
Ok(Ok(HostResponse::ContractResponse(ContractResponse::SubscribeResponse {
key: response_key,
subscribed: true,
}))) => {
eprintln!("Subscribed to {response_key}, waiting for updates (Ctrl+C to stop)...");
}
Ok(Ok(HostResponse::ContractResponse(ContractResponse::SubscribeResponse {
key: response_key,
subscribed: false,
}))) => {
close_api_client(&mut client).await;
return Err(anyhow::anyhow!(
"Subscription rejected for contract {response_key}"
));
}
Ok(Ok(HostResponse::ContractResponse(ContractResponse::NotFound { instance_id }))) => {
close_api_client(&mut client).await;
return Err(anyhow::anyhow!("Contract not found: {instance_id}"));
}
Ok(Ok(other)) => {
close_api_client(&mut client).await;
return Err(anyhow::anyhow!("Unexpected response: {:?}", other));
}
Ok(Err(e)) => {
close_api_client(&mut client).await;
return Err(anyhow::anyhow!("Failed to receive response: {e}"));
}
Err(_) => {
close_api_client(&mut client).await;
return Err(anyhow::anyhow!(
"Timeout waiting for subscribe response after {} seconds",
RESPONSE_TIMEOUT.as_secs()
));
}
}
let mut update_count: u64 = 0;
loop {
tokio::select! {
response = client.recv() => {
match response {
Ok(HostResponse::ContractResponse(ContractResponse::UpdateNotification {
key: update_key,
update,
})) => {
update_count += 1;
let update_bytes = extract_update_bytes(&update);
eprintln!(
"Update #{update_count} for {update_key}: {} bytes ({})",
update_bytes.len(),
describe_update_variant(&update),
);
if let Some(output_path) = &config.output {
atomic_write(output_path, update_bytes)?;
eprintln!("Update written to {}", output_path.display());
} else {
use std::io::Write;
std::io::stdout().write_all(update_bytes)?;
std::io::stdout().flush()?;
}
}
Ok(other) => {
tracing::debug!("Received non-update response: {:?}", other);
}
Err(e) => {
close_api_client(&mut client).await;
return Err(anyhow::anyhow!("Connection error: {e}"));
}
}
}
_ = tokio::signal::ctrl_c() => {
eprintln!("\nReceived {update_count} updates total");
close_api_client(&mut client).await;
return Ok(());
}
}
}
}
pub(crate) async fn start_api_client(cfg: BaseConfig) -> anyhow::Result<WebApi> {
v1::start_api_client(cfg).await
}
pub(crate) async fn close_api_client(client: &mut WebApi) {
let _ = client.send(ClientRequest::Disconnect { cause: None }).await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
fn extract_update_bytes<'a>(update: &'a UpdateData<'_>) -> &'a [u8] {
match update {
UpdateData::State(state) => state.as_ref(),
UpdateData::Delta(delta) => delta.as_ref(),
UpdateData::StateAndDelta { state, .. } => state.as_ref(),
UpdateData::RelatedState { state, .. } => state.as_ref(),
UpdateData::RelatedDelta { delta, .. } => delta.as_ref(),
UpdateData::RelatedStateAndDelta { state, .. } => state.as_ref(),
_ => &[],
}
}
fn describe_update_variant(update: &UpdateData<'_>) -> &'static str {
match update {
UpdateData::State(_) => "state",
UpdateData::Delta(_) => "delta",
UpdateData::StateAndDelta { .. } => "state+delta",
UpdateData::RelatedState { .. } => "related-state",
UpdateData::RelatedDelta { .. } => "related-delta",
UpdateData::RelatedStateAndDelta { .. } => "related-state+delta",
_ => "unknown",
}
}
fn atomic_write(path: &std::path::Path, data: &[u8]) -> anyhow::Result<()> {
let dir = path.parent().unwrap_or(std::path::Path::new("."));
let tmp = dir.join(format!(
".{}.tmp",
path.file_name().unwrap_or_default().to_string_lossy()
));
std::fs::write(&tmp, data)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
pub(crate) async fn execute_command(
request: ClientRequest<'static>,
api_client: &mut WebApi,
) -> anyhow::Result<()> {
tracing::debug!("Starting execute_command with request: {request}");
tracing::debug!("Sending request to server and waiting for response...");
match v1::execute_command(request, api_client).await {
Ok(_) => {
tracing::debug!("Server confirmed successful execution");
Ok(())
}
Err(e) => {
tracing::error!("Server returned error: {}", e);
Err(e)
}
}
}