diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 197a867..3dd37d3 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -268,7 +268,7 @@ def wallet_approve(self) -> bool: # --- External Signer (Two-Phase Upload) --- def prepare_upload(self, path: str) -> PrepareUploadResult: - """Prepare a file upload for external signing. + """Prepare a private file upload for external signing. Returns payment details that an external signer must process before calling finalize_upload. @@ -277,12 +277,34 @@ def prepare_upload(self, path: str) -> PrepareUploadResult: _check(resp) return _parse_prepare_result(resp.json()) + def prepare_upload_public(self, path: str) -> PrepareUploadResult: + """Prepare a public file upload for external signing. + + In addition to the data chunks, the daemon bundles the serialized + DataMap chunk into the same payment batch — the external signer + signs ONE EVM transaction covering chunks + DataMap. After + finalize_upload, the result's data_map_address is the shareable + retrieval handle. + + Requires antd >= 0.5.0. + """ + resp = self._http.post( + "/v1/upload/prepare", json={"path": path, "visibility": "public"} + ) + _check(resp) + return _parse_prepare_result(resp.json()) + def prepare_data_upload(self, data: bytes) -> PrepareUploadResult: - """Prepare a data upload for external signing. + """Prepare a private data upload for external signing. Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. Returns payment details that an external signer must process before calling finalize_upload. + + The public variant of this endpoint is not yet available — the + daemon returns 501 for visibility="public" until upstream ant-core + exposes data_prepare_upload_with_visibility. Use + prepare_upload_public with a file path instead. """ resp = self._http.post("/v1/data/prepare", json={"data": _b64(data)}) _check(resp) @@ -301,7 +323,11 @@ def finalize_upload(self, upload_id: str, tx_hashes: dict[str, str]) -> Finalize }) _check(resp) j = resp.json() - return FinalizeUploadResult(address=j.get("address", ""), chunks_stored=j.get("chunks_stored", 0)) + return FinalizeUploadResult( + address=j.get("address", ""), + chunks_stored=j.get("chunks_stored", 0), + data_map_address=j.get("data_map_address", ""), + ) def finalize_merkle_upload( self, upload_id: str, winner_pool_hash: str, store_data_map: bool = False, @@ -320,7 +346,11 @@ def finalize_merkle_upload( }) _check(resp) j = resp.json() - return FinalizeUploadResult(address=j.get("address", ""), chunks_stored=j.get("chunks_stored", 0)) + return FinalizeUploadResult( + address=j.get("address", ""), + chunks_stored=j.get("chunks_stored", 0), + data_map_address=j.get("data_map_address", ""), + ) class AsyncRestClient: @@ -476,7 +506,7 @@ async def wallet_approve(self) -> bool: # --- External Signer (Two-Phase Upload) --- async def prepare_upload(self, path: str) -> PrepareUploadResult: - """Prepare a file upload for external signing. + """Prepare a private file upload for external signing. Returns payment details that an external signer must process before calling finalize_upload. @@ -485,12 +515,34 @@ async def prepare_upload(self, path: str) -> PrepareUploadResult: _check(resp) return _parse_prepare_result(resp.json()) + async def prepare_upload_public(self, path: str) -> PrepareUploadResult: + """Prepare a public file upload for external signing. + + In addition to the data chunks, the daemon bundles the serialized + DataMap chunk into the same payment batch — the external signer + signs ONE EVM transaction covering chunks + DataMap. After + finalize_upload, the result's data_map_address is the shareable + retrieval handle. + + Requires antd >= 0.5.0. + """ + resp = await self._http.post( + "/v1/upload/prepare", json={"path": path, "visibility": "public"} + ) + _check(resp) + return _parse_prepare_result(resp.json()) + async def prepare_data_upload(self, data: bytes) -> PrepareUploadResult: - """Prepare a data upload for external signing. + """Prepare a private data upload for external signing. Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. Returns payment details that an external signer must process before calling finalize_upload. + + The public variant of this endpoint is not yet available — the + daemon returns 501 for visibility="public" until upstream ant-core + exposes data_prepare_upload_with_visibility. Use + prepare_upload_public with a file path instead. """ resp = await self._http.post("/v1/data/prepare", json={"data": _b64(data)}) _check(resp) @@ -509,7 +561,11 @@ async def finalize_upload(self, upload_id: str, tx_hashes: dict[str, str]) -> Fi }) _check(resp) j = resp.json() - return FinalizeUploadResult(address=j.get("address", ""), chunks_stored=j.get("chunks_stored", 0)) + return FinalizeUploadResult( + address=j.get("address", ""), + chunks_stored=j.get("chunks_stored", 0), + data_map_address=j.get("data_map_address", ""), + ) async def finalize_merkle_upload( self, upload_id: str, winner_pool_hash: str, store_data_map: bool = False, @@ -528,4 +584,8 @@ async def finalize_merkle_upload( }) _check(resp) j = resp.json() - return FinalizeUploadResult(address=j.get("address", ""), chunks_stored=j.get("chunks_stored", 0)) + return FinalizeUploadResult( + address=j.get("address", ""), + chunks_stored=j.get("chunks_stored", 0), + data_map_address=j.get("data_map_address", ""), + ) diff --git a/antd-py/src/antd/models.py b/antd-py/src/antd/models.py index 2cac8dc..77183ab 100644 --- a/antd-py/src/antd/models.py +++ b/antd-py/src/antd/models.py @@ -81,8 +81,9 @@ class PrepareUploadResult: @dataclass(frozen=True) class FinalizeUploadResult: """Result of finalizing an externally-signed upload.""" - address: str # hex address of stored data + address: str # legacy: set when store_data_map=true was passed (paid by daemon wallet) chunks_stored: int = 0 + data_map_address: str = "" # set when prepare was called with visibility="public" (paid in same external-signer batch) @dataclass(frozen=True) diff --git a/antd-py/tests/test_rest_client.py b/antd-py/tests/test_rest_client.py index d6a4ebc..3e3b8f8 100644 --- a/antd-py/tests/test_rest_client.py +++ b/antd-py/tests/test_rest_client.py @@ -114,6 +114,9 @@ def do_POST(self): # noqa: N802 elif path == "/v1/upload/prepare": req = json.loads(body) if body else {} + # Stash the request so tests can inspect it. + self.server._last_prepare_request = req + self.server._last_prepare_was_public = req.get("visibility") == "public" # Return merkle response when path contains "merkle", else wave_batch if "merkle" in req.get("path", ""): self._json_response(200, { @@ -165,7 +168,16 @@ def do_POST(self): # noqa: N802 req = json.loads(body) if body else {} # Store the request so tests can inspect it self.server._last_finalize_request = req - self._json_response(200, {"address": "0xFINAL", "chunks_stored": 42}) + response = { + "data_map": "0xDM", + "address": "0xFINAL", + "chunks_stored": 42, + } + # Emit data_map_address only when the matching prepare was public, + # mirroring antd's behavior at the wire layer. + if getattr(self.server, "_last_prepare_was_public", False): + response["data_map_address"] = "0xPUBADDR" + self._json_response(200, response) else: self._json_response(404, {"error": f"unknown route: {path}"}) @@ -364,3 +376,40 @@ def test_502_raises_network_error(self, client: RestClient): _check(resp) assert exc_info.value.status_code == 502 assert "bad gateway" in str(exc_info.value) + + +class TestPrepareUploadPublic: + """Verify prepare_upload_public sends visibility="public" and surfaces the + bundled DataMap address on finalize.""" + + def test_sends_visibility_public(self, client: RestClient, mock_server): + client.prepare_upload_public("/tmp/pub/file.dat") + req = mock_server._last_prepare_request + assert req["visibility"] == "public" + assert req["path"] == "/tmp/pub/file.dat" + + def test_finalize_surfaces_data_map_address_for_public( + self, client: RestClient, mock_server + ): + client.prepare_upload_public("/tmp/pub/file.dat") + result = client.finalize_upload("up_pub_1", {"qh1": "tx1"}) + assert result.data_map_address == "0xPUBADDR" + + def test_finalize_omits_data_map_address_for_private( + self, client: RestClient, mock_server + ): + # Old/private prepare path: daemon doesn't emit data_map_address; + # SDK must default the field to "" rather than crash. + client.prepare_upload("/tmp/private/file.dat") + result = client.finalize_upload("up_wave_1", {"qh1": "tx1"}) + assert result.data_map_address == "" + + def test_returned_result_is_a_finalize_upload_result( + self, client: RestClient, mock_server + ): + # Defensive: confirm the dataclass shape (other fields untouched). + client.prepare_upload_public("/tmp/pub/file.dat") + result = client.finalize_upload("up_pub_2", {"qh1": "tx1"}) + assert isinstance(result, FinalizeUploadResult) + assert result.address == "0xFINAL" + assert result.chunks_stored == 42