Skip to content
Draft
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
76 changes: 68 additions & 8 deletions antd-py/src/antd/_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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", ""),
)
3 changes: 2 additions & 1 deletion antd-py/src/antd/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 50 additions & 1 deletion antd-py/tests/test_rest_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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}"})
Expand Down Expand Up @@ -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