From ce19c38358188040eae3d1b9acbb8d066a1ba945 Mon Sep 17 00:00:00 2001 From: Nic Dorman Date: Thu, 21 May 2026 09:46:51 +0000 Subject: [PATCH] feat(antd-csharp)!: adopt PaymentMode enum + put/get convention (G2-rest) Same template as antd-go #116 / antd-rust #118 / G1 #117. New PaymentMode enum (Auto/Merkle/Single) with ToWire() extension; new result records DataPutResult, DataPutPublicResult, FilePutResult, FilePutPublicResult; PutResult kept for ChunkPut only; FileUploadResult removed. REST + gRPC method renames: DataPutPrivateAsync -> DataPutAsync (POST /v1/data) DataGetPrivateAsync -> DataGetAsync (POST /v1/data/get) FileUploadPublicAsync -> FilePutPublicAsync (POST /v1/files/public) FileDownloadPublicAsync -> FileGetPublicAsync (POST /v1/files/public/get) NEW: FilePutAsync (POST /v1/files), FileGetAsync (POST /v1/files/get) paymentMode: PaymentMode parameter added to DataPutAsync, DataPutPublicAsync, DataCostAsync, FilePutAsync, FilePutPublicAsync, FileCostAsync on both REST and gRPC transports (kwarg with PaymentMode.Auto default; was stringly typed Option on REST and hardcoded on gRPC). Proto stubs regenerate via grpc-tools at dotnet build time. dotnet build clean; dotnet test 32/32 pass. ant dev example all -l csharp passes against the renamed antd daemon. Co-Authored-By: Claude Opus 4.7 (1M context) --- antd-csharp/Antd.Sdk.Tests/TestRunner.cs | 2 +- antd-csharp/Antd.Sdk.Tests/UnitTests.cs | 18 +-- antd-csharp/Antd.Sdk/AntdGrpcClient.cs | 111 +++++++++----- antd-csharp/Antd.Sdk/AntdRestClient.cs | 187 +++++++++-------------- antd-csharp/Antd.Sdk/IAntdClient.cs | 22 ++- antd-csharp/Antd.Sdk/Models.cs | 101 +++++++----- antd-csharp/Examples/Program.cs | 16 +- 7 files changed, 243 insertions(+), 214 deletions(-) diff --git a/antd-csharp/Antd.Sdk.Tests/TestRunner.cs b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs index b19aedc..8ea1f03 100644 --- a/antd-csharp/Antd.Sdk.Tests/TestRunner.cs +++ b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs @@ -122,7 +122,7 @@ private async Task TestHealth() var testData = System.Text.Encoding.UTF8.GetBytes("hello from C# SDK!"); var result = await _client.DataPutPublicAsync(testData); dataAddr = result.Address; - Pass("Data put public", $"addr={result.Address[..16]}... cost={result.Cost}"); + Pass("Data put public", $"addr={result.Address[..16]}... chunks={result.ChunksStored} mode={result.PaymentModeUsed}"); } catch (Exception ex) { diff --git a/antd-csharp/Antd.Sdk.Tests/UnitTests.cs b/antd-csharp/Antd.Sdk.Tests/UnitTests.cs index e7991ee..929c7eb 100644 --- a/antd-csharp/Antd.Sdk.Tests/UnitTests.cs +++ b/antd-csharp/Antd.Sdk.Tests/UnitTests.cs @@ -230,7 +230,6 @@ public async Task DataPutPublicAsync_ReturnsCostAndAddress() var result = await _client.DataPutPublicAsync(Encoding.UTF8.GetBytes("hello")); - Assert.Equal("42", result.Cost); Assert.Equal("abc123def456", result.Address); } @@ -254,30 +253,29 @@ public async Task DataGetPublicAsync_ReturnsDecodedBytes() [Fact] public async Task DataPutPrivateAsync_ReturnsCostAndDataMap() { - _server.RouteOk("POST", "/v1/data/private", new + _server.RouteOk("POST", "/v1/data", new { cost = "99", data_map = "map_abc123" }); _server.Start(); - var result = await _client.DataPutPrivateAsync(Encoding.UTF8.GetBytes("secret")); + var result = await _client.DataPutAsync(Encoding.UTF8.GetBytes("secret")); - Assert.Equal("99", result.Cost); - Assert.Equal("map_abc123", result.Address); + Assert.Equal("map_abc123", result.DataMap); } [Fact] public async Task DataGetPrivateAsync_ReturnsDecodedBytes() { var original = Encoding.UTF8.GetBytes("private data content"); - _server.RouteOk("GET", "/v1/data/private", new + _server.RouteOk("POST", "/v1/data/get", new { data = Convert.ToBase64String(original) }); _server.Start(); - var result = await _client.DataGetPrivateAsync("some_data_map"); + var result = await _client.DataGetAsync("some_data_map"); Assert.Equal(original, result); } @@ -461,9 +459,9 @@ public async Task ErrorMapping_503_ThrowsServiceUnavailableException() // ── Files ── [Fact] - public async Task FileUploadPublicAsync_ReturnsFileUploadResult() + public async Task FileUploadPublicAsync_ReturnsFilePutPublicResult() { - _server.RouteOk("POST", "/v1/files/upload/public", new + _server.RouteOk("POST", "/v1/files/public", new { address = "file_addr_001", storage_cost_atto = "1000", @@ -473,7 +471,7 @@ public async Task FileUploadPublicAsync_ReturnsFileUploadResult() }); _server.Start(); - var result = await _client.FileUploadPublicAsync("/tmp/test.txt"); + var result = await _client.FilePutPublicAsync("/tmp/test.txt"); Assert.Equal("file_addr_001", result.Address); Assert.Equal("1000", result.StorageCostAtto); diff --git a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs index 98cd6d5..aed07b5 100644 --- a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs +++ b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs @@ -22,10 +22,6 @@ public AntdGrpcClient(string target = "http://localhost:50051") _files = new FileService.FileServiceClient(_channel); } - /// - /// Creates an AntdGrpcClient by reading the daemon.port file written by antd. - /// Falls back to the default target if the port file is not found. - /// public static AntdGrpcClient AutoDiscover() { var target = DaemonDiscovery.DiscoverGrpcTarget(); @@ -34,9 +30,15 @@ public static AntdGrpcClient AutoDiscover() public void Dispose() => _channel.Dispose(); + public ValueTask DisposeAsync() + { + _channel.Dispose(); + return ValueTask.CompletedTask; + } + private static AntdException Wrap(RpcException ex) => ExceptionMapping.FromGrpcStatus(ex); - // ── Health ── + // Health public async Task HealthAsync() { @@ -51,7 +53,7 @@ public async Task HealthAsync() } catch (RpcException) { - return new HealthStatus(true, "unknown"); // server responded — it's reachable + return new HealthStatus(true, "unknown"); } catch { @@ -59,10 +61,6 @@ public async Task HealthAsync() } } - /// - /// Convert a gRPC into a typed - /// . - /// internal static HealthStatus HealthStatusFromResp(HealthCheckResponse resp) => new( resp.Status == "ok", @@ -74,53 +72,65 @@ internal static HealthStatus HealthStatusFromResp(HealthCheckResponse resp) => resp.PaymentTokenAddress ?? "", resp.PaymentVaultAddress ?? ""); - // ── Data ── + // Data - public async Task DataPutPublicAsync(byte[] data, string? paymentMode = null) + public async Task DataPutAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto) { try { - var resp = await _data.PutPublicAsync(new PutPublicDataRequest { Data = ByteString.CopyFrom(data) }); - return new PutResult(resp.Cost.AttoTokens, resp.Address); + var resp = await _data.PutAsync(new PutDataRequest + { + Data = ByteString.CopyFrom(data), + PaymentMode = paymentMode.ToWire(), + }); + return new DataPutResult(resp.DataMap); } catch (RpcException ex) { throw Wrap(ex); } } - public async Task DataGetPublicAsync(string address) + public async Task DataGetAsync(string dataMap) { try { - var resp = await _data.GetPublicAsync(new GetPublicDataRequest { Address = address }); + var resp = await _data.GetAsync(new GetDataRequest { DataMap = dataMap }); return resp.Data.ToByteArray(); } catch (RpcException ex) { throw Wrap(ex); } } - public async Task DataPutPrivateAsync(byte[] data, string? paymentMode = null) + public async Task DataPutPublicAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto) { try { - var resp = await _data.PutPrivateAsync(new PutPrivateDataRequest { Data = ByteString.CopyFrom(data) }); - return new PutResult(resp.Cost.AttoTokens, resp.DataMap); + var resp = await _data.PutPublicAsync(new PutPublicDataRequest + { + Data = ByteString.CopyFrom(data), + PaymentMode = paymentMode.ToWire(), + }); + return new DataPutPublicResult(resp.Address); } catch (RpcException ex) { throw Wrap(ex); } } - public async Task DataGetPrivateAsync(string dataMap) + public async Task DataGetPublicAsync(string address) { try { - var resp = await _data.GetPrivateAsync(new GetPrivateDataRequest { DataMap = dataMap }); + var resp = await _data.GetPublicAsync(new GetPublicDataRequest { Address = address }); return resp.Data.ToByteArray(); } catch (RpcException ex) { throw Wrap(ex); } } - public async Task DataCostAsync(byte[] data) + public async Task DataCostAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto) { try { - var resp = await _data.GetCostAsync(new DataCostRequest { Data = ByteString.CopyFrom(data) }); + var resp = await _data.CostAsync(new DataCostRequest + { + Data = ByteString.CopyFrom(data), + PaymentMode = paymentMode.ToWire(), + }); return new UploadCostEstimate( resp.AttoTokens, resp.FileSize, resp.ChunkCount, resp.EstimatedGasCostWei, resp.PaymentMode); @@ -128,7 +138,7 @@ public async Task DataCostAsync(byte[] data) catch (RpcException ex) { throw Wrap(ex); } } - // ── Chunks ── + // Chunks public async Task ChunkPutAsync(byte[] data) { @@ -156,35 +166,67 @@ public Task PrepareChunkUploadAsync(byte[] data) public Task FinalizeChunkUploadAsync(string uploadId, IDictionary txHashes) => throw new NotSupportedException("FinalizeChunkUpload is not yet supported via gRPC"); - // ── Files ── + // Files - public async Task FileUploadPublicAsync(string path, string? paymentMode = null) + public async Task FilePutAsync(string path, PaymentMode paymentMode = PaymentMode.Auto) { try { - var resp = await _files.UploadPublicAsync(new UploadFileRequest { Path = path }); - return new FileUploadResult(resp.Address, resp.StorageCostAtto, resp.GasCostWei, resp.ChunksStored, resp.PaymentModeUsed); + var resp = await _files.PutAsync(new PutFileRequest + { + Path = path, + PaymentMode = paymentMode.ToWire(), + }); + return new FilePutResult( + resp.DataMap, resp.StorageCostAtto, resp.GasCostWei, + resp.ChunksStored, resp.PaymentModeUsed); } catch (RpcException ex) { throw Wrap(ex); } } - public async Task FileDownloadPublicAsync(string address, string destPath) + public async Task FileGetAsync(string dataMap, string destPath) { try { - await _files.DownloadPublicAsync(new DownloadPublicRequest { Address = address, DestPath = destPath }); + await _files.GetAsync(new GetFileRequest { DataMap = dataMap, DestPath = destPath }); } catch (RpcException ex) { throw Wrap(ex); } } - public async Task FileCostAsync(string path, bool isPublic = true) + public async Task FilePutPublicAsync(string path, PaymentMode paymentMode = PaymentMode.Auto) { try { - var resp = await _files.GetFileCostAsync(new Antd.V1.FileCostRequest + var resp = await _files.PutPublicAsync(new PutFileRequest + { + Path = path, + PaymentMode = paymentMode.ToWire(), + }); + return new FilePutPublicResult( + resp.Address, resp.StorageCostAtto, resp.GasCostWei, + resp.ChunksStored, resp.PaymentModeUsed); + } + catch (RpcException ex) { throw Wrap(ex); } + } + + public async Task FileGetPublicAsync(string address, string destPath) + { + try + { + await _files.GetPublicAsync(new GetFilePublicRequest { Address = address, DestPath = destPath }); + } + catch (RpcException ex) { throw Wrap(ex); } + } + + public async Task FileCostAsync(string path, bool isPublic = true, PaymentMode paymentMode = PaymentMode.Auto) + { + try + { + var resp = await _files.CostAsync(new Antd.V1.FileCostRequest { Path = path, IsPublic = isPublic, + PaymentMode = paymentMode.ToWire(), }); return new UploadCostEstimate( resp.AttoTokens, resp.FileSize, resp.ChunkCount, @@ -193,7 +235,7 @@ public async Task FileCostAsync(string path, bool isPublic = catch (RpcException ex) { throw Wrap(ex); } } - // ── Wallet (not yet available via gRPC) ── + // Wallet (not yet available via gRPC) public Task WalletAddressAsync() => throw new NotSupportedException("WalletAddress is not yet supported via gRPC"); @@ -204,7 +246,8 @@ public Task WalletBalanceAsync() public Task WalletApproveAsync() => throw new NotSupportedException("WalletApprove is not yet supported via gRPC"); - // ── External Signer (not yet available via gRPC) ── + + // External Signer (Two-Phase Upload) — not yet available via gRPC public Task PrepareUploadAsync(string path, string? visibility = null) => throw new NotSupportedException("PrepareUpload is not yet supported via gRPC"); diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index 4d136c0..71ba502 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -21,10 +21,6 @@ public AntdRestClient(string baseUrl = "http://localhost:8082", TimeSpan? timeou _http = new HttpClient { BaseAddress = new Uri(_baseUrl), Timeout = timeout ?? TimeSpan.FromSeconds(300) }; } - /// - /// Creates an AntdRestClient by reading the daemon.port file written by antd. - /// Falls back to the default base URL if the port file is not found. - /// public static AntdRestClient AutoDiscover(TimeSpan? timeout = null) { var url = DaemonDiscovery.DiscoverDaemonUrl(); @@ -33,7 +29,13 @@ public static AntdRestClient AutoDiscover(TimeSpan? timeout = null) public void Dispose() => _http.Dispose(); - // ── Helpers ── + public ValueTask DisposeAsync() + { + _http.Dispose(); + return ValueTask.CompletedTask; + } + + // Helpers private async Task GetJsonAsync(string path) { @@ -55,15 +57,6 @@ private async Task PostJsonNoResultAsync(string path, object body) await EnsureSuccessAsync(resp); } - private async Task HeadExistsAsync(string path) - { - var req = new HttpRequestMessage(HttpMethod.Head, path); - var resp = await _http.SendAsync(req); - if (resp.StatusCode == HttpStatusCode.NotFound) return false; - await EnsureSuccessAsync(resp); - return true; - } - private static async Task EnsureSuccessAsync(HttpResponseMessage resp) { if (resp.IsSuccessStatusCode) return; @@ -71,7 +64,7 @@ private static async Task EnsureSuccessAsync(HttpResponseMessage resp) throw ExceptionMapping.FromHttpStatus(resp.StatusCode, body); } - // ── Health ── + // Health public async Task HealthAsync() { @@ -89,11 +82,6 @@ public async Task HealthAsync() } } - /// - /// Convert a parsed into a typed - /// . Diagnostic fields default to empty / 0 - /// when talking to a pre-0.4.0 daemon that omits them. - /// internal static HealthStatus HealthStatusFromDto(HealthResponseDto? dto) { if (dto is null) return new HealthStatus(false, "unknown"); @@ -108,49 +96,46 @@ internal static HealthStatus HealthStatusFromDto(HealthResponseDto? dto) dto.PaymentVaultAddress ?? ""); } - // ── Data ── + // Data - public async Task DataPutPublicAsync(byte[] data, string? paymentMode = null) + public async Task DataPutAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto) { - object body = paymentMode != null - ? new { data = Convert.ToBase64String(data), payment_mode = paymentMode } - : new { data = Convert.ToBase64String(data) }; - var resp = await PostJsonAsync("/v1/data/public", body); - return new PutResult(resp.Cost, resp.Address); + var body = new { data = Convert.ToBase64String(data), payment_mode = paymentMode.ToWire() }; + var resp = await PostJsonAsync("/v1/data", body); + return new DataPutResult(resp.DataMap, resp.ChunksStored, resp.PaymentModeUsed); } - public async Task DataGetPublicAsync(string address) + public async Task DataGetAsync(string dataMap) { - var resp = await GetJsonAsync($"/v1/data/public/{address}"); + var resp = await PostJsonAsync("/v1/data/get", new { data_map = dataMap }); return Convert.FromBase64String(resp.Data); } - public async Task DataPutPrivateAsync(byte[] data, string? paymentMode = null) + public async Task DataPutPublicAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto) { - object body = paymentMode != null - ? new { data = Convert.ToBase64String(data), payment_mode = paymentMode } - : new { data = Convert.ToBase64String(data) }; - var resp = await PostJsonAsync("/v1/data/private", body); - return new PutResult(resp.Cost, resp.DataMap); + var body = new { data = Convert.ToBase64String(data), payment_mode = paymentMode.ToWire() }; + var resp = await PostJsonAsync("/v1/data/public", body); + return new DataPutPublicResult(resp.Address, resp.ChunksStored, resp.PaymentModeUsed); } - public async Task DataGetPrivateAsync(string dataMap) + public async Task DataGetPublicAsync(string address) { - var resp = await GetJsonAsync($"/v1/data/private?data_map={Uri.EscapeDataString(dataMap)}"); + var resp = await GetJsonAsync($"/v1/data/public/{address}"); return Convert.FromBase64String(resp.Data); } - public async Task DataCostAsync(byte[] data) + public async Task DataCostAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto) { - var resp = await PostJsonAsync("/v1/data/cost", new { data = Convert.ToBase64String(data) }); + var body = new { data = Convert.ToBase64String(data), payment_mode = paymentMode.ToWire() }; + var resp = await PostJsonAsync("/v1/data/cost", body); return new UploadCostEstimate(resp.Cost, resp.FileSize, resp.ChunkCount, resp.EstimatedGasCostWei, resp.PaymentMode); } - // ── Chunks ── + // Chunks public async Task ChunkPutAsync(byte[] data) { - var resp = await PostJsonAsync("/v1/chunks", new { data = Convert.ToBase64String(data) }); + var resp = await PostJsonAsync("/v1/chunks", new { data = Convert.ToBase64String(data) }); return new PutResult(resp.Cost, resp.Address); } @@ -160,18 +145,6 @@ public async Task ChunkGetAsync(string address) return Convert.FromBase64String(resp.Data); } - /// - /// Prepares a single chunk for external-signer publish via - /// POST /v1/chunks/prepare. - /// - /// The daemon quotes the close group for the supplied bytes and returns - /// either = true with - /// set (no payment needed), or a - /// wave-batch payment intent. Mirrors but - /// routes payment through an external signer instead of the daemon wallet. - /// - /// Requires antd >= 0.7.0. - /// public async Task PrepareChunkUploadAsync(byte[] data) { var resp = await PostJsonAsync("/v1/chunks/prepare", @@ -191,13 +164,6 @@ public async Task PrepareChunkUploadAsync(byte[] data) resp.RpcUrl ?? ""); } - /// - /// Submits a prepared chunk to the network after the external signer has - /// paid via POST /v1/chunks/finalize. Returns the hex address of - /// the stored chunk (matches ). - /// - /// Requires antd >= 0.7.0. - /// public async Task FinalizeChunkUploadAsync(string uploadId, IDictionary txHashes) { var resp = await PostJsonAsync("/v1/chunks/finalize", @@ -205,30 +171,40 @@ public async Task FinalizeChunkUploadAsync(string uploadId, IDictionary< return resp.Address ?? ""; } - // ── Files ── + // Files - public async Task FileUploadPublicAsync(string path, string? paymentMode = null) + public async Task FilePutAsync(string path, PaymentMode paymentMode = PaymentMode.Auto) { - object body = paymentMode != null - ? new { path, payment_mode = paymentMode } - : (object)new { path }; - var resp = await PostJsonAsync("/v1/files/upload/public", body); - return new FileUploadResult(resp.Address, resp.StorageCostAtto, resp.GasCostWei, resp.ChunksStored, resp.PaymentModeUsed); + var body = new { path, payment_mode = paymentMode.ToWire() }; + var resp = await PostJsonAsync("/v1/files", body); + return new FilePutResult(resp.DataMap, resp.StorageCostAtto, resp.GasCostWei, resp.ChunksStored, resp.PaymentModeUsed); + } + + public async Task FileGetAsync(string dataMap, string destPath) + { + await PostJsonNoResultAsync("/v1/files/get", new { data_map = dataMap, dest_path = destPath }); + } + + public async Task FilePutPublicAsync(string path, PaymentMode paymentMode = PaymentMode.Auto) + { + var body = new { path, payment_mode = paymentMode.ToWire() }; + var resp = await PostJsonAsync("/v1/files/public", body); + return new FilePutPublicResult(resp.Address, resp.StorageCostAtto, resp.GasCostWei, resp.ChunksStored, resp.PaymentModeUsed); } - public async Task FileDownloadPublicAsync(string address, string destPath) + public async Task FileGetPublicAsync(string address, string destPath) { - await PostJsonNoResultAsync("/v1/files/download/public", new { address, dest_path = destPath }); + await PostJsonNoResultAsync("/v1/files/public/get", new { address, dest_path = destPath }); } - public async Task FileCostAsync(string path, bool isPublic = true) + public async Task FileCostAsync(string path, bool isPublic = true, PaymentMode paymentMode = PaymentMode.Auto) { - var body = new { path, is_public = isPublic }; + var body = new { path, is_public = isPublic, payment_mode = paymentMode.ToWire() }; var resp = await PostJsonAsync("/v1/files/cost", body); return new UploadCostEstimate(resp.Cost, resp.FileSize, resp.ChunkCount, resp.EstimatedGasCostWei, resp.PaymentMode); } - // ── Wallet ── + // Wallet public async Task WalletAddressAsync() { @@ -242,28 +218,14 @@ public async Task WalletBalanceAsync() return new WalletBalance(resp.Balance, resp.GasBalance); } - /// - /// Approves the wallet to spend tokens on payment contracts (one-time operation). - /// public async Task WalletApproveAsync() { var resp = await PostJsonAsync("/v1/wallet/approve", new { }); return resp.Approved; } - // ── External Signer (Two-Phase Upload) ── - - /// - /// Prepares a file upload for external signing. - /// - /// Path to the file to upload. - /// - /// Pass "public" to bundle the DataMap chunk into the same - /// external-signer payment batch — the resulting - /// is the shareable - /// retrieval handle. "private" or null preserves the - /// pre-public daemon wire shape (private-only). - /// + // External Signer (Two-Phase Upload) + public async Task PrepareUploadAsync(string path, string? visibility = null) { object body = visibility != null @@ -273,27 +235,9 @@ public async Task PrepareUploadAsync(string path, string? v return MapPrepareUpload(resp); } - /// - /// Convenience wrapper: prepares a public file upload for external - /// signing. Equivalent to with - /// visibility="public". - /// - /// Requires antd >= 0.6.1. - /// public Task PrepareUploadPublicAsync(string path) => PrepareUploadAsync(path, visibility: "public"); - /// - /// Prepares a data upload for external signing. - /// Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. - /// - /// Raw bytes to upload. - /// - /// Pass "public" to request the public flow; note the daemon - /// currently returns 501 for visibility="public" on the data path - /// until upstream data_prepare_upload_with_visibility lands. Use - /// with a file path until then. - /// public async Task PrepareDataUploadAsync(byte[] data, string? visibility = null) { object body = visibility != null @@ -303,9 +247,6 @@ public async Task PrepareDataUploadAsync(byte[] data, strin return MapPrepareUpload(resp); } - /// - /// Finalizes an upload after an external signer has submitted payment transactions. - /// public async Task FinalizeUploadAsync(string uploadId, Dictionary txHashes) { var resp = await PostJsonAsync("/v1/upload/finalize", new { upload_id = uploadId, tx_hashes = txHashes }); @@ -316,9 +257,6 @@ public async Task FinalizeUploadAsync(string uploadId, Dic resp.DataMapAddress ?? ""); } - /// - /// Finalizes a merkle batch upload by selecting a winner pool. - /// public async Task FinalizeMerkleUploadAsync(string uploadId, string winnerPoolHash) { var resp = await PostJsonAsync("/v1/upload/finalize", @@ -345,7 +283,7 @@ private static PrepareUploadResult MapPrepareUpload(PrepareUploadDto resp) MerklePaymentTimestamp: resp.MerklePaymentTimestamp); } - // ── Internal DTOs for JSON deserialization ── + // Internal DTOs for JSON deserialization internal sealed record HealthResponseDto( [property: JsonPropertyName("status")] string Status, @@ -358,19 +296,32 @@ internal sealed record HealthResponseDto( [property: JsonPropertyName("payment_vault_address")] string? PaymentVaultAddress = null); private sealed record DataPutPublicDto( - [property: JsonPropertyName("cost")] string Cost, - [property: JsonPropertyName("address")] string Address); + [property: JsonPropertyName("address")] string Address, + [property: JsonPropertyName("chunks_stored")] ulong ChunksStored = 0, + [property: JsonPropertyName("payment_mode_used")] string PaymentModeUsed = ""); + + private sealed record DataPutDto( + [property: JsonPropertyName("data_map")] string DataMap, + [property: JsonPropertyName("chunks_stored")] ulong ChunksStored = 0, + [property: JsonPropertyName("payment_mode_used")] string PaymentModeUsed = ""); + + private sealed record FilePutDto( + [property: JsonPropertyName("data_map")] string DataMap, + [property: JsonPropertyName("storage_cost_atto")] string StorageCostAtto, + [property: JsonPropertyName("gas_cost_wei")] string GasCostWei, + [property: JsonPropertyName("chunks_stored")] ulong ChunksStored, + [property: JsonPropertyName("payment_mode_used")] string PaymentModeUsed); - private sealed record FileUploadPublicDto( + private sealed record FilePutPublicDto( [property: JsonPropertyName("address")] string Address, [property: JsonPropertyName("storage_cost_atto")] string StorageCostAtto, [property: JsonPropertyName("gas_cost_wei")] string GasCostWei, [property: JsonPropertyName("chunks_stored")] ulong ChunksStored, [property: JsonPropertyName("payment_mode_used")] string PaymentModeUsed); - private sealed record DataPutPrivateDto( + private sealed record ChunkPutDto( [property: JsonPropertyName("cost")] string Cost, - [property: JsonPropertyName("data_map")] string DataMap); + [property: JsonPropertyName("address")] string Address); private sealed record DataGetDto( [property: JsonPropertyName("data")] string Data); diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index 65a4c37..7798467 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -1,16 +1,20 @@ namespace Antd.Sdk; -public interface IAntdClient : IDisposable +/// +/// Common interface for both REST () and gRPC +/// () antd clients. +/// +public interface IAntdClient : IDisposable, IAsyncDisposable { // Health Task HealthAsync(); // Data - Task DataPutPublicAsync(byte[] data, string? paymentMode = null); + Task DataPutPublicAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto); Task DataGetPublicAsync(string address); - Task DataPutPrivateAsync(byte[] data, string? paymentMode = null); - Task DataGetPrivateAsync(string dataMap); - Task DataCostAsync(byte[] data); + Task DataPutAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto); + Task DataGetAsync(string dataMap); + Task DataCostAsync(byte[] data, PaymentMode paymentMode = PaymentMode.Auto); // Chunks Task ChunkPutAsync(byte[] data); @@ -19,9 +23,11 @@ public interface IAntdClient : IDisposable Task FinalizeChunkUploadAsync(string uploadId, IDictionary txHashes); // Files - Task FileUploadPublicAsync(string path, string? paymentMode = null); - Task FileDownloadPublicAsync(string address, string destPath); - Task FileCostAsync(string path, bool isPublic = true); + Task FilePutAsync(string path, PaymentMode paymentMode = PaymentMode.Auto); + Task FileGetAsync(string dataMap, string destPath); + Task FilePutPublicAsync(string path, PaymentMode paymentMode = PaymentMode.Auto); + Task FileGetPublicAsync(string address, string destPath); + Task FileCostAsync(string path, bool isPublic = true, PaymentMode paymentMode = PaymentMode.Auto); // Wallet Task WalletAddressAsync(); diff --git a/antd-csharp/Antd.Sdk/Models.cs b/antd-csharp/Antd.Sdk/Models.cs index ac17897..1539955 100644 --- a/antd-csharp/Antd.Sdk/Models.cs +++ b/antd-csharp/Antd.Sdk/Models.cs @@ -1,5 +1,31 @@ namespace Antd.Sdk; +/// +/// Payment-batching strategy for uploads. +/// +/// - — server picks (merkle for 64+ chunks, single otherwise). +/// - — force merkle-batch (saves gas, min 2 chunks). +/// - — force per-chunk payments. +/// +public enum PaymentMode +{ + Auto, + Merkle, + Single, +} + +public static class PaymentModeExtensions +{ + /// Serialize a to the wire string the daemon expects. + public static string ToWire(this PaymentMode mode) => mode switch + { + PaymentMode.Auto => "auto", + PaymentMode.Merkle => "merkle", + PaymentMode.Single => "single", + _ => "auto", + }; +} + /// /// Health check result from the antd daemon. /// @@ -19,11 +45,44 @@ public sealed record HealthStatus( string PaymentTokenAddress = "", string PaymentVaultAddress = ""); -/// Result of a put/create operation that stores data on the network. +/// Result of a single-chunk put (used by ChunkPutAsync). public sealed record PutResult(string Cost, string Address); -/// Result of a public file or directory upload. -public sealed record FileUploadResult( +/// +/// Result of a private data put. The DataMap is returned to the caller; +/// it is NOT stored on-network. REST populates ChunksStored and PaymentModeUsed; +/// the gRPC transport currently leaves them empty. +/// +public sealed record DataPutResult( + string DataMap, + ulong ChunksStored = 0, + string PaymentModeUsed = ""); + +/// +/// Result of a public data put. The DataMap is stored on-network as an extra +/// chunk; Address is the shareable retrieval handle. +/// +public sealed record DataPutPublicResult( + string Address, + ulong ChunksStored = 0, + string PaymentModeUsed = ""); + +/// +/// Result of a private file upload. The DataMap is returned to the caller; +/// it is NOT stored on-network. +/// +public sealed record FilePutResult( + string DataMap, + string StorageCostAtto, + string GasCostWei, + ulong ChunksStored, + string PaymentModeUsed); + +/// +/// Result of a public file upload. The DataMap is stored on-network as an +/// extra chunk; Address is the shareable retrieval handle. +/// +public sealed record FilePutPublicResult( string Address, string StorageCostAtto, string GasCostWei, @@ -58,44 +117,21 @@ public sealed record PrepareUploadResult( List? PoolCommitments = null, long? MerklePaymentTimestamp = null); -/// -/// Result of finalizing an externally-signed wave-batch upload. -/// -/// is the hex-encoded serialized DataMap and is always -/// populated. is set only when prepare was called -/// with visibility="public" — the DataMap chunk was bundled into the -/// same external-signer payment batch and stored on-network, and the address -/// is the shareable retrieval handle. Pre-0.6.1 daemons that don't emit this -/// field leave it as "". -/// +/// Result of finalizing an externally-signed wave-batch upload. public sealed record FinalizeUploadResult( string Address, long ChunksStored, string DataMap = "", string DataMapAddress = ""); -/// -/// Result of finalizing a merkle batch upload. -/// -/// See for the meaning of -/// and . -/// +/// Result of finalizing a merkle batch upload. public sealed record FinalizeMerkleUploadResult( string Address, long ChunksStored, string DataMap = "", string DataMapAddress = ""); -/// -/// Result of preparing a single-chunk external-signer publish via -/// POST /v1/chunks/prepare. -/// -/// When is true the chunk is already on -/// the network — only and -/// are meaningful, and no finalize call is needed. Otherwise the wave-batch -/// payment fields describe what the external signer must submit before -/// calling FinalizeChunkUploadAsync. -/// +/// Result of preparing a single-chunk external-signer publish. public sealed record PrepareChunkResult( string Address, bool AlreadyStored = false, @@ -107,12 +143,7 @@ public sealed record PrepareChunkResult( string PaymentTokenAddress = "", string RpcUrl = ""); -/// -/// Pre-upload cost breakdown returned by DataCostAsync and FileCostAsync. -/// -/// The server samples up to 5 chunk addresses and extrapolates the storage cost. -/// Gas is an advisory heuristic, not a live gas-oracle query. -/// +/// Pre-upload cost breakdown returned by DataCostAsync and FileCostAsync. public sealed record UploadCostEstimate( string Cost, ulong FileSize, diff --git a/antd-csharp/Examples/Program.cs b/antd-csharp/Examples/Program.cs index d0a6574..21fee4d 100644 --- a/antd-csharp/Examples/Program.cs +++ b/antd-csharp/Examples/Program.cs @@ -65,7 +65,7 @@ static async Task Example02_Data() // Store public data var result = await client.DataPutPublicAsync(payload); Console.WriteLine($"Stored at address: {result.Address}"); - Console.WriteLine($"Actual cost: {result.Cost} atto tokens"); + Console.WriteLine($"Chunks: {result.ChunksStored}, mode: {result.PaymentModeUsed}"); // Retrieve var data = await client.DataGetPublicAsync(result.Address); @@ -116,14 +116,14 @@ static async Task Example04_Files() Console.WriteLine($"File upload cost estimate: {cost} atto tokens"); // Upload - var result = await client.FileUploadPublicAsync(srcPath); + var result = await client.FilePutPublicAsync(srcPath); Console.WriteLine($"File uploaded to: {result.Address}"); Console.WriteLine($"Storage cost: {result.StorageCostAtto} atto, gas: {result.GasCostWei} wei"); Console.WriteLine($"Chunks stored: {result.ChunksStored}, payment mode: {result.PaymentModeUsed}"); // Download to new location var destPath = srcPath + ".downloaded"; - await client.FileDownloadPublicAsync(result.Address, destPath); + await client.FileGetPublicAsync(result.Address, destPath); Console.WriteLine($"Downloaded to: {destPath}"); var content = await File.ReadAllTextAsync(destPath); @@ -147,13 +147,13 @@ static async Task Example06_PrivateData() var secretMessage = Encoding.UTF8.GetBytes("This message is encrypted on the network"); // Store private data - var result = await client.DataPutPrivateAsync(secretMessage); - var dataMap = result.Address; // for private data, address holds the data map + var result = await client.DataPutAsync(secretMessage); + var dataMap = result.DataMap; Console.WriteLine($"Data map: {dataMap}"); - Console.WriteLine($"Cost: {result.Cost} atto tokens"); + Console.WriteLine($"Chunks: {result.ChunksStored}, mode: {result.PaymentModeUsed}"); // Retrieve and decrypt - var retrieved = await client.DataGetPrivateAsync(dataMap); + var retrieved = await client.DataGetAsync(dataMap); Console.WriteLine($"Decrypted: {Encoding.UTF8.GetString(retrieved)}"); if (!retrieved.SequenceEqual(secretMessage)) @@ -203,7 +203,7 @@ static async Task Example07_ExternalSigner() $"chunks_stored={fileFin.ChunksStored}"); var dstPath = srcPath + ".downloaded"; - await client.FileDownloadPublicAsync(fileFin.DataMapAddress, dstPath); + await client.FileGetPublicAsync(fileFin.DataMapAddress, dstPath); var downloaded = await File.ReadAllBytesAsync(dstPath); if (!downloaded.SequenceEqual(fileContent)) throw new Exception("file round-trip mismatch");