From 457d4d2733aa8af2d1df056bd7751a7d50251fbb Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 5 May 2026 16:07:43 +0100 Subject: [PATCH 1/2] feat(antd): wire visibility through /v1/upload/prepare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today /v1/upload/prepare unconditionally calls file_prepare_upload(path), which is private-only. The legacy store_data_map=true on finalize *does* publish the DataMap chunk, but pays for it from the daemon's internal wallet — not the external signer. Indelible needs the truly bundled atomic external-signer public path. This PR: - Adds visibility: Option to PrepareUploadRequest. Accepts "private" (default) or "public". Omitting preserves pre-0.6.x behavior. - Switches prepare_upload to call file_prepare_upload_with_visibility, which is in our pinned ant-cli-v0.2.2. For "public", ant-core bundles the serialized DataMap as one extra chunk in the same PreparedUpload payment batch — the external signer signs ONE EVM tx covering chunks + DataMap. - Adds the same visibility field to PrepareDataUploadRequest, but rejects "public" with 501 because the in-memory data path requires upstream data_prepare_upload_with_visibility (drafted separately as ant-client #73). Once that merges, the 501 flips to a real call. - Adds data_map_address to FinalizeUploadResponse, populated from FileUploadResult.data_map_address when the prepared upload was public. Independent of the legacy store_data_map flag — that field stays honored for backward compat and is documented as legacy. - Bumps antd to 0.6.1 (additive, wire-compat preserved via serde optional fields). - Updates openapi.yaml for the new fields and the 501 response on /v1/data/prepare. Tests cover serde round-trips for the new fields, the visibility parser's accept/reject cases, and that data_map_address is omitted from JSON when None (so old clients see no new fields). Validated end-to-end on Arbitrum One mainnet: external-signer wave-batch public upload of a 10 KB file produced 4 quotes (3 data + 1 DataMap), single payForQuotes tx covered all 4 atomically, finalize returned data_map_address, and download_public via that address round-tripped byte-for-byte. Co-Authored-By: Claude Opus 4.7 (1M context) --- antd/Cargo.lock | 2 +- antd/Cargo.toml | 2 +- antd/openapi.yaml | 34 +++++++++++- antd/src/rest/upload.rs | 32 ++++++++++-- antd/src/types.rs | 111 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 173 insertions(+), 8 deletions(-) diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 5099a643..cd45c43e 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 cea5e56a..94e77d9c 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 f7526c82..024647f6 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 8828c508..f593147d 100644 --- a/antd/src/rest/upload.rs +++ b/antd/src/rest/upload.rs @@ -113,10 +113,13 @@ 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 +152,22 @@ 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 +224,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 +287,9 @@ 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 +335,9 @@ 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 +347,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 ef21d3aa..8a84d27a 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.5.0 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,78 @@ 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()); + } } From 5940534630a15c4ac1300f1eea6b37b891f93467 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 5 May 2026 17:50:26 +0100 Subject: [PATCH 2/2] =?UTF-8?q?style(antd):=20cargo=20fmt=20=E2=80=94=20ma?= =?UTF-8?q?tch=20CI=20rustfmt=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's rustfmt 1.95.0 wants the finalize tuple split across lines and the test let-bindings collapsed onto one line; my local formatter had the opposite preference for both. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) --- antd/src/rest/upload.rs | 20 ++++++++++++++------ antd/src/types.rs | 8 +++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/antd/src/rest/upload.rs b/antd/src/rest/upload.rs index f593147d..93c075c4 100644 --- a/antd/src/rest/upload.rs +++ b/antd/src/rest/upload.rs @@ -113,8 +113,7 @@ pub async fn prepare_upload( AntdError::BadRequest("invalid path".into()) })?; - let visibility = - parse_visibility(req.visibility.as_deref()).map_err(AntdError::BadRequest)?; + let visibility = parse_visibility(req.visibility.as_deref()).map_err(AntdError::BadRequest)?; let client = state.client.clone(); let prepared = tokio::spawn(async move { @@ -152,8 +151,7 @@ pub async fn prepare_data_upload( use base64::Engine; use bytes::Bytes; - let visibility = - parse_visibility(req.visibility.as_deref()).map_err(AntdError::BadRequest)?; + 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 @@ -289,7 +287,12 @@ pub async fn finalize_upload( let data_map_address = result.data_map_address.map(hex::encode); - Ok::<_, AntdError>((data_map_hex, address, data_map_address, result.chunks_stored)) + Ok::<_, AntdError>(( + data_map_hex, + address, + data_map_address, + result.chunks_stored, + )) }) .await .map_err(|e| AntdError::Internal(format!("task failed: {e}")))?? @@ -337,7 +340,12 @@ pub async fn finalize_upload( let data_map_address = result.data_map_address.map(hex::encode); - Ok::<_, AntdError>((data_map_hex, address, data_map_address, result.chunks_stored)) + Ok::<_, AntdError>(( + data_map_hex, + address, + data_map_address, + result.chunks_stored, + )) }) .await .map_err(|e| AntdError::Internal(format!("task failed: {e}")))?? diff --git a/antd/src/types.rs b/antd/src/types.rs index 8a84d27a..efdc4d8b 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -67,7 +67,7 @@ pub struct PrepareUploadRequest { /// 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.5.0 behavior. + /// pre-0.6.1 behavior. #[serde(default)] pub visibility: Option, } @@ -564,8 +564,7 @@ mod tests { #[test] fn prepare_request_visibility_defaults_to_none() { - let req: PrepareUploadRequest = - serde_json::from_str(r#"{"path":"/tmp/foo"}"#).unwrap(); + let req: PrepareUploadRequest = serde_json::from_str(r#"{"path":"/tmp/foo"}"#).unwrap(); assert_eq!(req.path, "/tmp/foo"); assert!(req.visibility.is_none()); } @@ -579,8 +578,7 @@ mod tests { #[test] fn prepare_data_request_visibility_defaults_to_none() { - let req: PrepareDataUploadRequest = - serde_json::from_str(r#"{"data":"AAA="}"#).unwrap(); + let req: PrepareDataUploadRequest = serde_json::from_str(r#"{"data":"AAA="}"#).unwrap(); assert!(req.visibility.is_none()); }