diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 5099a64..cd45c43 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "antd" -version = "0.6.0" +version = "0.6.1" dependencies = [ "ant-core", "axum 0.8.9", diff --git a/antd/Cargo.toml b/antd/Cargo.toml index cea5e56..94e77d9 100644 --- a/antd/Cargo.toml +++ b/antd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "antd" -version = "0.6.0" +version = "0.6.1" edition = "2021" [dependencies] diff --git a/antd/openapi.yaml b/antd/openapi.yaml index f7526c8..024647f 100644 --- a/antd/openapi.yaml +++ b/antd/openapi.yaml @@ -447,6 +447,10 @@ paths: $ref: "#/components/responses/BadRequest" "500": $ref: "#/components/responses/InternalError" + "501": + description: >- + visibility:"public" not yet supported on this endpoint — + blocked on upstream ant-client #73. "503": $ref: "#/components/responses/ServiceUnavailable" @@ -765,6 +769,15 @@ components: data: type: string description: Base64-encoded data to upload + visibility: + type: string + enum: [private, public] + description: >- + Upload visibility. "private" (default) returns the DataMap to + the caller. "public" is reserved — returns 501 until upstream + ant-core exposes data_prepare_upload_with_visibility (tracked + as ant-client PR #73). Use /v1/upload/prepare with a file path + today. PrepareUploadRequest: type: object @@ -773,6 +786,15 @@ components: path: type: string description: Absolute path to local file + visibility: + type: string + enum: [private, public] + description: >- + Upload visibility. "private" (default) returns the DataMap to + the caller. "public" bundles the serialized DataMap chunk into + the same external-signer payment batch and surfaces its + network address on /v1/upload/finalize via data_map_address. + Omitting this field preserves pre-0.5.0 (private) behavior. PrepareUploadResponse: type: object @@ -901,7 +923,17 @@ components: description: Hex-encoded serialized DataMap (always returned) address: type: string - description: Network address of the stored DataMap (only set when store_data_map=true) + description: >- + Network address of the DataMap stored via the legacy + store_data_map=true path (paid by the daemon's internal + wallet). Prefer visibility:"public" on prepare instead. + data_map_address: + type: string + description: >- + Network address of the bundled DataMap chunk when the upload + was prepared with visibility:"public". The DataMap chunk's + payment is part of the same external-signer batch as the + data chunks — no separate daemon-wallet payment. chunks_stored: type: integer description: Number of chunks stored on the network diff --git a/antd/src/rest/upload.rs b/antd/src/rest/upload.rs index 8828c50..93c075c 100644 --- a/antd/src/rest/upload.rs +++ b/antd/src/rest/upload.rs @@ -113,10 +113,12 @@ pub async fn prepare_upload( AntdError::BadRequest("invalid path".into()) })?; + let visibility = parse_visibility(req.visibility.as_deref()).map_err(AntdError::BadRequest)?; + let client = state.client.clone(); let prepared = tokio::spawn(async move { client - .file_prepare_upload(&path) + .file_prepare_upload_with_visibility(&path, visibility) .await .map_err(AntdError::from_core) }) @@ -149,6 +151,21 @@ pub async fn prepare_data_upload( use base64::Engine; use bytes::Bytes; + let visibility = parse_visibility(req.visibility.as_deref()).map_err(AntdError::BadRequest)?; + + // Public visibility on the in-memory data path requires the upstream + // `data_prepare_upload_with_visibility` (ant-client PR #73). Until that + // lands, refuse the public variant rather than silently returning a + // private prepare. The file-path equivalent works today. + if matches!(visibility, ant_core::data::Visibility::Public) { + return Err(AntdError::NotImplemented( + "visibility:\"public\" is not yet supported on /v1/data/prepare; \ + use /v1/upload/prepare with a file path, or wait for upstream \ + ant-client #73 to land" + .into(), + )); + } + let data = BASE64 .decode(&req.data) .map_err(|e| AntdError::BadRequest(format!("invalid base64: {e}")))?; @@ -205,7 +222,7 @@ pub async fn finalize_upload( let store_on_network = req.store_data_map; let client = state.client.clone(); - let (data_map_hex, address, chunks_stored) = match &prepared.payment_info { + let (data_map_hex, address, data_map_address, chunks_stored) = match &prepared.payment_info { ant_core::data::ExternalPaymentInfo::WaveBatch { .. } => { // Wave-batch: require tx_hashes let tx_hashes_raw = req.tx_hashes.ok_or_else(|| { @@ -268,7 +285,14 @@ pub async fn finalize_upload( None }; - Ok::<_, AntdError>((data_map_hex, address, result.chunks_stored)) + let data_map_address = result.data_map_address.map(hex::encode); + + Ok::<_, AntdError>(( + data_map_hex, + address, + data_map_address, + result.chunks_stored, + )) }) .await .map_err(|e| AntdError::Internal(format!("task failed: {e}")))?? @@ -314,7 +338,14 @@ pub async fn finalize_upload( None }; - Ok::<_, AntdError>((data_map_hex, address, result.chunks_stored)) + let data_map_address = result.data_map_address.map(hex::encode); + + Ok::<_, AntdError>(( + data_map_hex, + address, + data_map_address, + result.chunks_stored, + )) }) .await .map_err(|e| AntdError::Internal(format!("task failed: {e}")))?? @@ -324,6 +355,7 @@ pub async fn finalize_upload( Ok(Json(FinalizeUploadResponse { data_map: data_map_hex, address, + data_map_address, chunks_stored: chunks_stored as u64, })) } diff --git a/antd/src/types.rs b/antd/src/types.rs index ef21d3a..efdc4d8 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -63,11 +63,24 @@ pub struct ChunkGetResponse { #[derive(Deserialize)] pub struct PrepareUploadRequest { pub path: String, + /// Upload visibility: `"private"` (default — DataMap returned to the + /// caller) or `"public"` (DataMap chunk bundled into the same payment + /// batch and stored on-network; its address is returned on finalize). + /// Omitting this field is equivalent to `"private"` and preserves + /// pre-0.6.1 behavior. + #[serde(default)] + pub visibility: Option, } #[derive(Deserialize)] pub struct PrepareDataUploadRequest { pub data: String, // base64 + /// Same semantics as [`PrepareUploadRequest::visibility`]. Currently + /// only `"private"` (or omission) is accepted on this endpoint; + /// `"public"` returns 501 until upstream `ant-core` exposes + /// `data_prepare_upload_with_visibility` (tracked as ant-client PR #73). + #[serde(default)] + pub visibility: Option, } #[derive(Serialize)] @@ -154,9 +167,18 @@ pub struct FinalizeUploadRequest { pub struct FinalizeUploadResponse { /// Hex-encoded serialized DataMap. Always returned. pub data_map: String, - /// Network address of the stored DataMap (only set when store_data_map=true). + /// Network address of the stored DataMap, only set when the legacy + /// `store_data_map=true` path published the DataMap via the daemon's + /// internal wallet. New callers should prefer `visibility:"public"` + /// on prepare and read [`Self::data_map_address`] instead. #[serde(skip_serializing_if = "Option::is_none")] pub address: Option, + /// Network address of the bundled DataMap chunk when the upload was + /// prepared with `visibility:"public"`. The DataMap chunk's payment + /// is part of the same external-signer batch as the data chunks, so + /// no separate daemon-wallet payment is required. + #[serde(skip_serializing_if = "Option::is_none")] + pub data_map_address: Option, /// Number of chunks stored on the network. pub chunks_stored: u64, } @@ -275,6 +297,19 @@ pub fn format_payment_mode(mode: ant_core::data::PaymentMode) -> String { } } +/// Parse a visibility string into ant-core's `Visibility` enum. +/// +/// Accepts `"private"`, `"public"`, or absent (defaults to `Private`). +pub fn parse_visibility(s: Option<&str>) -> Result { + match s { + None | Some("private") => Ok(ant_core::data::Visibility::Private), + Some("public") => Ok(ant_core::data::Visibility::Public), + Some(other) => Err(format!( + "invalid visibility: {other:?}. Use \"private\" or \"public\"" + )), + } +} + // ── Wallet ── #[derive(Serialize)] @@ -526,4 +561,76 @@ mod tests { assert_eq!(chunks, 6); assert_eq!(cost, "0"); } + + #[test] + fn prepare_request_visibility_defaults_to_none() { + let req: PrepareUploadRequest = serde_json::from_str(r#"{"path":"/tmp/foo"}"#).unwrap(); + assert_eq!(req.path, "/tmp/foo"); + assert!(req.visibility.is_none()); + } + + #[test] + fn prepare_request_visibility_round_trips_public() { + let req: PrepareUploadRequest = + serde_json::from_str(r#"{"path":"/tmp/foo","visibility":"public"}"#).unwrap(); + assert_eq!(req.visibility.as_deref(), Some("public")); + } + + #[test] + fn prepare_data_request_visibility_defaults_to_none() { + let req: PrepareDataUploadRequest = serde_json::from_str(r#"{"data":"AAA="}"#).unwrap(); + assert!(req.visibility.is_none()); + } + + #[test] + fn parse_visibility_accepts_known_values() { + assert!(matches!( + parse_visibility(None), + Ok(ant_core::data::Visibility::Private) + )); + assert!(matches!( + parse_visibility(Some("private")), + Ok(ant_core::data::Visibility::Private) + )); + assert!(matches!( + parse_visibility(Some("public")), + Ok(ant_core::data::Visibility::Public) + )); + } + + #[test] + fn parse_visibility_rejects_unknown_values() { + let err = parse_visibility(Some("Public")).unwrap_err(); + assert!(err.contains("Public"), "err was: {err}"); + let err = parse_visibility(Some("")).unwrap_err(); + assert!(err.contains("\"\""), "err was: {err}"); + } + + #[test] + fn finalize_response_serializes_data_map_address() { + let resp = FinalizeUploadResponse { + data_map: "deadbeef".into(), + address: None, + data_map_address: Some("cafebabe".into()), + chunks_stored: 4, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["data_map"], "deadbeef"); + assert_eq!(json["data_map_address"], "cafebabe"); + assert!(json.get("address").is_none()); + assert_eq!(json["chunks_stored"], 4); + } + + #[test] + fn finalize_response_omits_data_map_address_when_private() { + let resp = FinalizeUploadResponse { + data_map: "deadbeef".into(), + address: None, + data_map_address: None, + chunks_stored: 4, + }; + let json = serde_json::to_value(&resp).unwrap(); + assert!(json.get("address").is_none()); + assert!(json.get("data_map_address").is_none()); + } }