Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 71 additions & 6 deletions ffi/rust/ant-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,41 @@ impl Client {
})
}

/// Same as [`Self::file_upload_public`] but reports live progress to
/// `listener`: the `"encrypting"`, `"quoting"` and `"storing"` upload phases
/// as the file is self-encrypted, quoted and its chunks land on the network.
/// Use this on the wallet-backed (built-in payment) path to drive a progress
/// bar; the external-signer path uses the `prepare`/`finalize_*_with_progress`
/// pair instead.
pub async fn file_upload_public_with_progress(
&self,
path: String,
payment_mode: String,
listener: Box<dyn ProgressListener>,
) -> Result<FilePutPublicResult, ClientError> {
let mode = parse_payment_mode(&payment_mode)
.map_err(|e| ClientError::InvalidInput { reason: e })?;
let file_path = PathBuf::from(&path);

let (sender, handle) = upload_progress_bridge(listener);
let result = self
.inner
.file_upload_with_progress(&file_path, mode, Some(sender))
.await;
// Drop of `sender` inside the call ends the bridge; await it to flush
// any queued progress events before returning either way.
let _ = handle.await;
let result = result?;

// The data-map chunk is small and stored separately (no progress),
// matching `file_upload_public`.
let address = self.inner.data_map_store(&result.data_map).await?;

Ok(FilePutPublicResult {
address: hex::encode(address),
})
}

/// Upload a file from disk privately. Returns the serialized data map (hex)
/// rather than publishing it — the caller must keep it to retrieve the file
/// later via `dataGetPrivate`. This is the private counterpart of
Expand All @@ -640,6 +675,37 @@ impl Client {
})
}

/// Same as [`Self::file_upload_private`] but reports live progress to
/// `listener` (the `"encrypting"`, `"quoting"` and `"storing"` upload
/// phases). See [`Self::file_upload_public_with_progress`].
pub async fn file_upload_private_with_progress(
&self,
path: String,
payment_mode: String,
listener: Box<dyn ProgressListener>,
) -> Result<FilePutPrivateResult, ClientError> {
let mode = parse_payment_mode(&payment_mode)
.map_err(|e| ClientError::InvalidInput { reason: e })?;
let file_path = PathBuf::from(&path);

let (sender, handle) = upload_progress_bridge(listener);
let result = self
.inner
.file_upload_with_progress(&file_path, mode, Some(sender))
.await;
let _ = handle.await;
let result = result?;

let data_map_bytes =
rmp_serde::to_vec(&result.data_map).map_err(|e| ClientError::InternalError {
reason: format!("failed to serialize data map: {e}"),
})?;

Ok(FilePutPrivateResult {
data_map: hex::encode(data_map_bytes),
})
}

/// Publish an existing private data map as a public network chunk, returning
/// its hex address. Lets a caller turn a previously-private upload (a hex
/// data map from `dataPutPrivate` / `fileUploadPrivate`) into a shareable
Expand Down Expand Up @@ -732,12 +798,11 @@ impl Client {
let address = hex_to_address(&address_hex)?;
let data_map = self.inner.data_map_fetch(&address).await?;
let dest = PathBuf::from(&dest_path);
self.inner
.file_download(&data_map, &dest)
.await
.map_err(|e| ClientError::NetworkError {
reason: e.to_string(),
})?;
// Propagate the real error via the `From<ant_core::data::Error>` mapping
// instead of flattening everything into `NetworkError` — a timeout or an
// out-of-disk-space failure now surfaces as its own `ClientError`
// variant rather than a misleading "network error".
self.inner.file_download(&data_map, &dest).await?;
Ok(())
}

Expand Down
66 changes: 57 additions & 9 deletions ffi/rust/ant-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,15 @@ pub struct TxReceipt {
/// `phase` is one of the following strings. Note which methods actually emit
/// each phase today:
///
/// - **upload** — `"encrypting"` then `"quoting"`, emitted by
/// `prepare_file_upload_with_progress` while the file is encrypted and
/// quoted, then `"storing"`, emitted by `finalize_upload_with_progress` as
/// chunks land on the network. (The plain `prepare_file_upload` /
/// `prepare_data_upload` and the in-memory `prepare_data_upload` path take
/// no listener, so they surface no encrypt/quote progress.)
/// - **upload** — `"encrypting"`, then `"quoting"`, then `"storing"` as the
/// file is self-encrypted, quoted, and its chunks land on the network. On
/// the wallet-backed path all three phases come from a single
/// `file_upload_public_with_progress` / `file_upload_private_with_progress`
/// call. On the external-signer path they are split across the two steps:
/// `prepare_file_upload_with_progress` emits `"encrypting"`/`"quoting"` and
/// `finalize_upload_with_progress` emits `"storing"`. (The plain
/// `file_upload_*` / `prepare_*` methods take no listener, so they surface
/// no progress.)
/// - **download** — `"resolving"` then `"downloading"`, emitted by the
/// `download_*_to_file` methods.
///
Expand Down Expand Up @@ -269,6 +272,10 @@ pub enum ClientError {
NetworkError { reason: String },
#[error("Payment error: {reason}")]
PaymentError { reason: String },
#[error("Timeout: {reason}")]
Timeout { reason: String },
#[error("Insufficient disk space: {reason}")]
InsufficientDiskSpace { reason: String },
#[error("Invalid input: {reason}")]
InvalidInput { reason: String },
#[error("Not found: {reason}")]
Expand All @@ -290,10 +297,23 @@ impl From<ant_core::data::Error> for ClientError {
Error::InvalidData(msg) => ClientError::InvalidInput { reason: msg },
Error::Payment(msg) => ClientError::PaymentError { reason: msg },
Error::Network(msg) => ClientError::NetworkError { reason: msg },
Error::Timeout(msg) => ClientError::NetworkError {
reason: format!("timeout: {msg}"),
},
Error::Timeout(msg) => ClientError::Timeout { reason: msg },
Error::InsufficientDiskSpace(msg) => ClientError::InsufficientDiskSpace { reason: msg },
Error::InsufficientPeers(msg) => ClientError::NetworkError { reason: msg },
// A full disk during a download comes back as `Io` (core writes
// the destination file); surface it as disk-space rather than a
// generic internal error. Other `Io` kinds fall through below.
Error::Io(e) if e.kind() == std::io::ErrorKind::StorageFull => {
ClientError::InsufficientDiskSpace {
reason: format!("local disk full: {e}"),
}
}
// Note: at the pinned ant-core (0.3.1) a missing record surfaces
// as `InvalidData(..)` -> `InvalidInput` above, so this mapping
// never produces `NotFound`. Upstream ant-client#153 (merged
// 2026-07-20) adds a core `NotFound` variant; its arm lands with
// the 0.4.x pin bump (V2-650 / V2-686). Low balance still
// surfaces as `Payment(..)` pending an upstream variant (V2-601).
other => ClientError::InternalError {
reason: other.to_string(),
},
Expand All @@ -309,3 +329,31 @@ pub enum WalletError {
#[error("Operation failed: {reason}")]
OperationFailed { reason: String },
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn core_io_storage_full_maps_to_insufficient_disk_space() {
let io = std::io::Error::new(std::io::ErrorKind::StorageFull, "No space left on device");
let err: ClientError = ant_core::data::Error::Io(io).into();
assert!(matches!(
err,
ClientError::InsufficientDiskSpace { reason } if reason.contains("No space left")
));
}

#[test]
fn core_io_other_kinds_stay_internal() {
let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let err: ClientError = ant_core::data::Error::Io(io).into();
assert!(matches!(err, ClientError::InternalError { .. }));
}

#[test]
fn core_timeout_maps_to_timeout_variant() {
let err: ClientError = ant_core::data::Error::Timeout("slow".into()).into();
assert!(matches!(err, ClientError::Timeout { reason } if reason == "slow"));
}
}
2 changes: 1 addition & 1 deletion ffi/rust/ant-ffi/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pub async fn wait_for_receipt(
}

if start.elapsed().as_secs() >= timeout_secs {
return Err(ClientError::NetworkError {
return Err(ClientError::Timeout {
reason: format!("timed out waiting for receipt of {tx_hash} after {timeout_secs}s"),
});
}
Expand Down
Loading