From d4db47e9859a71a92d961cb8eb45d14a721135ee Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Wed, 25 Mar 2026 11:09:30 +0000 Subject: [PATCH 01/20] Add port discovery for automatic daemon detection across all SDKs antd now writes a daemon.port file on startup containing the REST and gRPC ports, enabling all SDK clients to auto-discover the daemon without hardcoded URLs. This supports managed mode where antd is spawned with --rest-port 0 for OS-assigned ports. Changes: - antd (Rust): port file lifecycle (atomic write/cleanup), --rest-port and --grpc-port CLI flags, pre-bound listeners for both servers - All 15 SDKs: discover module + auto-discover constructors for REST and gRPC clients - Default REST port corrected from 8080 to 8082 across all SDKs - ant-dev CLI: status/wallet/start commands use port-file discovery - Docs: README, llms.txt, llms-full.txt, skill.md updated Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 33 +++- ant-dev/src/ant_dev/cmd_start.py | 18 ++- ant-dev/src/ant_dev/cmd_status.py | 16 +- ant-dev/src/ant_dev/cmd_wallet.py | 16 +- antd-cpp/include/antd/client.hpp | 11 +- antd-cpp/include/antd/discover.hpp | 17 ++ antd-cpp/include/antd/grpc_client.hpp | 9 ++ antd-cpp/src/discover.cpp | 84 ++++++++++ antd-csharp/Antd.Sdk/AntdGrpcClient.cs | 10 ++ antd-csharp/Antd.Sdk/AntdRestClient.cs | 12 +- antd-csharp/Antd.Sdk/DaemonDiscovery.cs | 97 +++++++++++ antd-dart/lib/antd.dart | 1 + antd-dart/lib/src/client.dart | 21 ++- antd-dart/lib/src/discover.dart | 88 ++++++++++ antd-dart/lib/src/grpc_client.dart | 15 ++ antd-elixir/lib/antd.ex | 2 +- antd-elixir/lib/antd/client.ex | 30 +++- antd-elixir/lib/antd/discover.ex | 111 +++++++++++++ antd-elixir/lib/antd/grpc_client.ex | 24 +++ antd-go/README.md | 5 +- antd-go/client.go | 13 +- antd-go/discover.go | 101 ++++++++++++ antd-go/discover_test.go | 150 ++++++++++++++++++ antd-go/grpc_client.go | 12 ++ .../java/com/autonomi/antd/AntdClient.java | 16 +- .../com/autonomi/antd/DaemonDiscovery.java | 138 ++++++++++++++++ .../com/autonomi/antd/GrpcAntdClient.java | 14 ++ antd-js/src/discover.ts | 85 ++++++++++ antd-js/src/index.ts | 2 + antd-js/src/rest-client.ts | 22 ++- .../kotlin/com/autonomi/sdk/AntdGrpcClient.kt | 11 ++ .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 13 +- .../com/autonomi/sdk/DaemonDiscovery.kt | 73 +++++++++ antd-lua/src/antd/client.lua | 17 +- antd-lua/src/antd/discover.lua | 96 +++++++++++ antd-lua/src/antd/init.lua | 17 +- antd-mcp/README.md | 11 +- antd-mcp/src/antd_mcp/discover.py | 102 ++++++++++++ antd-mcp/src/antd_mcp/server.py | 6 +- antd-php/src/AntdClient.php | 20 ++- antd-php/src/DaemonDiscovery.php | 129 +++++++++++++++ antd-py/src/antd/__init__.py | 8 +- antd-py/src/antd/_discover.py | 93 +++++++++++ antd-py/src/antd/_grpc.py | 30 ++++ antd-py/src/antd/_rest.py | 34 +++- antd-py/tests/test_discover.py | 128 +++++++++++++++ antd-ruby/lib/antd.rb | 1 + antd-ruby/lib/antd/client.rb | 15 +- antd-ruby/lib/antd/discover.rb | 98 ++++++++++++ antd-ruby/lib/antd/grpc_client.rb | 12 ++ antd-rust/src/client.rs | 19 ++- antd-rust/src/discover.rs | 112 +++++++++++++ antd-rust/src/grpc_client.rs | 9 ++ antd-rust/src/lib.rs | 2 + .../Sources/AntdSdk/AntdRestClient.swift | 2 +- .../Sources/AntdSdk/DaemonDiscovery.swift | 93 +++++++++++ antd-zig/src/antd.zig | 16 +- antd-zig/src/discover.zig | 96 +++++++++++ antd/Cargo.lock | 103 ++++++------ antd/Cargo.toml | 3 +- antd/src/config.rs | 34 ++++ antd/src/grpc/mod.rs | 7 +- antd/src/main.rs | 46 ++++-- antd/src/port_file.rs | 65 ++++++++ llms-full.txt | 128 +++++++++------ llms.txt | 22 ++- skill.md | 6 +- 67 files changed, 2690 insertions(+), 160 deletions(-) create mode 100644 antd-cpp/include/antd/discover.hpp create mode 100644 antd-cpp/src/discover.cpp create mode 100644 antd-csharp/Antd.Sdk/DaemonDiscovery.cs create mode 100644 antd-dart/lib/src/discover.dart create mode 100644 antd-elixir/lib/antd/discover.ex create mode 100644 antd-go/discover.go create mode 100644 antd-go/discover_test.go create mode 100644 antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java create mode 100644 antd-js/src/discover.ts create mode 100644 antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt create mode 100644 antd-lua/src/antd/discover.lua create mode 100644 antd-mcp/src/antd_mcp/discover.py create mode 100644 antd-php/src/DaemonDiscovery.php create mode 100644 antd-py/src/antd/_discover.py create mode 100644 antd-py/tests/test_discover.py create mode 100644 antd-ruby/lib/antd/discover.rb create mode 100644 antd-rust/src/discover.rs create mode 100644 antd-swift/Sources/AntdSdk/DaemonDiscovery.swift create mode 100644 antd-zig/src/discover.zig create mode 100644 antd/src/port_file.rs diff --git a/README.md b/README.md index d101d85..9a054a1 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,37 @@ A developer-friendly SDK for the [Autonomi](https://autonomi.com) decentralized **antd** is a local gateway daemon (written in Rust) that exposes the Autonomi network via REST and gRPC APIs. The SDKs and MCP server talk to antd — your application code never touches the network directly. +### Port Discovery + +All SDKs support automatic daemon discovery. When antd starts, it writes a `daemon.port` file containing the REST and gRPC ports to a platform-specific location: + +| Platform | Path | +|----------|------| +| Windows | `%APPDATA%\ant\daemon.port` | +| Linux | `~/.local/share/ant/daemon.port` (or `$XDG_DATA_HOME/ant/`) | +| macOS | `~/Library/Application Support/ant/daemon.port` | + +Every SDK provides an auto-discover constructor that reads this file and connects automatically: + +```python +# Python +client, url = RestClient.auto_discover() +``` + +```go +// Go +client, url := antd.NewClientAutoDiscover() +``` + +```typescript +// TypeScript +const { client, url } = RestClient.autoDiscover(); +``` + +This is especially useful in managed mode, where a parent process (e.g. indelible) spawns antd with `--rest-port 0` to let the OS assign a free port. The SDK discovers the actual port via the port file without any hardcoded configuration. + +If no port file is found, all SDKs fall back to the default REST endpoint (`http://localhost:8082`) or gRPC target (`localhost:50051`). + ## Components ### Infrastructure @@ -216,7 +247,7 @@ import ( ) func main() { - client := antd.NewClient(antd.DefaultBaseURL) + client, _ := antd.NewClientAutoDiscover() ctx := context.Background() health, err := client.Health(ctx) diff --git a/ant-dev/src/ant_dev/cmd_start.py b/ant-dev/src/ant_dev/cmd_start.py index 5c1a78e..e09e9d1 100644 --- a/ant-dev/src/ant_dev/cmd_start.py +++ b/ant-dev/src/ant_dev/cmd_start.py @@ -23,6 +23,19 @@ ) from .process import start_process, wait_for_http +DEFAULT_REST_URL = "http://localhost:8082" + +def _discover_rest_url() -> str: + """Try to discover antd REST URL via port file, fall back to default.""" + try: + from antd import discover_daemon_url + url = discover_daemon_url() + if url: + return url + except ImportError: + pass + return DEFAULT_REST_URL + # ── ANSI colours (disabled on Windows without VT support) ── @@ -128,15 +141,16 @@ def run(args) -> None: print() if ready: + rest_url = _discover_rest_url() print(green("=== Ready! ===")) print() - print(white(" REST: http://localhost:8082")) + print(white(f" REST: {rest_url}")) print(white(" gRPC: localhost:50051")) if wallet_key: print(white(f" Key: {wallet_key[:10]}...")) print() print(gray("Quick test:")) - print(gray(" curl http://localhost:8082/health")) + print(gray(f" curl {rest_url}/health")) print() print(gray("To tear down:")) print(gray(" ant dev stop")) diff --git a/ant-dev/src/ant_dev/cmd_status.py b/ant-dev/src/ant_dev/cmd_status.py index 96d6953..17cf856 100644 --- a/ant-dev/src/ant_dev/cmd_status.py +++ b/ant-dev/src/ant_dev/cmd_status.py @@ -10,6 +10,19 @@ from .env import is_windows, load_state from .process import is_alive +DEFAULT_REST_URL = "http://localhost:8082" + +def _discover_rest_url() -> str: + """Try to discover antd REST URL via port file, fall back to default.""" + try: + from antd import discover_daemon_url + url = discover_daemon_url() + if url: + return url + except ImportError: + pass + return DEFAULT_REST_URL + def _color(code: str, text: str) -> str: if is_windows() and "WT_SESSION" not in os.environ: @@ -48,7 +61,8 @@ def run(args) -> None: # Health check print() try: - r = httpx.get("http://localhost:8082/health", timeout=5) + rest_url = _discover_rest_url() + r = httpx.get(f"{rest_url}/health", timeout=5) data = r.json() ok = data.get("status") == "ok" or data.get("ok", False) network = data.get("network", "unknown") diff --git a/ant-dev/src/ant_dev/cmd_wallet.py b/ant-dev/src/ant_dev/cmd_wallet.py index caa381e..e28ae2d 100644 --- a/ant-dev/src/ant_dev/cmd_wallet.py +++ b/ant-dev/src/ant_dev/cmd_wallet.py @@ -8,6 +8,19 @@ from .env import load_state +DEFAULT_REST_URL = "http://localhost:8082" + +def _discover_rest_url() -> str: + """Try to discover antd REST URL via port file, fall back to default.""" + try: + from antd import discover_daemon_url + url = discover_daemon_url() + if url: + return url + except ImportError: + pass + return DEFAULT_REST_URL + def run(args) -> None: state = load_state() @@ -30,7 +43,8 @@ def run(args) -> None: # On local testnet the EVM testnet already funds this key. # Verify via health check. try: - r = httpx.get("http://localhost:8082/health", timeout=5) + rest_url = _discover_rest_url() + r = httpx.get(f"{rest_url}/health", timeout=5) data = r.json() if data.get("status") == "ok" or data.get("ok", False): print("Wallet is already funded on local testnet.") diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index 3f4a0cd..a759289 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -7,13 +7,14 @@ #include #include +#include "discover.hpp" #include "errors.hpp" #include "models.hpp" namespace antd { /// Default address of the antd daemon. -inline constexpr const char* kDefaultBaseURL = "http://localhost:8080"; +inline constexpr const char* kDefaultBaseURL = "http://localhost:8082"; /// Default request timeout in seconds (5 minutes). inline constexpr int kDefaultTimeoutSeconds = 300; @@ -34,6 +35,14 @@ class Client { Client(Client&&) noexcept; Client& operator=(Client&&) noexcept; + /// Create a client by auto-discovering the daemon port from the + /// daemon.port file. Falls back to kDefaultBaseURL if not found. + static Client auto_discover(int timeout_seconds = kDefaultTimeoutSeconds) { + auto url = discover_daemon_url(); + if (url.empty()) url = kDefaultBaseURL; + return Client(url, timeout_seconds); + } + // --- Health --- /// Check daemon status. diff --git a/antd-cpp/include/antd/discover.hpp b/antd-cpp/include/antd/discover.hpp new file mode 100644 index 0000000..6ac1695 --- /dev/null +++ b/antd-cpp/include/antd/discover.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace antd { + +/// Read the daemon.port file written by antd on startup and return the REST +/// base URL (e.g. "http://127.0.0.1:8082"). +/// Returns an empty string if the port file is not found or unreadable. +std::string discover_daemon_url(); + +/// Read the daemon.port file written by antd on startup and return the gRPC +/// target (e.g. "127.0.0.1:50051"). +/// Returns an empty string if the port file has no gRPC line or is unreadable. +std::string discover_grpc_target(); + +} // namespace antd diff --git a/antd-cpp/include/antd/grpc_client.hpp b/antd-cpp/include/antd/grpc_client.hpp index e82489c..98194a0 100644 --- a/antd-cpp/include/antd/grpc_client.hpp +++ b/antd-cpp/include/antd/grpc_client.hpp @@ -6,6 +6,7 @@ #include #include +#include "discover.hpp" #include "errors.hpp" #include "models.hpp" @@ -38,6 +39,14 @@ class GrpcClient { GrpcClient(GrpcClient&&) noexcept; GrpcClient& operator=(GrpcClient&&) noexcept; + /// Create a client by auto-discovering the daemon gRPC port from the + /// daemon.port file. Falls back to kDefaultGrpcTarget if not found. + static GrpcClient auto_discover() { + auto target = discover_grpc_target(); + if (target.empty()) target = kDefaultGrpcTarget; + return GrpcClient(target); + } + // --- Health --- /// Check daemon status. diff --git a/antd-cpp/src/discover.cpp b/antd-cpp/src/discover.cpp new file mode 100644 index 0000000..174a84a --- /dev/null +++ b/antd-cpp/src/discover.cpp @@ -0,0 +1,84 @@ +#include "antd/discover.hpp" + +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace antd { +namespace { + +constexpr const char* kPortFileName = "daemon.port"; +constexpr const char* kDataDirName = "ant"; + +/// Parse a port number (1-65535) from a string. Returns 0 on failure. +uint16_t parse_port(const std::string& s) { + try { + unsigned long n = std::stoul(s); + if (n == 0 || n > 65535) return 0; + return static_cast(n); + } catch (...) { + return 0; + } +} + +/// Return the platform-specific data directory for ant. +/// - Windows: %APPDATA%\ant +/// - macOS: ~/Library/Application Support/ant +/// - Linux: $XDG_DATA_HOME/ant or ~/.local/share/ant +fs::path data_dir() { +#ifdef _WIN32 + const char* appdata = std::getenv("APPDATA"); + if (!appdata || appdata[0] == '\0') return {}; + return fs::path(appdata) / kDataDirName; +#elif defined(__APPLE__) + const char* home = std::getenv("HOME"); + if (!home || home[0] == '\0') return {}; + return fs::path(home) / "Library" / "Application Support" / kDataDirName; +#else + const char* xdg = std::getenv("XDG_DATA_HOME"); + if (xdg && xdg[0] != '\0') { + return fs::path(xdg) / kDataDirName; + } + const char* home = std::getenv("HOME"); + if (!home || home[0] == '\0') return {}; + return fs::path(home) / ".local" / "share" / kDataDirName; +#endif +} + +/// Read the daemon.port file and return the REST and gRPC ports. +/// The file format is two lines: REST port on line 1, gRPC port on line 2. +std::pair read_port_file() { + auto dir = data_dir(); + if (dir.empty()) return {0, 0}; + + auto path = dir / kPortFileName; + std::ifstream ifs(path); + if (!ifs.is_open()) return {0, 0}; + + std::string line1, line2; + if (!std::getline(ifs, line1)) return {0, 0}; + std::getline(ifs, line2); // optional second line + + return {parse_port(line1), parse_port(line2)}; +} + +} // namespace + +std::string discover_daemon_url() { + auto [rest, grpc] = read_port_file(); + (void)grpc; + if (rest == 0) return {}; + return "http://127.0.0.1:" + std::to_string(rest); +} + +std::string discover_grpc_target() { + auto [rest, grpc] = read_port_file(); + (void)rest; + if (grpc == 0) return {}; + return "127.0.0.1:" + std::to_string(grpc); +} + +} // namespace antd diff --git a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs index ba5b750..0cae715 100644 --- a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs +++ b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs @@ -24,6 +24,16 @@ 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(); + return string.IsNullOrEmpty(target) ? new AntdGrpcClient() : new AntdGrpcClient(target); + } + public void Dispose() => _channel.Dispose(); private static AntdException Wrap(RpcException ex) => ExceptionMapping.FromGrpcStatus(ex); diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index f0fa8ce..64d78a9 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -15,12 +15,22 @@ public sealed class AntdRestClient : IAntdClient DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; - public AntdRestClient(string baseUrl = "http://localhost:8080", TimeSpan? timeout = null) + public AntdRestClient(string baseUrl = "http://localhost:8082", TimeSpan? timeout = null) { _baseUrl = baseUrl.TrimEnd('/'); _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(); + return string.IsNullOrEmpty(url) ? new AntdRestClient(timeout: timeout) : new AntdRestClient(url, timeout); + } + public void Dispose() => _http.Dispose(); // ── Helpers ── diff --git a/antd-csharp/Antd.Sdk/DaemonDiscovery.cs b/antd-csharp/Antd.Sdk/DaemonDiscovery.cs new file mode 100644 index 0000000..86ce519 --- /dev/null +++ b/antd-csharp/Antd.Sdk/DaemonDiscovery.cs @@ -0,0 +1,97 @@ +using System.Runtime.InteropServices; + +namespace Antd.Sdk; + +/// +/// Reads the daemon.port file written by antd on startup to auto-discover +/// the REST and gRPC ports. The file contains two lines: REST port on line 1, +/// gRPC port on line 2. +/// +public static class DaemonDiscovery +{ + private const string PortFileName = "daemon.port"; + private const string DataDirName = "ant"; + + /// + /// Reads line 1 of the daemon.port file and returns the REST base URL + /// (e.g. "http://127.0.0.1:8082"). Returns empty string on failure. + /// + public static string DiscoverDaemonUrl() + { + var (restPort, _) = ReadPortFile(); + return restPort > 0 ? $"http://127.0.0.1:{restPort}" : ""; + } + + /// + /// Reads line 2 of the daemon.port file and returns the gRPC target + /// (e.g. "http://127.0.0.1:50051"). Returns empty string on failure. + /// + public static string DiscoverGrpcTarget() + { + var (_, grpcPort) = ReadPortFile(); + return grpcPort > 0 ? $"http://127.0.0.1:{grpcPort}" : ""; + } + + private static (ushort restPort, ushort grpcPort) ReadPortFile() + { + var dir = DataDir(); + if (string.IsNullOrEmpty(dir)) + return (0, 0); + + var path = Path.Combine(dir, PortFileName); + if (!File.Exists(path)) + return (0, 0); + + try + { + var text = File.ReadAllText(path).Trim(); + var lines = text.Split('\n'); + + ushort rest = 0, grpc = 0; + if (lines.Length >= 1) + rest = ParsePort(lines[0]); + if (lines.Length >= 2) + grpc = ParsePort(lines[1]); + + return (rest, grpc); + } + catch + { + return (0, 0); + } + } + + private static ushort ParsePort(string s) + { + return ushort.TryParse(s.Trim(), out var port) ? port : (ushort)0; + } + + /// + /// Returns the platform-specific data directory for ant. + /// Windows: %APPDATA%\ant + /// macOS: ~/Library/Application Support/ant + /// Linux: $XDG_DATA_HOME/ant or ~/.local/share/ant + /// + private static string DataDir() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var appdata = Environment.GetEnvironmentVariable("APPDATA"); + return string.IsNullOrEmpty(appdata) ? "" : Path.Combine(appdata, DataDirName); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var home = Environment.GetEnvironmentVariable("HOME"); + return string.IsNullOrEmpty(home) ? "" : Path.Combine(home, "Library", "Application Support", DataDirName); + } + + // Linux and others + var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + if (!string.IsNullOrEmpty(xdg)) + return Path.Combine(xdg, DataDirName); + + var homeDir = Environment.GetEnvironmentVariable("HOME"); + return string.IsNullOrEmpty(homeDir) ? "" : Path.Combine(homeDir, ".local", "share", DataDirName); + } +} diff --git a/antd-dart/lib/antd.dart b/antd-dart/lib/antd.dart index 53fe29f..85b6b10 100644 --- a/antd-dart/lib/antd.dart +++ b/antd-dart/lib/antd.dart @@ -2,5 +2,6 @@ library antd; export 'src/client.dart'; +export 'src/discover.dart'; export 'src/errors.dart'; export 'src/models.dart'; diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index d86cf05..0798ed5 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -3,11 +3,12 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; +import 'discover.dart'; import 'errors.dart'; import 'models.dart'; /// Default base URL for the antd daemon. -const defaultBaseUrl = 'http://localhost:8080'; +const defaultBaseUrl = 'http://localhost:8082'; /// Default request timeout. const defaultTimeout = Duration(minutes: 5); @@ -21,7 +22,7 @@ class AntdClient { /// Creates a new antd REST client. /// - /// [baseUrl] defaults to `http://localhost:8080`. + /// [baseUrl] defaults to `http://localhost:8082`. /// [timeout] defaults to 5 minutes. /// [httpClient] optionally provide a custom HTTP client (e.g. for testing). AntdClient({ @@ -33,6 +34,22 @@ class AntdClient { _httpClient = httpClient ?? http.Client(), _ownsClient = httpClient == null; + /// Creates an antd REST client by auto-discovering the daemon port from the + /// daemon.port file written by antd on startup. Falls back to [defaultBaseUrl] + /// if the port file is not found. + factory AntdClient.autoDiscover({ + Duration timeout = defaultTimeout, + http.Client? httpClient, + }) { + final discovered = discoverDaemonUrl(); + final baseUrl = discovered.isNotEmpty ? discovered : defaultBaseUrl; + return AntdClient( + baseUrl: baseUrl, + timeout: timeout, + httpClient: httpClient, + ); + } + /// Closes the HTTP client. Only closes if the client was created internally. void close() { if (_ownsClient) { diff --git a/antd-dart/lib/src/discover.dart b/antd-dart/lib/src/discover.dart new file mode 100644 index 0000000..51eb547 --- /dev/null +++ b/antd-dart/lib/src/discover.dart @@ -0,0 +1,88 @@ +import 'dart:io'; + +const _portFileName = 'daemon.port'; +const _dataDirName = 'ant'; + +/// Reads the daemon.port file written by antd on startup and returns the +/// REST base URL (e.g. "http://127.0.0.1:8082"). +/// Returns empty string if the port file is not found or unreadable. +String discoverDaemonUrl() { + final ports = _readPortFile(); + if (ports.$1 == 0) { + return ''; + } + return 'http://127.0.0.1:${ports.$1}'; +} + +/// Reads the daemon.port file written by antd on startup and returns the +/// gRPC target (e.g. "127.0.0.1:50051"). +/// Returns empty string if the port file is not found or has no gRPC line. +String discoverGrpcTarget() { + final ports = _readPortFile(); + if (ports.$2 == 0) { + return ''; + } + return '127.0.0.1:${ports.$2}'; +} + +/// Reads the daemon.port file and returns (restPort, grpcPort). +/// The file format is two lines: REST port on line 1, gRPC port on line 2. +/// A single-line file is valid (gRPC port will be 0). +(int, int) _readPortFile() { + final dir = _dataDir(); + if (dir.isEmpty) { + return (0, 0); + } + + final file = File('$dir${Platform.pathSeparator}$_portFileName'); + String data; + try { + data = file.readAsStringSync(); + } catch (_) { + return (0, 0); + } + + final lines = data.trim().split('\n'); + if (lines.isEmpty) { + return (0, 0); + } + + final rest = _parsePort(lines[0]); + final grpc = lines.length >= 2 ? _parsePort(lines[1]) : 0; + return (rest, grpc); +} + +int _parsePort(String s) { + final n = int.tryParse(s.trim()); + if (n == null || n < 1 || n > 65535) { + return 0; + } + return n; +} + +/// Returns the platform-specific data directory for ant. +/// - Windows: %APPDATA%\ant +/// - macOS: ~/Library/Application Support/ant +/// - Linux: $XDG_DATA_HOME/ant or ~/.local/share/ant +String _dataDir() { + if (Platform.isWindows) { + final appdata = Platform.environment['APPDATA'] ?? ''; + if (appdata.isEmpty) return ''; + return '$appdata${Platform.pathSeparator}$_dataDirName'; + } + + if (Platform.isMacOS) { + final home = Platform.environment['HOME'] ?? ''; + if (home.isEmpty) return ''; + return '$home/Library/Application Support/$_dataDirName'; + } + + // Linux and others + final xdg = Platform.environment['XDG_DATA_HOME'] ?? ''; + if (xdg.isNotEmpty) { + return '$xdg/$_dataDirName'; + } + final home = Platform.environment['HOME'] ?? ''; + if (home.isEmpty) return ''; + return '$home/.local/share/$_dataDirName'; +} diff --git a/antd-dart/lib/src/grpc_client.dart b/antd-dart/lib/src/grpc_client.dart index 74a0d2f..41228e4 100644 --- a/antd-dart/lib/src/grpc_client.dart +++ b/antd-dart/lib/src/grpc_client.dart @@ -16,6 +16,7 @@ import 'generated/antd/v1/common.pb.dart' as common_pb; import 'generated/antd/v1/files.pbgrpc.dart' as files_pb; import 'generated/antd/v1/files.pb.dart' as files_msg; +import 'discover.dart'; import 'errors.dart'; import 'models.dart'; @@ -105,6 +106,20 @@ class GrpcAntdClient { ), ); + /// Creates a gRPC client by auto-discovering the daemon port from the + /// daemon.port file written by antd on startup. Falls back to + /// `localhost:50051` if the port file is not found or has no gRPC line. + factory GrpcAntdClient.autoDiscover() { + final target = discoverGrpcTarget(); + if (target.isEmpty) { + return GrpcAntdClient.withChannel(); + } + final parts = target.split(':'); + final host = parts[0]; + final port = int.parse(parts[1]); + return GrpcAntdClient.withChannel(host: host, port: port); + } + /// Factory constructor that creates stubs from a single shared channel. factory GrpcAntdClient.withChannel({ String host = 'localhost', diff --git a/antd-elixir/lib/antd.ex b/antd-elixir/lib/antd.ex index 0749bc4..656f610 100644 --- a/antd-elixir/lib/antd.ex +++ b/antd-elixir/lib/antd.ex @@ -24,7 +24,7 @@ defmodule Antd do IO.puts("Retrieved: \#{data}") """ - defdelegate new(base_url \\ "http://localhost:8080", opts \\ []), to: Antd.Client + defdelegate new(base_url \\ "http://localhost:8082", opts \\ []), to: Antd.Client defdelegate health(client), to: Antd.Client defdelegate health!(client), to: Antd.Client defdelegate data_put_public(client, data), to: Antd.Client diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index 3956831..a9e8229 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -7,7 +7,7 @@ defmodule Antd.Client do raise on error. """ - @default_base_url "http://localhost:8080" + @default_base_url "http://localhost:8082" @default_timeout 300_000 defstruct base_url: @default_base_url, timeout: @default_timeout @@ -17,6 +17,32 @@ defmodule Antd.Client do timeout: integer() } + @doc """ + Creates a client using port discovery. + + Reads the daemon.port file to find the REST port. Falls back to the + default base URL if the port file is not found. + + ## Options + + * `:timeout` - HTTP request timeout in milliseconds (default: 300_000) + + ## Examples + + {client, url} = Antd.Client.auto_discover() + {client, url} = Antd.Client.auto_discover(timeout: 30_000) + """ + @spec auto_discover(keyword()) :: {t(), String.t()} + def auto_discover(opts \\ []) do + url = + case Antd.Discover.discover_daemon_url() do + "" -> @default_base_url + discovered -> discovered + end + + {new(url, opts), url} + end + @doc """ Creates a new client. @@ -28,7 +54,7 @@ defmodule Antd.Client do client = Antd.Client.new() client = Antd.Client.new("http://custom-host:9090") - client = Antd.Client.new("http://localhost:8080", timeout: 30_000) + client = Antd.Client.new("http://localhost:8082", timeout: 30_000) """ @spec new(String.t(), keyword()) :: t() def new(base_url \\ @default_base_url, opts \\ []) do diff --git a/antd-elixir/lib/antd/discover.ex b/antd-elixir/lib/antd/discover.ex new file mode 100644 index 0000000..e34dea4 --- /dev/null +++ b/antd-elixir/lib/antd/discover.ex @@ -0,0 +1,111 @@ +defmodule Antd.Discover do + @moduledoc """ + Auto-discovers the antd daemon by reading the `daemon.port` file that antd + writes on startup. + + The file contains two lines: REST port on line 1, gRPC port on line 2. + + Port file location is platform-specific: + - Windows: `%APPDATA%\\ant\\daemon.port` + - macOS: `~/Library/Application Support/ant/daemon.port` + - Linux: `$XDG_DATA_HOME/ant/daemon.port` or `~/.local/share/ant/daemon.port` + """ + + @port_file_name "daemon.port" + @data_dir_name "ant" + + @doc """ + Reads the daemon.port file and returns the REST base URL + (e.g. `"http://127.0.0.1:8082"`). + + Returns `""` if the port file is not found or unreadable. + """ + @spec discover_daemon_url() :: String.t() + def discover_daemon_url do + case read_port_file() do + {rest, _grpc} when rest > 0 -> "http://127.0.0.1:#{rest}" + _ -> "" + end + end + + @doc """ + Reads the daemon.port file and returns the gRPC target + (e.g. `"127.0.0.1:50051"`). + + Returns `""` if the port file is not found or has no gRPC line. + """ + @spec discover_grpc_target() :: String.t() + def discover_grpc_target do + case read_port_file() do + {_rest, grpc} when grpc > 0 -> "127.0.0.1:#{grpc}" + _ -> "" + end + end + + # --------------------------------------------------------------------------- + # Private helpers + # --------------------------------------------------------------------------- + + defp read_port_file do + case data_dir() do + "" -> + {0, 0} + + dir -> + path = Path.join(dir, @port_file_name) + + case File.read(path) do + {:ok, contents} -> + lines = + contents + |> String.trim() + |> String.split("\n", trim: true) + + rest_port = parse_port(Enum.at(lines, 0, "")) + grpc_port = parse_port(Enum.at(lines, 1, "")) + {rest_port, grpc_port} + + {:error, _} -> + {0, 0} + end + end + end + + defp parse_port(s) do + case Integer.parse(String.trim(s)) do + {n, ""} when n > 0 and n <= 65535 -> n + _ -> 0 + end + end + + defp data_dir do + case :os.type() do + {:win32, _} -> + case System.get_env("APPDATA") do + nil -> "" + "" -> "" + appdata -> Path.join(appdata, @data_dir_name) + end + + {:unix, :darwin} -> + case System.get_env("HOME") do + nil -> "" + "" -> "" + home -> Path.join([home, "Library", "Application Support", @data_dir_name]) + end + + {:unix, _} -> + case System.get_env("XDG_DATA_HOME") do + xdg when is_binary(xdg) and xdg != "" -> + Path.join(xdg, @data_dir_name) + + _ -> + case System.get_env("HOME") do + nil -> "" + "" -> "" + home -> Path.join([home, ".local", "share", @data_dir_name]) + end + end + end + end +end diff --git a/antd-elixir/lib/antd/grpc_client.ex b/antd-elixir/lib/antd/grpc_client.ex index ffa0132..4b1074f 100644 --- a/antd-elixir/lib/antd/grpc_client.ex +++ b/antd-elixir/lib/antd/grpc_client.ex @@ -29,6 +29,30 @@ defmodule Antd.GrpcClient do channel: GRPC.Channel.t() | nil } + @doc """ + Creates a gRPC client using port discovery. + + Reads the daemon.port file to find the gRPC port. Falls back to the + default target if the port file is not found. + + ## Examples + + {:ok, client, target} = Antd.GrpcClient.auto_discover() + """ + @spec auto_discover() :: {:ok, t(), String.t()} | {:error, Exception.t()} + def auto_discover do + target = + case Antd.Discover.discover_grpc_target() do + "" -> @default_target + discovered -> discovered + end + + case new(target) do + {:ok, client} -> {:ok, client, target} + {:error, _} = err -> err + end + end + @doc """ Creates a new gRPC client and opens a channel to the daemon. diff --git a/antd-go/README.md b/antd-go/README.md index 9365ea4..05c14e0 100644 --- a/antd-go/README.md +++ b/antd-go/README.md @@ -59,7 +59,10 @@ ant dev start ## Configuration ```go -// Default: http://localhost:8080, 5 minute timeout +// Auto-discover daemon via port file (recommended) +client, url := antd.NewClientAutoDiscover() + +// Explicit URL (default: http://localhost:8082) client := antd.NewClient(antd.DefaultBaseURL) // Custom URL diff --git a/antd-go/client.go b/antd-go/client.go index b15272e..04e96ba 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -14,7 +14,7 @@ import ( ) // DefaultBaseURL is the default address of the antd daemon. -const DefaultBaseURL = "http://localhost:8080" +const DefaultBaseURL = "http://localhost:8082" // DefaultTimeout is the default request timeout. const DefaultTimeout = 5 * time.Minute @@ -39,6 +39,17 @@ type Client struct { http *http.Client } +// NewClientAutoDiscover creates a client that discovers the daemon URL automatically. +// It reads the port file written by antd on startup, falling back to DefaultBaseURL. +// Returns the client and the resolved URL. +func NewClientAutoDiscover(opts ...Option) (*Client, string) { + url := DiscoverDaemonURL() + if url == "" { + url = DefaultBaseURL + } + return NewClient(url, opts...), url +} + // NewClient creates a new antd REST client. func NewClient(baseURL string, opts ...Option) *Client { c := &Client{ diff --git a/antd-go/discover.go b/antd-go/discover.go new file mode 100644 index 0000000..b11b3fb --- /dev/null +++ b/antd-go/discover.go @@ -0,0 +1,101 @@ +package antd + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" +) + +const portFileName = "daemon.port" +const dataDirName = "ant" + +// DiscoverDaemonURL reads the daemon.port file written by antd on startup +// and returns the REST base URL (e.g. "http://127.0.0.1:8082"). +// Returns empty string if the port file is not found or unreadable. +func DiscoverDaemonURL() string { + rest, _ := readPortFile() + if rest == 0 { + return "" + } + return fmt.Sprintf("http://127.0.0.1:%d", rest) +} + +// DiscoverGrpcTarget reads the daemon.port file written by antd on startup +// and returns the gRPC target (e.g. "127.0.0.1:50051"). +// Returns empty string if the port file is not found or has no gRPC line. +func DiscoverGrpcTarget() string { + _, grpc := readPortFile() + if grpc == 0 { + return "" + } + return fmt.Sprintf("127.0.0.1:%d", grpc) +} + +// readPortFile reads the daemon.port file and returns the REST and gRPC ports. +// The file format is two lines: REST port on line 1, gRPC port on line 2. +// A single-line file is valid (gRPC port will be 0). +func readPortFile() (restPort, grpcPort uint16) { + dir := dataDir() + if dir == "" { + return 0, 0 + } + + data, err := os.ReadFile(filepath.Join(dir, portFileName)) + if err != nil { + return 0, 0 + } + + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + if len(lines) < 1 { + return 0, 0 + } + + restPort = parsePort(lines[0]) + if len(lines) >= 2 { + grpcPort = parsePort(lines[1]) + } + return restPort, grpcPort +} + +func parsePort(s string) uint16 { + n, err := strconv.ParseUint(strings.TrimSpace(s), 10, 16) + if err != nil { + return 0 + } + return uint16(n) +} + +// dataDir returns the platform-specific data directory for ant. +// - Windows: %APPDATA%\ant +// - macOS: ~/Library/Application Support/ant +// - Linux: $XDG_DATA_HOME/ant or ~/.local/share/ant +func dataDir() string { + switch runtime.GOOS { + case "windows": + appdata := os.Getenv("APPDATA") + if appdata == "" { + return "" + } + return filepath.Join(appdata, dataDirName) + + case "darwin": + home := os.Getenv("HOME") + if home == "" { + return "" + } + return filepath.Join(home, "Library", "Application Support", dataDirName) + + default: // linux and others + if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { + return filepath.Join(xdg, dataDirName) + } + home := os.Getenv("HOME") + if home == "" { + return "" + } + return filepath.Join(home, ".local", "share", dataDirName) + } +} diff --git a/antd-go/discover_test.go b/antd-go/discover_test.go new file mode 100644 index 0000000..a79e6da --- /dev/null +++ b/antd-go/discover_test.go @@ -0,0 +1,150 @@ +package antd + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +// withTempPortFile creates a temp directory, writes a daemon.port file with the +// given content, and sets the environment so dataDir() returns that directory. +// It returns a cleanup function that restores the original env. +func withTempPortFile(t *testing.T, content string) (cleanup func()) { + t.Helper() + dir := t.TempDir() + antDir := filepath.Join(dir, dataDirName) + if err := os.MkdirAll(antDir, 0o755); err != nil { + t.Fatal(err) + } + if content != "" { + if err := os.WriteFile(filepath.Join(antDir, portFileName), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + + // Override env so dataDir() finds our temp directory + switch runtime.GOOS { + case "windows": + old := os.Getenv("APPDATA") + os.Setenv("APPDATA", dir) + return func() { os.Setenv("APPDATA", old) } + case "darwin": + old := os.Getenv("HOME") + os.Setenv("HOME", dir) + // On macOS dataDir uses ~/Library/Application Support/ant, so adjust + macDir := filepath.Join(dir, "Library", "Application Support", dataDirName) + os.MkdirAll(macDir, 0o755) + if content != "" { + os.WriteFile(filepath.Join(macDir, portFileName), []byte(content), 0o644) + } + return func() { os.Setenv("HOME", old) } + default: + old := os.Getenv("XDG_DATA_HOME") + os.Setenv("XDG_DATA_HOME", dir) + return func() { + if old == "" { + os.Unsetenv("XDG_DATA_HOME") + } else { + os.Setenv("XDG_DATA_HOME", old) + } + } + } +} + +func TestDiscoverDaemonURL_ValidFile(t *testing.T) { + cleanup := withTempPortFile(t, "8082\n50051\n") + defer cleanup() + + url := DiscoverDaemonURL() + if url != "http://127.0.0.1:8082" { + t.Fatalf("expected http://127.0.0.1:8082, got %s", url) + } +} + +func TestDiscoverGrpcTarget_ValidFile(t *testing.T) { + cleanup := withTempPortFile(t, "8082\n50051\n") + defer cleanup() + + target := DiscoverGrpcTarget() + if target != "127.0.0.1:50051" { + t.Fatalf("expected 127.0.0.1:50051, got %s", target) + } +} + +func TestDiscoverDaemonURL_SingleLine(t *testing.T) { + cleanup := withTempPortFile(t, "9000\n") + defer cleanup() + + url := DiscoverDaemonURL() + if url != "http://127.0.0.1:9000" { + t.Fatalf("expected http://127.0.0.1:9000, got %s", url) + } + + // gRPC should be empty with single line + target := DiscoverGrpcTarget() + if target != "" { + t.Fatalf("expected empty gRPC target, got %s", target) + } +} + +func TestDiscoverDaemonURL_MissingFile(t *testing.T) { + cleanup := withTempPortFile(t, "") + defer cleanup() + + url := DiscoverDaemonURL() + if url != "" { + t.Fatalf("expected empty string, got %s", url) + } +} + +func TestDiscoverDaemonURL_InvalidContent(t *testing.T) { + cleanup := withTempPortFile(t, "not-a-number\n") + defer cleanup() + + url := DiscoverDaemonURL() + if url != "" { + t.Fatalf("expected empty string, got %s", url) + } +} + +func TestDiscoverDaemonURL_WhitespaceHandling(t *testing.T) { + cleanup := withTempPortFile(t, " 8082 \n 50051 \n") + defer cleanup() + + url := DiscoverDaemonURL() + if url != "http://127.0.0.1:8082" { + t.Fatalf("expected http://127.0.0.1:8082, got %s", url) + } + + target := DiscoverGrpcTarget() + if target != "127.0.0.1:50051" { + t.Fatalf("expected 127.0.0.1:50051, got %s", target) + } +} + +func TestNewClientAutoDiscover_WithPortFile(t *testing.T) { + cleanup := withTempPortFile(t, "9999\n") + defer cleanup() + + c, url := NewClientAutoDiscover() + if url != "http://127.0.0.1:9999" { + t.Fatalf("expected http://127.0.0.1:9999, got %s", url) + } + if c.baseURL != "http://127.0.0.1:9999" { + t.Fatalf("client baseURL mismatch: %s", c.baseURL) + } +} + +func TestNewClientAutoDiscover_Fallback(t *testing.T) { + cleanup := withTempPortFile(t, "") + defer cleanup() + + c, url := NewClientAutoDiscover() + if url != DefaultBaseURL { + t.Fatalf("expected %s, got %s", DefaultBaseURL, url) + } + if c.baseURL != DefaultBaseURL { + t.Fatalf("client baseURL mismatch: %s", c.baseURL) + } +} diff --git a/antd-go/grpc_client.go b/antd-go/grpc_client.go index 85469dc..b5aa136 100644 --- a/antd-go/grpc_client.go +++ b/antd-go/grpc_client.go @@ -47,6 +47,18 @@ type GrpcClient struct { file pb.FileServiceClient } +// NewGrpcClientAutoDiscover creates a gRPC client that discovers the daemon target +// automatically. It reads the port file written by antd on startup, falling back +// to DefaultGrpcTarget. Returns the client, the resolved target, and any error. +func NewGrpcClientAutoDiscover(opts ...GrpcOption) (*GrpcClient, string, error) { + target := DiscoverGrpcTarget() + if target == "" { + target = DefaultGrpcTarget + } + c, err := NewGrpcClient(target, opts...) + return c, target, err +} + // NewGrpcClient creates a new gRPC client connected to the given target // (e.g. "localhost:50051"). The connection is established lazily on first use. func NewGrpcClient(target string, opts ...GrpcOption) (*GrpcClient, error) { diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index fc3db20..f592c35 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -31,7 +31,7 @@ public class AntdClient implements AutoCloseable { /** Default daemon address. */ - public static final String DEFAULT_BASE_URL = "http://localhost:8080"; + public static final String DEFAULT_BASE_URL = "http://localhost:8082"; /** Default request timeout (5 minutes). */ public static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(5); @@ -40,6 +40,20 @@ public class AntdClient implements AutoCloseable { private final HttpClient httpClient; private final Duration timeout; + /** + * Creates a client that auto-discovers the daemon via the {@code daemon.port} file. + * Falls back to {@link #DEFAULT_BASE_URL} if discovery fails. + * + * @return a new AntdClient connected to the discovered or default URL + */ + public static AntdClient autoDiscover() { + String url = DaemonDiscovery.discoverDaemonUrl(); + if (url.isEmpty()) { + url = DEFAULT_BASE_URL; + } + return new AntdClient(url); + } + public AntdClient() { this(DEFAULT_BASE_URL, DEFAULT_TIMEOUT); } diff --git a/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java b/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java new file mode 100644 index 0000000..e9de895 --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java @@ -0,0 +1,138 @@ +package com.autonomi.antd; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Discovers the antd daemon by reading the {@code daemon.port} file + * that the daemon writes on startup. + * + *

The file contains two lines: the REST port on line 1 and the gRPC port on line 2. + * + *

Port file locations by platform: + *

    + *
  • Windows: {@code %APPDATA%\ant\daemon.port}
  • + *
  • macOS: {@code ~/Library/Application Support/ant/daemon.port}
  • + *
  • Linux: {@code $XDG_DATA_HOME/ant/daemon.port} or {@code ~/.local/share/ant/daemon.port}
  • + *
+ */ +public final class DaemonDiscovery { + + private static final String PORT_FILE_NAME = "daemon.port"; + private static final String DATA_DIR_NAME = "ant"; + + private DaemonDiscovery() {} + + /** + * Reads the daemon.port file and returns the REST base URL + * (e.g. {@code "http://127.0.0.1:8082"}). + * + * @return the discovered URL, or an empty string if discovery fails + */ + public static String discoverDaemonUrl() { + int port = readPort(0); + if (port == 0) { + return ""; + } + return "http://127.0.0.1:" + port; + } + + /** + * Reads the daemon.port file and returns the gRPC target + * (e.g. {@code "127.0.0.1:50051"}). + * + * @return the discovered gRPC target, or an empty string if discovery fails + */ + public static String discoverGrpcTarget() { + int port = readPort(1); + if (port == 0) { + return ""; + } + return "127.0.0.1:" + port; + } + + /** + * Reads the specified line from the port file and parses it as a port number. + * + * @param lineIndex 0 for REST port, 1 for gRPC port + * @return the port number, or 0 on failure + */ + private static int readPort(int lineIndex) { + Path dir = dataDir(); + if (dir == null) { + return 0; + } + + Path portFile = dir.resolve(PORT_FILE_NAME); + try { + List lines = Files.readAllLines(portFile); + if (lines.size() <= lineIndex) { + return 0; + } + return parsePort(lines.get(lineIndex)); + } catch (IOException e) { + return 0; + } + } + + /** + * Parses a port string into an integer in the valid port range (1-65535). + * + * @return the port number, or 0 if invalid + */ + private static int parsePort(String s) { + try { + int n = Integer.parseInt(s.trim()); + if (n < 1 || n > 65535) { + return 0; + } + return n; + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Returns the platform-specific data directory for ant. + *
    + *
  • Windows: {@code %APPDATA%\ant}
  • + *
  • macOS: {@code ~/Library/Application Support/ant}
  • + *
  • Linux: {@code $XDG_DATA_HOME/ant} or {@code ~/.local/share/ant}
  • + *
+ * + * @return the data directory path, or null if it cannot be determined + */ + private static Path dataDir() { + String os = System.getProperty("os.name", "").toLowerCase(); + + if (os.contains("win")) { + String appdata = System.getenv("APPDATA"); + if (appdata == null || appdata.isEmpty()) { + return null; + } + return Paths.get(appdata, DATA_DIR_NAME); + } + + if (os.contains("mac") || os.contains("darwin")) { + String home = System.getProperty("user.home"); + if (home == null || home.isEmpty()) { + return null; + } + return Paths.get(home, "Library", "Application Support", DATA_DIR_NAME); + } + + // Linux and others + String xdg = System.getenv("XDG_DATA_HOME"); + if (xdg != null && !xdg.isEmpty()) { + return Paths.get(xdg, DATA_DIR_NAME); + } + String home = System.getProperty("user.home"); + if (home == null || home.isEmpty()) { + return null; + } + return Paths.get(home, ".local", "share", DATA_DIR_NAME); + } +} diff --git a/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java b/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java index 8a970ae..37d2c2a 100644 --- a/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java @@ -86,6 +86,20 @@ public class GrpcAntdClient implements AutoCloseable { private final GraphServiceGrpc.GraphServiceBlockingStub graphStub; private final FileServiceGrpc.FileServiceBlockingStub fileStub; + /** + * Creates a client that auto-discovers the daemon via the {@code daemon.port} file. + * Falls back to {@link #DEFAULT_TARGET} if discovery fails. + * + * @return a new GrpcAntdClient connected to the discovered or default target + */ + public static GrpcAntdClient autoDiscover() { + String target = DaemonDiscovery.discoverGrpcTarget(); + if (target.isEmpty()) { + target = DEFAULT_TARGET; + } + return new GrpcAntdClient(target); + } + /** * Creates a client connected to {@code localhost:50051} with plaintext (no TLS). */ diff --git a/antd-js/src/discover.ts b/antd-js/src/discover.ts new file mode 100644 index 0000000..18879f0 --- /dev/null +++ b/antd-js/src/discover.ts @@ -0,0 +1,85 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +const PORT_FILE_NAME = "daemon.port"; +const DATA_DIR_NAME = "ant"; + +/** + * Reads the daemon.port file written by antd on startup and returns the + * REST base URL (e.g. "http://127.0.0.1:8082"). + * Returns empty string if the port file is not found or unreadable. + */ +export function discoverDaemonUrl(): string { + const ports = readPortFile(); + if (ports.rest === 0) { + return ""; + } + return `http://127.0.0.1:${ports.rest}`; +} + +/** + * Reads the daemon.port file and returns the parsed REST and gRPC ports. + * The file format is two lines: REST port on line 1, gRPC port on line 2. + * A single-line file is valid (gRPC port will be 0). + */ +function readPortFile(): { rest: number; grpc: number } { + const dir = dataDir(); + if (dir === "") { + return { rest: 0, grpc: 0 }; + } + + let data: string; + try { + data = fs.readFileSync(path.join(dir, PORT_FILE_NAME), "utf-8"); + } catch { + return { rest: 0, grpc: 0 }; + } + + const lines = data.trim().split("\n"); + if (lines.length < 1) { + return { rest: 0, grpc: 0 }; + } + + const rest = parsePort(lines[0]); + const grpc = lines.length >= 2 ? parsePort(lines[1]) : 0; + return { rest, grpc }; +} + +function parsePort(s: string): number { + const n = parseInt(s.trim(), 10); + if (isNaN(n) || n < 1 || n > 65535) { + return 0; + } + return n; +} + +/** + * Returns the platform-specific data directory for ant. + * - Windows: %APPDATA%\ant + * - macOS: ~/Library/Application Support/ant + * - Linux: $XDG_DATA_HOME/ant or ~/.local/share/ant + */ +function dataDir(): string { + switch (process.platform) { + case "win32": { + const appdata = process.env.APPDATA ?? ""; + if (appdata === "") return ""; + return path.join(appdata, DATA_DIR_NAME); + } + case "darwin": { + const home = os.homedir(); + if (home === "") return ""; + return path.join(home, "Library", "Application Support", DATA_DIR_NAME); + } + default: { + const xdg = process.env.XDG_DATA_HOME ?? ""; + if (xdg !== "") { + return path.join(xdg, DATA_DIR_NAME); + } + const home = os.homedir(); + if (home === "") return ""; + return path.join(home, ".local", "share", DATA_DIR_NAME); + } + } +} diff --git a/antd-js/src/index.ts b/antd-js/src/index.ts index c2ec3fb..5266fa0 100644 --- a/antd-js/src/index.ts +++ b/antd-js/src/index.ts @@ -23,6 +23,8 @@ export { export { RestClient } from "./rest-client.js"; export type { RestClientOptions } from "./rest-client.js"; +export { discoverDaemonUrl } from "./discover.js"; + import { RestClient, type RestClientOptions } from "./rest-client.js"; /** Create a REST client for the antd daemon. */ diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index 268cdbe..add8b29 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -1,3 +1,4 @@ +import { discoverDaemonUrl } from "./discover.js"; import { fromHttpStatus } from "./errors.js"; import type { Archive, @@ -10,7 +11,7 @@ import type { /** Options for creating a REST client. */ export interface RestClientOptions { - /** Base URL of the antd daemon. Defaults to "http://localhost:8080". */ + /** Base URL of the antd daemon. Defaults to "http://localhost:8082". */ baseUrl?: string; /** Request timeout in milliseconds. Defaults to 300000 (5 minutes). */ timeout?: number; @@ -21,8 +22,25 @@ export class RestClient { private readonly baseUrl: string; private readonly timeout: number; + /** + * Creates a REST client by auto-discovering the daemon port from the + * daemon.port file written by antd on startup. Falls back to the default + * base URL if the port file is not found. + * + * @returns An object with the created `client` and the discovered `url` + * (empty string if discovery failed and default was used). + */ + static autoDiscover(options?: RestClientOptions): { client: RestClient; url: string } { + const discovered = discoverDaemonUrl(); + const opts: RestClientOptions = { ...options }; + if (discovered !== "") { + opts.baseUrl = discovered; + } + return { client: new RestClient(opts), url: discovered }; + } + constructor(options: RestClientOptions = {}) { - this.baseUrl = (options.baseUrl ?? "http://localhost:8080").replace(/\/+$/, ""); + this.baseUrl = (options.baseUrl ?? "http://localhost:8082").replace(/\/+$/, ""); this.timeout = options.timeout ?? 300_000; } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt index f470c01..b34f5f8 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt @@ -8,6 +8,17 @@ import io.grpc.Status class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { + companion object { + /** + * Create a client by auto-discovering the daemon gRPC port from the + * `daemon.port` file. Falls back to `localhost:50051` if not found. + */ + fun autoDiscover(): AntdGrpcClient { + val target = DaemonDiscovery.discoverGrpcTarget().ifEmpty { "localhost:50051" } + return AntdGrpcClient(target) + } + } + private val channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build() private val healthStub = HealthServiceGrpcKt.HealthServiceCoroutineStub(channel) private val dataStub = DataServiceGrpcKt.DataServiceCoroutineStub(channel) diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index 1bf4fcd..b17139e 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -18,10 +18,21 @@ import java.time.Duration import java.util.Base64 class AntdRestClient( - baseUrl: String = "http://localhost:8080", + baseUrl: String = "http://localhost:8082", timeout: Duration = Duration.ofSeconds(300), ) : IAntdClient { + companion object { + /** + * Create a client by auto-discovering the daemon port from the + * `daemon.port` file. Falls back to `http://localhost:8082` if not found. + */ + fun autoDiscover(timeout: Duration = Duration.ofSeconds(300)): AntdRestClient { + val url = DaemonDiscovery.discoverDaemonUrl().ifEmpty { "http://localhost:8082" } + return AntdRestClient(url, timeout) + } + } + private val baseUrl = baseUrl.trimEnd('/') private val http = OkHttpClient.Builder() .callTimeout(timeout) diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt new file mode 100644 index 0000000..1352fb8 --- /dev/null +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt @@ -0,0 +1,73 @@ +package com.autonomi.sdk + +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Reads the `daemon.port` file written by antd on startup to auto-discover + * the REST and gRPC ports. + * + * The file contains two lines: REST port on line 1, gRPC port on line 2. + */ +object DaemonDiscovery { + + private const val PORT_FILE_NAME = "daemon.port" + private const val DATA_DIR_NAME = "ant" + + /** + * Returns the REST base URL (e.g. `"http://127.0.0.1:8082"`) discovered + * from the daemon.port file, or an empty string if not found. + */ + fun discoverDaemonUrl(): String { + val (rest, _) = readPortFile() + return if (rest == 0) "" else "http://127.0.0.1:$rest" + } + + /** + * Returns the gRPC target (e.g. `"127.0.0.1:50051"`) discovered from + * the daemon.port file, or an empty string if not found. + */ + fun discoverGrpcTarget(): String { + val (_, grpc) = readPortFile() + return if (grpc == 0) "" else "127.0.0.1:$grpc" + } + + private fun readPortFile(): Pair { + val dir = dataDir() ?: return 0 to 0 + val file = dir.resolve(PORT_FILE_NAME).toFile() + if (!file.exists()) return 0 to 0 + + return try { + val lines = file.readLines().map { it.trim() } + val rest = lines.getOrNull(0)?.toIntOrNull()?.takeIf { it in 1..65535 } ?: 0 + val grpc = lines.getOrNull(1)?.toIntOrNull()?.takeIf { it in 1..65535 } ?: 0 + rest to grpc + } catch (_: Exception) { + 0 to 0 + } + } + + private fun dataDir(): Path? { + val os = System.getProperty("os.name", "").lowercase() + return when { + os.contains("win") -> { + val appdata = System.getenv("APPDATA") ?: return null + Paths.get(appdata, DATA_DIR_NAME) + } + os.contains("mac") || os.contains("darwin") -> { + val home = System.getProperty("user.home") ?: return null + Paths.get(home, "Library", "Application Support", DATA_DIR_NAME) + } + else -> { + val xdg = System.getenv("XDG_DATA_HOME") + if (!xdg.isNullOrEmpty()) { + Paths.get(xdg, DATA_DIR_NAME) + } else { + val home = System.getProperty("user.home") ?: return null + Paths.get(home, ".local", "share", DATA_DIR_NAME) + } + } + } + } +} diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index fa867fd..bf6e526 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -7,18 +7,19 @@ local cjson = require("cjson") local base64 = require("antd.base64") local errors = require("antd.errors") local models = require("antd.models") +local discover = require("antd.discover") local Client = {} Client.__index = Client --- Default base URL for the antd daemon. -Client.DEFAULT_BASE_URL = "http://localhost:8080" +Client.DEFAULT_BASE_URL = "http://localhost:8082" --- Default request timeout in seconds. Client.DEFAULT_TIMEOUT = 300 --- Create a new antd client. --- @param base_url string base URL (default "http://localhost:8080") +-- @param base_url string base URL (default "http://localhost:8082") -- @param opts table optional settings: { timeout = number } -- @return Client function Client:new(base_url, opts) @@ -401,4 +402,16 @@ function Client:file_cost(path, is_public, include_archive) return str(j, "cost"), nil end +--- Create a client using daemon port discovery. +-- Falls back to the default base URL if discovery fails. +-- @param opts table optional settings: { timeout = number } +-- @return Client client, string url +function Client.auto_discover(opts) + local url = discover.daemon_url() + if url == "" then + url = Client.DEFAULT_BASE_URL + end + return Client:new(url, opts), url +end + return Client diff --git a/antd-lua/src/antd/discover.lua b/antd-lua/src/antd/discover.lua new file mode 100644 index 0000000..19da82f --- /dev/null +++ b/antd-lua/src/antd/discover.lua @@ -0,0 +1,96 @@ +--- Port discovery for the antd daemon. +-- Reads the `daemon.port` file written by antd on startup. +-- @module antd.discover + +local Discover = {} + +local PORT_FILE_NAME = "daemon.port" +local DATA_DIR_NAME = "ant" + +--- Returns true if running on Windows. +local function is_windows() + return package.config:sub(1, 1) == "\\" +end + +--- Returns the platform-specific data directory for ant. +-- @return string|nil directory path, or nil if not determinable +local function data_dir() + if is_windows() then + local appdata = os.getenv("APPDATA") + if not appdata or appdata == "" then return nil end + return appdata .. "\\" .. DATA_DIR_NAME + end + + -- Check for macOS by looking for ~/Library + local home = os.getenv("HOME") + if home and home ~= "" then + local lib = home .. "/Library" + local f = io.open(lib, "r") + if f then + f:close() + -- macOS + return home .. "/Library/Application Support/" .. DATA_DIR_NAME + end + end + + -- Linux / other Unix + local xdg = os.getenv("XDG_DATA_HOME") + if xdg and xdg ~= "" then + return xdg .. "/" .. DATA_DIR_NAME + end + if home and home ~= "" then + return home .. "/.local/share/" .. DATA_DIR_NAME + end + + return nil +end + +--- Read the daemon.port file and return the two port numbers. +-- @return number|nil REST port +-- @return number|nil gRPC port +local function read_port_file() + local dir = data_dir() + if not dir then return nil, nil end + + local sep = is_windows() and "\\" or "/" + local path = dir .. sep .. PORT_FILE_NAME + + local f = io.open(path, "r") + if not f then return nil, nil end + + local contents = f:read("*a") + f:close() + if not contents or contents == "" then return nil, nil end + + local lines = {} + for line in contents:gmatch("[^\r\n]+") do + lines[#lines + 1] = line + end + + if #lines < 1 then return nil, nil end + + local rest_port = tonumber(lines[1]) + local grpc_port = #lines >= 2 and tonumber(lines[2]) or nil + + return rest_port, grpc_port +end + +--- Discover the antd daemon REST URL. +-- Returns the URL (e.g. "http://127.0.0.1:8082") or "" if unavailable. +-- @return string +function Discover.daemon_url() + local rest = read_port_file() + if not rest or rest == 0 then return "" end + return string.format("http://127.0.0.1:%d", rest) +end + +--- Discover the antd daemon gRPC target. +-- Returns the target (e.g. "127.0.0.1:50051") or "" if unavailable. +-- @return string +function Discover.grpc_target() + local _, grpc = read_port_file() + if not grpc or grpc == 0 then return "" end + return string.format("127.0.0.1:%d", grpc) +end + +return Discover diff --git a/antd-lua/src/antd/init.lua b/antd-lua/src/antd/init.lua index 0ff90a5..1e8370c 100644 --- a/antd-lua/src/antd/init.lua +++ b/antd-lua/src/antd/init.lua @@ -4,6 +4,7 @@ local Client = require("antd.client") local models = require("antd.models") local errors = require("antd.errors") +local discover = require("antd.discover") local M = {} @@ -13,11 +14,25 @@ M._VERSION = "0.1.0" --- Default base URL for the antd daemon. M.DEFAULT_BASE_URL = Client.DEFAULT_BASE_URL +--- Discover the daemon REST URL from the port file. +-- @return string URL or "" if unavailable +M.discover_daemon_url = discover.daemon_url + +--- Discover the daemon gRPC target from the port file. +-- @return string target or "" if unavailable +M.discover_grpc_target = discover.grpc_target + +--- Create a client using daemon port discovery. +-- Falls back to the default base URL if discovery fails. +-- @param opts table optional settings: { timeout = number } +-- @return Client client, string url +M.auto_discover = Client.auto_discover + --- Default timeout in seconds. M.DEFAULT_TIMEOUT = Client.DEFAULT_TIMEOUT --- Create a new antd client. --- @param base_url string base URL (default "http://localhost:8080") +-- @param base_url string base URL (default "http://localhost:8082") -- @param opts table optional settings: { timeout = number } -- @return Client function M.new_client(base_url, opts) diff --git a/antd-mcp/README.md b/antd-mcp/README.md index 3a12fe1..56f514f 100644 --- a/antd-mcp/README.md +++ b/antd-mcp/README.md @@ -24,7 +24,9 @@ antd-mcp --sse | Variable | Default | Description | |----------|---------|-------------| -| `ANTD_BASE_URL` | `http://localhost:8080` | antd daemon URL | +| `ANTD_BASE_URL` | auto-discovered | antd daemon URL (overrides port-file discovery) | + +The MCP server automatically discovers the antd daemon via the `daemon.port` file written by antd on startup. Set `ANTD_BASE_URL` only if you need to override this (e.g. connecting to a remote daemon). If neither the env var nor port file is available, falls back to `http://127.0.0.1:8082`. ## Claude Desktop Configuration @@ -34,15 +36,14 @@ Add to your Claude Desktop config (`claude_desktop_config.json`): { "mcpServers": { "antd-autonomi": { - "command": "antd-mcp", - "env": { - "ANTD_BASE_URL": "http://localhost:8080" - } + "command": "antd-mcp" } } } ``` +The server will auto-discover the daemon via the port file. Add `"env": {"ANTD_BASE_URL": "http://your-host:port"}` only if you need to override discovery. + ## Tool Reference ### Data Operations diff --git a/antd-mcp/src/antd_mcp/discover.py b/antd-mcp/src/antd_mcp/discover.py new file mode 100644 index 0000000..613c1df --- /dev/null +++ b/antd-mcp/src/antd_mcp/discover.py @@ -0,0 +1,102 @@ +"""Port discovery for the antd daemon. + +The antd daemon writes a ``daemon.port`` file on startup containing two lines: + - Line 1: REST port + - Line 2: gRPC port + +This module reads that file using platform-specific data directory paths to +auto-discover the daemon without requiring manual configuration. +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +_PORT_FILE_NAME = "daemon.port" +_DATA_DIR_NAME = "ant" + + +def _data_dir() -> Path | None: + """Return the platform-specific data directory for ant, or None.""" + if sys.platform == "win32": + appdata = os.environ.get("APPDATA") + if not appdata: + return None + return Path(appdata) / _DATA_DIR_NAME + + if sys.platform == "darwin": + home = os.environ.get("HOME") + if not home: + return None + return Path(home) / "Library" / "Application Support" / _DATA_DIR_NAME + + # Linux and other Unix-likes + xdg = os.environ.get("XDG_DATA_HOME") + if xdg: + return Path(xdg) / _DATA_DIR_NAME + home = os.environ.get("HOME") + if not home: + return None + return Path(home) / ".local" / "share" / _DATA_DIR_NAME + + +def _read_port_file() -> tuple[int, int]: + """Read the daemon.port file and return (rest_port, grpc_port). + + Returns (0, 0) if the file is missing or unreadable. + A single-line file is valid; grpc_port will be 0 in that case. + """ + data_dir = _data_dir() + if data_dir is None: + return 0, 0 + + port_file = data_dir / _PORT_FILE_NAME + try: + text = port_file.read_text().strip() + except (OSError, ValueError): + return 0, 0 + + lines = text.splitlines() + if not lines: + return 0, 0 + + rest_port = _parse_port(lines[0]) + grpc_port = _parse_port(lines[1]) if len(lines) >= 2 else 0 + return rest_port, grpc_port + + +def _parse_port(s: str) -> int: + """Parse a port string, returning 0 on failure.""" + try: + n = int(s.strip()) + if 1 <= n <= 65535: + return n + except ValueError: + pass + return 0 + + +def discover_daemon_url() -> str: + """Read the daemon.port file and return the REST base URL. + + Returns ``"http://127.0.0.1:{port}"`` on success, or ``""`` if the port + file is not found or unreadable. + """ + rest, _ = _read_port_file() + if rest == 0: + return "" + return f"http://127.0.0.1:{rest}" + + +def discover_grpc_target() -> str: + """Read the daemon.port file and return the gRPC target. + + Returns ``"127.0.0.1:{port}"`` on success, or ``""`` if the port file + is not found or has no gRPC line. + """ + _, grpc = _read_port_file() + if grpc == 0: + return "" + return f"127.0.0.1:{grpc}" diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index 2377e07..4168323 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -14,16 +14,20 @@ from antd.exceptions import AntdError from antd.models import GraphDescendant +from .discover import discover_daemon_url from .errors import format_error, format_unexpected_error # --------------------------------------------------------------------------- # Lifespan — create/close a single AsyncRestClient for the server's lifetime # --------------------------------------------------------------------------- +_DEFAULT_BASE_URL = "http://127.0.0.1:8082" + @asynccontextmanager async def lifespan(server: FastMCP): - base_url = os.environ.get("ANTD_BASE_URL", "http://localhost:8080") + # Priority: env var > port-file discovery > default + base_url = os.environ.get("ANTD_BASE_URL") or discover_daemon_url() or _DEFAULT_BASE_URL client = AsyncAntdClient(transport="rest", base_url=base_url) # Query the daemon's network on startup network = "unknown" diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index 7fdb39e..7a73775 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -25,7 +25,7 @@ class AntdClient private string $baseUrl; public function __construct( - string $baseUrl = 'http://localhost:8080', + string $baseUrl = 'http://localhost:8082', float $timeout = 300.0, ?Client $httpClient = null, ) { @@ -36,6 +36,24 @@ public function __construct( ]); } + /** + * Create a client using daemon port discovery. + * Falls back to http://localhost:8082 if discovery fails. + * + * @param float $timeout Request timeout in seconds. + * @param \GuzzleHttp\Client|null $httpClient Optional HTTP client. + * @return array{0: self, 1: string} [$client, $url] + */ + public static function autoDiscover(float $timeout = 300.0, ?Client $httpClient = null): array + { + $url = DaemonDiscovery::discoverDaemonUrl(); + if ($url === '') { + $url = 'http://localhost:8082'; + } + $client = new self($url, $timeout, $httpClient); + return [$client, $url]; + } + // --- Internal helpers --- private function b64Encode(string $data): string diff --git a/antd-php/src/DaemonDiscovery.php b/antd-php/src/DaemonDiscovery.php new file mode 100644 index 0000000..4f521a0 --- /dev/null +++ b/antd-php/src/DaemonDiscovery.php @@ -0,0 +1,129 @@ += 2 ? self::parsePort($lines[1]) : 0; + return [$rest, $grpc]; + } + + private static function parsePort(string $s): int + { + $n = (int) trim($s); + if ($n < 1 || $n > 65535) { + return 0; + } + return $n; + } + + private static function dataDir(): string + { + switch (PHP_OS_FAMILY) { + case 'Windows': + $appdata = getenv('APPDATA'); + if ($appdata === false || $appdata === '') { + return ''; + } + return $appdata . DIRECTORY_SEPARATOR . self::DATA_DIR_NAME; + + case 'Darwin': + $home = getenv('HOME'); + if ($home === false || $home === '') { + return ''; + } + return $home . '/Library/Application Support/' . self::DATA_DIR_NAME; + + default: // Linux and others + $xdg = getenv('XDG_DATA_HOME'); + if ($xdg !== false && $xdg !== '') { + return $xdg . '/' . self::DATA_DIR_NAME; + } + $home = getenv('HOME'); + if ($home === false || $home === '') { + return ''; + } + return $home . '/.local/share/' . self::DATA_DIR_NAME; + } + } +} diff --git a/antd-py/src/antd/__init__.py b/antd-py/src/antd/__init__.py index 3c1bc8c..8f3b9e7 100644 --- a/antd-py/src/antd/__init__.py +++ b/antd-py/src/antd/__init__.py @@ -19,6 +19,7 @@ HealthStatus, PutResult, ) +from ._discover import discover_daemon_url, discover_grpc_target from .exceptions import ( AntdError, AlreadyExistsError, @@ -32,6 +33,9 @@ ) __all__ = [ + # Discovery + "discover_daemon_url", + "discover_grpc_target", # Factory functions "AntdClient", "AsyncAntdClient", @@ -61,7 +65,7 @@ def AntdClient(transport: str = "rest", **kwargs): Args: transport: "rest" (default) or "grpc" **kwargs: Passed to the underlying client constructor. - REST: base_url (default "http://localhost:8080"), timeout + REST: base_url (default "http://localhost:8082"), timeout gRPC: target (default "localhost:50051") """ if transport == "rest": @@ -80,7 +84,7 @@ def AsyncAntdClient(transport: str = "rest", **kwargs): Args: transport: "rest" (default) or "grpc" **kwargs: Passed to the underlying client constructor. - REST: base_url (default "http://localhost:8080"), timeout + REST: base_url (default "http://localhost:8082"), timeout gRPC: target (default "localhost:50051") """ if transport == "rest": diff --git a/antd-py/src/antd/_discover.py b/antd-py/src/antd/_discover.py new file mode 100644 index 0000000..8119538 --- /dev/null +++ b/antd-py/src/antd/_discover.py @@ -0,0 +1,93 @@ +"""Daemon port-file discovery for antd. + +The antd daemon writes a ``daemon.port`` file on startup containing: + - Line 1: REST port + - Line 2: gRPC port + +This module reads that file to auto-discover the daemon's listen addresses. +""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +_PORT_FILE_NAME = "daemon.port" +_DATA_DIR_NAME = "ant" + + +def discover_daemon_url() -> str: + """Return the REST base URL from the daemon port file, or ``""`` on failure.""" + rest, _ = _read_port_file() + if rest == 0: + return "" + return f"http://127.0.0.1:{rest}" + + +def discover_grpc_target() -> str: + """Return the gRPC target from the daemon port file, or ``""`` on failure.""" + _, grpc = _read_port_file() + if grpc == 0: + return "" + return f"127.0.0.1:{grpc}" + + +def _read_port_file() -> tuple[int, int]: + """Read the daemon.port file and return ``(rest_port, grpc_port)``. + + A single-line file is valid (gRPC port will be 0). + Returns ``(0, 0)`` on any error. + """ + dir_path = _data_dir() + if not dir_path: + return 0, 0 + + port_file = Path(dir_path) / _PORT_FILE_NAME + try: + text = port_file.read_text(encoding="utf-8") + except OSError: + return 0, 0 + + lines = text.strip().splitlines() + if not lines: + return 0, 0 + + rest_port = _parse_port(lines[0]) + grpc_port = _parse_port(lines[1]) if len(lines) >= 2 else 0 + return rest_port, grpc_port + + +def _parse_port(s: str) -> int: + """Parse a port string, returning 0 on failure.""" + try: + n = int(s.strip()) + except (ValueError, TypeError): + return 0 + if 1 <= n <= 65535: + return n + return 0 + + +def _data_dir() -> str: + """Return the platform-specific data directory for ant, or ``""``.""" + if sys.platform == "win32": + appdata = os.environ.get("APPDATA", "") + if not appdata: + return "" + return os.path.join(appdata, _DATA_DIR_NAME) + + if sys.platform == "darwin": + home = os.environ.get("HOME", "") + if not home: + return "" + return os.path.join(home, "Library", "Application Support", _DATA_DIR_NAME) + + # Linux and others + xdg = os.environ.get("XDG_DATA_HOME", "") + if xdg: + return os.path.join(xdg, _DATA_DIR_NAME) + home = os.environ.get("HOME", "") + if not home: + return "" + return os.path.join(home, ".local", "share", _DATA_DIR_NAME) diff --git a/antd-py/src/antd/_grpc.py b/antd-py/src/antd/_grpc.py index 397a270..8f366a0 100644 --- a/antd-py/src/antd/_grpc.py +++ b/antd-py/src/antd/_grpc.py @@ -56,6 +56,21 @@ def _handle_rpc_error(e: grpc.RpcError) -> None: class GrpcClient: """Synchronous gRPC client for the antd daemon.""" + DEFAULT_TARGET = "localhost:50051" + + @classmethod + def auto_discover(cls, **kwargs) -> tuple["GrpcClient", str]: + """Create a client using daemon port discovery, falling back to the default target. + + Returns: + A tuple of ``(client, resolved_target)`` where *resolved_target* is + the gRPC target that was actually used (discovered or default). + """ + from ._discover import discover_grpc_target + + target = discover_grpc_target() or cls.DEFAULT_TARGET + return cls(target=target, **kwargs), target + def __init__(self, target: str = "localhost:50051"): self._channel = grpc.insecure_channel(target) self._health = health_pb2_grpc.HealthServiceStub(self._channel) @@ -256,6 +271,21 @@ def file_cost(self, path: str, is_public: bool = True, include_archive: bool = F class AsyncGrpcClient: """Asynchronous gRPC client for the antd daemon.""" + DEFAULT_TARGET = "localhost:50051" + + @classmethod + def auto_discover(cls, **kwargs) -> tuple["AsyncGrpcClient", str]: + """Create a client using daemon port discovery, falling back to the default target. + + Returns: + A tuple of ``(client, resolved_target)`` where *resolved_target* is + the gRPC target that was actually used (discovered or default). + """ + from ._discover import discover_grpc_target + + target = discover_grpc_target() or cls.DEFAULT_TARGET + return cls(target=target, **kwargs), target + def __init__(self, target: str = "localhost:50051"): self._channel = grpc.aio.insecure_channel(target) self._health = health_pb2_grpc.HealthServiceStub(self._channel) diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index de64ef1..3d8e6ff 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -43,10 +43,25 @@ def _check(resp: httpx.Response) -> None: class RestClient: """Synchronous REST client for the antd daemon.""" - def __init__(self, base_url: str = "http://localhost:8080", timeout: float = 300.0): + DEFAULT_BASE_URL = "http://localhost:8082" + + def __init__(self, base_url: str = "http://localhost:8082", timeout: float = 300.0): self._base = base_url.rstrip("/") self._http = httpx.Client(base_url=self._base, timeout=timeout) + @classmethod + def auto_discover(cls, **kwargs) -> tuple["RestClient", str]: + """Create a client using daemon port discovery, falling back to the default URL. + + Returns: + A tuple of ``(client, resolved_url)`` where *resolved_url* is the + URL that was actually used (discovered or default). + """ + from ._discover import discover_daemon_url + + url = discover_daemon_url() or cls.DEFAULT_BASE_URL + return cls(base_url=url, **kwargs), url + def close(self) -> None: self._http.close() @@ -210,10 +225,25 @@ def file_cost(self, path: str, is_public: bool = True, include_archive: bool = F class AsyncRestClient: """Asynchronous REST client for the antd daemon.""" - def __init__(self, base_url: str = "http://localhost:8080", timeout: float = 300.0): + DEFAULT_BASE_URL = "http://localhost:8082" + + def __init__(self, base_url: str = "http://localhost:8082", timeout: float = 300.0): self._base = base_url.rstrip("/") self._http = httpx.AsyncClient(base_url=self._base, timeout=timeout) + @classmethod + def auto_discover(cls, **kwargs) -> tuple["AsyncRestClient", str]: + """Create a client using daemon port discovery, falling back to the default URL. + + Returns: + A tuple of ``(client, resolved_url)`` where *resolved_url* is the + URL that was actually used (discovered or default). + """ + from ._discover import discover_daemon_url + + url = discover_daemon_url() or cls.DEFAULT_BASE_URL + return cls(base_url=url, **kwargs), url + async def close(self) -> None: await self._http.aclose() diff --git a/antd-py/tests/test_discover.py b/antd-py/tests/test_discover.py new file mode 100644 index 0000000..330c9c4 --- /dev/null +++ b/antd-py/tests/test_discover.py @@ -0,0 +1,128 @@ +"""Tests for antd._discover port-file discovery.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from antd._discover import ( + _data_dir, + _read_port_file, + discover_daemon_url, + discover_grpc_target, +) + + +def _write_port_file(tmp_path: Path, content: str, monkeypatch) -> None: + """Write a daemon.port file under tmp_path/ant/ and point env vars at it.""" + ant_dir = tmp_path / "ant" + ant_dir.mkdir(exist_ok=True) + (ant_dir / "daemon.port").write_text(content, encoding="utf-8") + # Use XDG_DATA_HOME on all platforms for test isolation + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + # Force sys.platform to linux so XDG_DATA_HOME is used + monkeypatch.setattr("sys.platform", "linux") + + +class TestDiscoverDaemonUrl: + def test_valid_file_both_lines(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\n50051\n", monkeypatch) + assert discover_daemon_url() == "http://127.0.0.1:8082" + + def test_valid_file_single_line(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "9000\n", monkeypatch) + assert discover_daemon_url() == "http://127.0.0.1:9000" + + def test_missing_file(self, tmp_path, monkeypatch): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + monkeypatch.setattr("sys.platform", "linux") + # No daemon.port file created + assert discover_daemon_url() == "" + + def test_invalid_content(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "not_a_number\n", monkeypatch) + assert discover_daemon_url() == "" + + def test_empty_file(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "", monkeypatch) + assert discover_daemon_url() == "" + + def test_whitespace_handling(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, " 8082 \n 50051 \n", monkeypatch) + assert discover_daemon_url() == "http://127.0.0.1:8082" + + def test_port_zero(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "0\n50051\n", monkeypatch) + assert discover_daemon_url() == "" + + def test_port_out_of_range(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "99999\n50051\n", monkeypatch) + assert discover_daemon_url() == "" + + +class TestDiscoverGrpcTarget: + def test_valid_file_both_lines(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\n50051\n", monkeypatch) + assert discover_grpc_target() == "127.0.0.1:50051" + + def test_single_line_no_grpc(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\n", monkeypatch) + assert discover_grpc_target() == "" + + def test_missing_file(self, tmp_path, monkeypatch): + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + monkeypatch.setattr("sys.platform", "linux") + assert discover_grpc_target() == "" + + def test_invalid_grpc_line(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\nabc\n", monkeypatch) + assert discover_grpc_target() == "" + + def test_whitespace_handling(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, " 8082 \n 50051 \n", monkeypatch) + assert discover_grpc_target() == "127.0.0.1:50051" + + +class TestDataDir: + def test_windows(self, monkeypatch): + monkeypatch.setattr("sys.platform", "win32") + monkeypatch.setenv("APPDATA", "C:\\Users\\test\\AppData\\Roaming") + result = _data_dir() + assert result == os.path.join("C:\\Users\\test\\AppData\\Roaming", "ant") + + def test_darwin(self, monkeypatch): + monkeypatch.setattr("sys.platform", "darwin") + monkeypatch.setenv("HOME", "/Users/test") + result = _data_dir() + assert result == os.path.join("/Users/test", "Library", "Application Support", "ant") + + def test_linux_xdg(self, monkeypatch): + monkeypatch.setattr("sys.platform", "linux") + monkeypatch.setenv("XDG_DATA_HOME", "/custom/data") + result = _data_dir() + assert result == os.path.join("/custom/data", "ant") + + def test_linux_no_xdg(self, monkeypatch): + monkeypatch.setattr("sys.platform", "linux") + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + monkeypatch.setenv("HOME", "/home/test") + result = _data_dir() + assert result == os.path.join("/home/test", ".local", "share", "ant") + + def test_linux_no_home(self, monkeypatch): + monkeypatch.setattr("sys.platform", "linux") + monkeypatch.delenv("XDG_DATA_HOME", raising=False) + monkeypatch.delenv("HOME", raising=False) + assert _data_dir() == "" + + def test_windows_no_appdata(self, monkeypatch): + monkeypatch.setattr("sys.platform", "win32") + monkeypatch.delenv("APPDATA", raising=False) + assert _data_dir() == "" + + def test_darwin_no_home(self, monkeypatch): + monkeypatch.setattr("sys.platform", "darwin") + monkeypatch.delenv("HOME", raising=False) + assert _data_dir() == "" diff --git a/antd-ruby/lib/antd.rb b/antd-ruby/lib/antd.rb index 966ee9f..f4dc727 100644 --- a/antd-ruby/lib/antd.rb +++ b/antd-ruby/lib/antd.rb @@ -3,6 +3,7 @@ require_relative "antd/version" require_relative "antd/models" require_relative "antd/errors" +require_relative "antd/discover" require_relative "antd/client" # gRPC client is optional — requires the `grpc` gem and proto-generated stubs. diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index 0dcd18f..3a3f6ad 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -6,11 +6,24 @@ require "uri" module Antd - DEFAULT_BASE_URL = "http://localhost:8080" + DEFAULT_BASE_URL = "http://localhost:8082" DEFAULT_TIMEOUT = 300 # seconds # REST client for the antd daemon. class Client + # Creates a client using port discovery. + # + # Reads the daemon.port file to find the REST port. Falls back to the + # default base URL if the port file is not found. + # + # @param kwargs [Hash] options passed to +initialize+ (e.g. +:timeout+) + # @return [Array(Client, String)] the client and the resolved URL + def self.auto_discover(**kwargs) + url = Antd::Discover.daemon_url + url = DEFAULT_BASE_URL if url.empty? + [new(base_url: url, **kwargs), url] + end + # @param base_url [String] Base URL of the antd daemon # @param timeout [Integer] HTTP request timeout in seconds def initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT) diff --git a/antd-ruby/lib/antd/discover.rb b/antd-ruby/lib/antd/discover.rb new file mode 100644 index 0000000..c76d768 --- /dev/null +++ b/antd-ruby/lib/antd/discover.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Antd + # Auto-discovers the antd daemon by reading the +daemon.port+ file that antd + # writes on startup. + # + # The file contains two lines: REST port on line 1, gRPC port on line 2. + # + # Port file location is platform-specific: + # - Windows: %APPDATA%\ant\daemon.port + # - macOS: ~/Library/Application Support/ant/daemon.port + # - Linux: $XDG_DATA_HOME/ant/daemon.port or ~/.local/share/ant/daemon.port + module Discover + PORT_FILE_NAME = "daemon.port" + DATA_DIR_NAME = "ant" + + # Reads the daemon.port file and returns the REST base URL + # (e.g. "http://127.0.0.1:8082"). + # + # @return [String] the URL, or "" if the port file is not found + def self.daemon_url + rest, _ = read_port_file + return "" if rest == 0 + + "http://127.0.0.1:#{rest}" + end + + # Reads the daemon.port file and returns the gRPC target + # (e.g. "127.0.0.1:50051"). + # + # @return [String] the target, or "" if the port file is not found + def self.grpc_target + _, grpc = read_port_file + return "" if grpc == 0 + + "127.0.0.1:#{grpc}" + end + + # @api private + def self.read_port_file + dir = data_dir + return [0, 0] if dir.empty? + + path = File.join(dir, PORT_FILE_NAME) + return [0, 0] unless File.exist?(path) + + lines = File.read(path).strip.split("\n") + rest_port = parse_port(lines[0]) + grpc_port = parse_port(lines[1]) + [rest_port, grpc_port] + rescue StandardError + [0, 0] + end + + # @api private + def self.parse_port(str) + return 0 if str.nil? + + s = str.strip + return 0 unless s.match?(/\A\d+\z/) + + n = s.to_i + (n > 0 && n <= 65535) ? n : 0 + end + + # @api private + def self.data_dir + host_os = RbConfig::CONFIG["host_os"] + + case host_os + when /mswin|mingw|cygwin/ + appdata = ENV["APPDATA"] + return "" if appdata.nil? || appdata.empty? + + File.join(appdata, DATA_DIR_NAME) + when /darwin/ + home = ENV["HOME"] + return "" if home.nil? || home.empty? + + File.join(home, "Library", "Application Support", DATA_DIR_NAME) + else + xdg = ENV["XDG_DATA_HOME"] + if xdg && !xdg.empty? + File.join(xdg, DATA_DIR_NAME) + else + home = ENV["HOME"] + return "" if home.nil? || home.empty? + + File.join(home, ".local", "share", DATA_DIR_NAME) + end + end + end + + private_class_method :read_port_file, :parse_port, :data_dir + end +end diff --git a/antd-ruby/lib/antd/grpc_client.rb b/antd-ruby/lib/antd/grpc_client.rb index 3e40d5e..aa5d618 100644 --- a/antd-ruby/lib/antd/grpc_client.rb +++ b/antd-ruby/lib/antd/grpc_client.rb @@ -25,6 +25,18 @@ module Antd # Provides the same 19 methods as the REST +Client+, but communicates over # gRPC using the proto-generated stubs from +antd/v1/*.proto+. class GrpcClient + # Creates a gRPC client using port discovery. + # + # Reads the daemon.port file to find the gRPC port. Falls back to the + # default target if the port file is not found. + # + # @return [Array(GrpcClient, String)] the client and the resolved target + def self.auto_discover + target = Antd::Discover.grpc_target + target = DEFAULT_GRPC_TARGET if target.empty? + [new(target: target), target] + end + # @param target [String] gRPC target address (default: "localhost:50051") def initialize(target: DEFAULT_GRPC_TARGET) @target = target diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index 96fdaf5..133928e 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -5,6 +5,7 @@ use base64::Engine; use reqwest; use serde_json::{json, Value}; +use crate::discover::discover_daemon_url; use crate::errors::{error_for_status, AntdError}; use crate::models::*; @@ -25,7 +26,7 @@ fn url_encode(s: &str) -> String { } /// Default base URL of the antd daemon. -pub const DEFAULT_BASE_URL: &str = "http://localhost:8080"; +pub const DEFAULT_BASE_URL: &str = "http://localhost:8082"; /// Default request timeout (5 minutes). pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(300); @@ -43,6 +44,22 @@ impl Client { Self::with_timeout(base_url, DEFAULT_TIMEOUT) } + /// Creates a client by auto-discovering the daemon port file, falling back + /// to [`DEFAULT_BASE_URL`] if discovery fails. + pub fn auto_discover() -> Self { + let url = discover_daemon_url() + .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()); + Self::new(&url) + } + + /// Like [`auto_discover`](Self::auto_discover) but with a custom request + /// timeout. + pub fn auto_discover_with_timeout(timeout: Duration) -> Self { + let url = discover_daemon_url() + .unwrap_or_else(|| DEFAULT_BASE_URL.to_string()); + Self::with_timeout(&url, timeout) + } + /// Creates a new client with the given base URL and custom timeout. pub fn with_timeout(base_url: &str, timeout: Duration) -> Self { let http = reqwest::Client::builder() diff --git a/antd-rust/src/discover.rs b/antd-rust/src/discover.rs new file mode 100644 index 0000000..52d70ef --- /dev/null +++ b/antd-rust/src/discover.rs @@ -0,0 +1,112 @@ +//! Port discovery for the antd daemon. +//! +//! The antd daemon writes a `daemon.port` file on startup containing the REST +//! port on line 1 and the gRPC port on line 2. These helpers read that file +//! to auto-discover the daemon without hard-coding a port. + +use std::env; +use std::fs; +use std::path::PathBuf; + +const PORT_FILE_NAME: &str = "daemon.port"; +const DATA_DIR_NAME: &str = "ant"; + +/// Reads the daemon port file and returns the REST base URL +/// (e.g. `"http://127.0.0.1:8082"`), or `None` if the file is missing or +/// unreadable. +pub fn discover_daemon_url() -> Option { + let (rest, _) = read_port_file()?; + Some(format!("http://127.0.0.1:{rest}")) +} + +/// Reads the daemon port file and returns the gRPC target URL +/// (e.g. `"http://127.0.0.1:50051"`), or `None` if the file is missing or +/// has no gRPC line. +pub fn discover_grpc_target() -> Option { + let (_, grpc) = read_port_file()?; + let grpc = grpc?; + Some(format!("http://127.0.0.1:{grpc}")) +} + +/// Reads the `daemon.port` file and returns `(rest_port, Option)`. +fn read_port_file() -> Option<(u16, Option)> { + let dir = data_dir()?; + let path = dir.join(PORT_FILE_NAME); + let contents = fs::read_to_string(path).ok()?; + + let mut lines = contents.trim().lines(); + + let rest: u16 = lines.next()?.trim().parse().ok()?; + let grpc: Option = lines.next().and_then(|l| l.trim().parse().ok()); + + Some((rest, grpc)) +} + +/// Returns the platform-specific data directory for ant. +/// +/// - Windows: `%APPDATA%\ant` +/// - macOS: `~/Library/Application Support/ant` +/// - Linux: `$XDG_DATA_HOME/ant` or `~/.local/share/ant` +fn data_dir() -> Option { + #[cfg(target_os = "windows")] + { + let appdata = env::var("APPDATA").ok()?; + Some(PathBuf::from(appdata).join(DATA_DIR_NAME)) + } + + #[cfg(target_os = "macos")] + { + let home = env::var("HOME").ok()?; + Some(PathBuf::from(home).join("Library").join("Application Support").join(DATA_DIR_NAME)) + } + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + if let Ok(xdg) = env::var("XDG_DATA_HOME") { + return Some(PathBuf::from(xdg).join(DATA_DIR_NAME)); + } + let home = env::var("HOME").ok()?; + Some(PathBuf::from(home).join(".local").join("share").join(DATA_DIR_NAME)) + } +} + +#[cfg(test)] +mod tests { + /// Simulate the same parsing logic used in `read_port_file`. + fn parse_port_contents(contents: &str) -> Option<(u16, Option)> { + let mut lines = contents.trim().lines(); + let rest: u16 = lines.next()?.trim().parse().ok()?; + let grpc: Option = lines.next().and_then(|l| l.trim().parse().ok()); + Some((rest, grpc)) + } + + #[test] + fn parse_two_line_port_file() { + let result = parse_port_contents("8082\n50051\n"); + assert_eq!(result, Some((8082, Some(50051)))); + } + + #[test] + fn parse_single_line_port_file() { + let result = parse_port_contents("8082\n"); + assert_eq!(result, Some((8082, None))); + } + + #[test] + fn parse_empty_returns_none() { + let result = parse_port_contents(""); + assert_eq!(result, None); + } + + #[test] + fn parse_invalid_port_returns_none() { + let result = parse_port_contents("notanumber\n"); + assert_eq!(result, None); + } + + #[test] + fn parse_with_whitespace() { + let result = parse_port_contents(" 8082 \n 50051 \n"); + assert_eq!(result, Some((8082, Some(50051)))); + } +} diff --git a/antd-rust/src/grpc_client.rs b/antd-rust/src/grpc_client.rs index 723d7bb..05fa05d 100644 --- a/antd-rust/src/grpc_client.rs +++ b/antd-rust/src/grpc_client.rs @@ -1,5 +1,6 @@ use tonic::transport::{Channel, Endpoint}; +use crate::discover::discover_grpc_target; use crate::errors::AntdError; use crate::models::*; @@ -44,6 +45,14 @@ impl GrpcClient { Self::connect(endpoint).await } + /// Creates a gRPC client by auto-discovering the daemon port file, + /// falling back to [`DEFAULT_GRPC_ENDPOINT`] if discovery fails. + pub async fn auto_discover() -> Result { + let endpoint = discover_grpc_target() + .unwrap_or_else(|| DEFAULT_GRPC_ENDPOINT.to_string()); + Self::connect(&endpoint).await + } + /// Connects to the antd gRPC server at the given endpoint. pub async fn connect(endpoint: &str) -> Result { let channel = Endpoint::from_shared(endpoint.to_string()) diff --git a/antd-rust/src/lib.rs b/antd-rust/src/lib.rs index 97ee375..aed7c6e 100644 --- a/antd-rust/src/lib.rs +++ b/antd-rust/src/lib.rs @@ -22,6 +22,7 @@ //! ``` pub mod client; +pub mod discover; pub mod errors; pub mod grpc_client; pub mod models; @@ -33,6 +34,7 @@ mod tests; mod grpc_tests; pub use client::{Client, DEFAULT_BASE_URL, DEFAULT_TIMEOUT}; +pub use discover::{discover_daemon_url, discover_grpc_target}; pub use errors::AntdError; pub use grpc_client::{GrpcClient, DEFAULT_GRPC_ENDPOINT}; pub use models::*; diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index b38bb40..6ae6250 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -5,7 +5,7 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { private let baseURL: String private let session: URLSession - public init(baseURL: String = "http://localhost:8080", timeout: TimeInterval = 300) { + public init(baseURL: String = "http://localhost:8082", timeout: TimeInterval = 300) { self.baseURL = baseURL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = timeout diff --git a/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift b/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift new file mode 100644 index 0000000..01a0150 --- /dev/null +++ b/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift @@ -0,0 +1,93 @@ +import Foundation + +/// Discovers the antd daemon by reading the `daemon.port` file written on startup. +/// +/// The port file contains two lines: REST port on line 1, gRPC port on line 2. +/// File location is platform-specific: +/// - macOS: `~/Library/Application Support/ant/daemon.port` +/// - Linux: `$XDG_DATA_HOME/ant/daemon.port` or `~/.local/share/ant/daemon.port` +/// - Windows: `%APPDATA%\ant\daemon.port` +public enum DaemonDiscovery { + + private static let portFileName = "daemon.port" + private static let dataDirName = "ant" + + /// Reads the daemon.port file and returns the REST base URL + /// (e.g. `"http://127.0.0.1:8082"`). Returns `""` if unavailable. + public static func discoverDaemonUrl() -> String { + guard let (rest, _) = readPortFile(), rest > 0 else { return "" } + return "http://127.0.0.1:\(rest)" + } + + /// Reads the daemon.port file and returns the gRPC target + /// (e.g. `"127.0.0.1:50051"`). Returns `""` if unavailable. + public static func discoverGrpcTarget() -> String { + guard let (_, grpc) = readPortFile(), grpc > 0 else { return "" } + return "127.0.0.1:\(grpc)" + } + + /// Create an ``AntdRestClient`` using the discovered daemon URL. + /// Falls back to `http://localhost:8082` if discovery fails. + /// + /// - Parameter timeout: Request timeout in seconds (default 300). + /// - Returns: A tuple of the client and the URL it connected to. + public static func autoDiscover(timeout: TimeInterval = 300) -> (client: AntdRestClient, url: String) { + var url = discoverDaemonUrl() + if url.isEmpty { + url = "http://localhost:8082" + } + let client = AntdRestClient(baseURL: url, timeout: timeout) + return (client, url) + } + + /// Create an ``AntdGrpcClient`` using the discovered gRPC target. + /// Falls back to `localhost:50051` if discovery fails. + /// + /// - Returns: A tuple of the client and the target it connected to. + public static func autoDiscoverGrpc() -> (client: AntdGrpcClient, target: String) { + var target = discoverGrpcTarget() + if target.isEmpty { + target = "localhost:50051" + } + let client = AntdGrpcClient(target: target) + return (client, target) + } + + // MARK: - Private + + private static func readPortFile() -> (rest: UInt16, grpc: UInt16)? { + guard let dir = dataDir() else { return nil } + let path = (dir as NSString).appendingPathComponent(portFileName) + guard let contents = try? String(contentsOfFile: path, encoding: .utf8) else { return nil } + + let lines = contents.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "\n") + guard !lines.isEmpty else { return nil } + + let rest = parsePort(lines[0]) + let grpc = lines.count >= 2 ? parsePort(lines[1]) : 0 + return (rest, grpc) + } + + private static func parsePort(_ s: String) -> UInt16 { + UInt16(s.trimmingCharacters(in: .whitespaces)) ?? 0 + } + + private static func dataDir() -> String? { + #if os(macOS) + guard let home = ProcessInfo.processInfo.environment["HOME"], !home.isEmpty else { return nil } + return (home as NSString).appendingPathComponent("Library/Application Support/\(dataDirName)") + #elseif os(Linux) + if let xdg = ProcessInfo.processInfo.environment["XDG_DATA_HOME"], !xdg.isEmpty { + return (xdg as NSString).appendingPathComponent(dataDirName) + } + guard let home = ProcessInfo.processInfo.environment["HOME"], !home.isEmpty else { return nil } + return (home as NSString).appendingPathComponent(".local/share/\(dataDirName)") + #elseif os(Windows) + guard let appdata = ProcessInfo.processInfo.environment["APPDATA"], !appdata.isEmpty else { return nil } + return (appdata as NSString).appendingPathComponent(dataDirName) + #else + return nil + #endif + } +} diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index eac71d5..2c42927 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -5,6 +5,7 @@ const http = std.http; pub const models = @import("models.zig"); pub const errors = @import("errors.zig"); pub const json_helpers = @import("json_helpers.zig"); +pub const discover = @import("discover.zig"); pub const HealthStatus = models.HealthStatus; pub const PutResult = models.PutResult; @@ -16,9 +17,11 @@ pub const AntdError = errors.AntdError; pub const ErrorInfo = errors.ErrorInfo; pub const errorForStatus = errors.errorForStatus; pub const JsonValue = json_helpers.JsonValue; +pub const discoverDaemonUrl = discover.discoverDaemonUrl; +pub const discoverGrpcTarget = discover.discoverGrpcTarget; /// Default antd daemon address. -pub const default_base_url = "http://localhost:8080"; +pub const default_base_url = "http://localhost:8082"; /// REST client for the antd daemon. pub const Client = struct { @@ -36,6 +39,17 @@ pub const Client = struct { }; } + /// Create a client using daemon port discovery. + /// Falls back to the default base URL if discovery fails. + /// Note: if a discovered URL is returned, the caller owns that memory. + pub fn autoDiscover(allocator: Allocator) Client { + const url = discover.discoverDaemonUrl(allocator); + return .{ + .allocator = allocator, + .base_url = url orelse default_base_url, + }; + } + /// Clean up client resources. pub fn deinit(self: *Client) void { if (self.last_error) |info| { diff --git a/antd-zig/src/discover.zig b/antd-zig/src/discover.zig new file mode 100644 index 0000000..68c7d15 --- /dev/null +++ b/antd-zig/src/discover.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); + +const port_file_name = "daemon.port"; +const data_dir_name = "ant"; + +/// Reads the daemon.port file written by antd on startup and returns +/// the REST base URL (e.g. "http://127.0.0.1:8082"). +/// Returns null if the port file is not found or unreadable. +/// Caller owns the returned memory. +pub fn discoverDaemonUrl(allocator: Allocator) ?[]const u8 { + const ports = readPortFile(allocator) orelse return null; + if (ports.rest == 0) return null; + return std.fmt.allocPrint(allocator, "http://127.0.0.1:{d}", .{ports.rest}) catch null; +} + +/// Reads the daemon.port file written by antd on startup and returns +/// the gRPC target (e.g. "127.0.0.1:50051"). +/// Returns null if the port file is not found or has no gRPC line. +/// Caller owns the returned memory. +pub fn discoverGrpcTarget(allocator: Allocator) ?[]const u8 { + const ports = readPortFile(allocator) orelse return null; + if (ports.grpc == 0) return null; + return std.fmt.allocPrint(allocator, "127.0.0.1:{d}", .{ports.grpc}) catch null; +} + +const Ports = struct { + rest: u16, + grpc: u16, +}; + +fn readPortFile(allocator: Allocator) ?Ports { + const dir = dataDir(allocator) orelse return null; + defer allocator.free(dir); + + const path = std.fs.path.join(allocator, &.{ dir, port_file_name }) catch return null; + defer allocator.free(path); + + const file = std.fs.openFileAbsolute(path, .{}) catch return null; + defer file.close(); + + var buf: [256]u8 = undefined; + const n = file.readAll(&buf) catch return null; + if (n == 0) return null; + + const contents = std.mem.trimRight(u8, buf[0..n], &.{ ' ', '\t', '\r', '\n' }); + + var rest: u16 = 0; + var grpc: u16 = 0; + + var line_iter = std.mem.splitSequence(u8, contents, "\n"); + if (line_iter.next()) |first_line| { + rest = parsePort(first_line); + } + if (line_iter.next()) |second_line| { + grpc = parsePort(second_line); + } + + return .{ .rest = rest, .grpc = grpc }; +} + +fn parsePort(s: []const u8) u16 { + const trimmed = std.mem.trim(u8, s, &.{ ' ', '\t', '\r' }); + return std.fmt.parseInt(u16, trimmed, 10) catch 0; +} + +fn dataDir(allocator: Allocator) ?[]const u8 { + switch (builtin.os.tag) { + .windows => { + const appdata = std.process.getEnvVarOwned(allocator, "APPDATA") catch return null; + defer allocator.free(appdata); + if (appdata.len == 0) return null; + return std.fs.path.join(allocator, &.{ appdata, data_dir_name }) catch null; + }, + .macos => { + const home = std.process.getEnvVarOwned(allocator, "HOME") catch return null; + defer allocator.free(home); + if (home.len == 0) return null; + return std.fs.path.join(allocator, &.{ home, "Library", "Application Support", data_dir_name }) catch null; + }, + else => { + // Linux and other Unix-like systems + if (std.process.getEnvVarOwned(allocator, "XDG_DATA_HOME")) |xdg| { + defer allocator.free(xdg); + if (xdg.len > 0) { + return std.fs.path.join(allocator, &.{ xdg, data_dir_name }) catch null; + } + } else |_| {} + const home = std.process.getEnvVarOwned(allocator, "HOME") catch return null; + defer allocator.free(home); + if (home.len == 0) return null; + return std.fs.path.join(allocator, &.{ home, ".local", "share", data_dir_name }) catch null; + }, + } +} diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 8b7261b..798f6ae 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -858,16 +858,68 @@ dependencies = [ "sha2", ] +[[package]] +name = "ant-node" +version = "0.5.0" +dependencies = [ + "aes-gcm-siv", + "ant-evm", + "blake3", + "bytes", + "chrono", + "clap", + "color-eyre", + "directories", + "evmlib", + "flate2", + "fs2", + "futures", + "heed", + "hex", + "hkdf", + "libp2p", + "lru", + "multihash", + "objc2", + "objc2-foundation", + "parking_lot", + "postcard", + "rand 0.8.5", + "reqwest 0.13.2", + "rmp-serde", + "saorsa-core", + "saorsa-pqc 0.5.0", + "self-replace", + "self_encryption", + "semver 1.0.27", + "serde", + "serde_json", + "sha2", + "tar", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", + "xor_name", + "zip", +] + [[package]] name = "antd" version = "0.2.0" dependencies = [ "ant-evm", + "ant-node", "axum 0.8.8", "base64", "blake3", "bytes", "clap", + "dirs 6.0.0", "evmlib", "futures", "hex", @@ -875,7 +927,6 @@ dependencies = [ "prost", "rand 0.8.5", "rmp-serde", - "saorsa-node", "serde", "serde_json", "thiserror 2.0.18", @@ -5410,56 +5461,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "saorsa-node" -version = "0.5.0" -dependencies = [ - "aes-gcm-siv", - "ant-evm", - "blake3", - "bytes", - "chrono", - "clap", - "color-eyre", - "directories", - "evmlib", - "flate2", - "fs2", - "futures", - "heed", - "hex", - "hkdf", - "libp2p", - "lru", - "multihash", - "objc2", - "objc2-foundation", - "parking_lot", - "postcard", - "rand 0.8.5", - "reqwest 0.13.2", - "rmp-serde", - "saorsa-core", - "saorsa-pqc 0.5.0", - "self-replace", - "self_encryption", - "semver 1.0.27", - "serde", - "serde_json", - "sha2", - "tar", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tokio-util", - "toml", - "tracing", - "tracing-appender", - "tracing-subscriber", - "xor_name", - "zip", -] - [[package]] name = "saorsa-pqc" version = "0.4.2" diff --git a/antd/Cargo.toml b/antd/Cargo.toml index 66c24ce..8fa86b5 100644 --- a/antd/Cargo.toml +++ b/antd/Cargo.toml @@ -12,7 +12,8 @@ tower-http = { version = "0.6", features = ["cors", "trace"] } tonic = "0.12" prost = "0.13" tokio = { version = "1", features = ["full"] } -tokio-stream = "0.1" +tokio-stream = { version = "0.1", features = ["net"] } +dirs = "6" serde = { version = "1", features = ["derive"] } serde_json = "1" hex = "0.4" diff --git a/antd/src/config.rs b/antd/src/config.rs index 9ec52da..b5184f3 100644 --- a/antd/src/config.rs +++ b/antd/src/config.rs @@ -11,6 +11,14 @@ pub struct Config { #[arg(long, default_value = "0.0.0.0:50051", env = "ANTD_GRPC_ADDR")] pub grpc_addr: String, + /// REST API port (overrides --rest-addr port; use 0 for OS-assigned) + #[arg(long, env = "ANTD_REST_PORT")] + pub rest_port: Option, + + /// gRPC port (overrides --grpc-addr port; use 0 for OS-assigned) + #[arg(long, env = "ANTD_GRPC_PORT")] + pub grpc_port: Option, + /// Network mode: default, local #[arg(long, default_value = "default", env = "ANTD_NETWORK")] pub network: String, @@ -23,3 +31,29 @@ pub struct Config { #[arg(long, default_value_t = false, env = "ANTD_CORS")] pub cors: bool, } + +impl Config { + /// Resolve the REST listen address, applying --rest-port override if set. + pub fn resolved_rest_addr(&self) -> Result { + let mut addr: std::net::SocketAddr = self + .rest_addr + .parse() + .map_err(|e| format!("invalid REST address: {e}"))?; + if let Some(port) = self.rest_port { + addr.set_port(port); + } + Ok(addr) + } + + /// Resolve the gRPC listen address, applying --grpc-port override if set. + pub fn resolved_grpc_addr(&self) -> Result { + let mut addr: std::net::SocketAddr = self + .grpc_addr + .parse() + .map_err(|e| format!("invalid gRPC address: {e}"))?; + if let Some(port) = self.grpc_port { + addr.set_port(port); + } + Ok(addr) + } +} diff --git a/antd/src/grpc/mod.rs b/antd/src/grpc/mod.rs index 1e6b0de..24e3704 100644 --- a/antd/src/grpc/mod.rs +++ b/antd/src/grpc/mod.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use tokio::net::TcpListener; +use tokio_stream::wrappers::TcpListenerStream; use tonic::transport::Server; use crate::state::AppState; @@ -15,7 +17,7 @@ use service::pb::{ health_service_server::HealthServiceServer, }; -pub async fn serve(addr: std::net::SocketAddr, state: Arc) -> Result<(), Box> { +pub async fn serve(listener: TcpListener, state: Arc) -> Result<(), Box> { let data_svc = DataServiceServer::new(service::DataServiceImpl { state: state.clone() }); let chunk_svc = ChunkServiceServer::new(service::ChunkServiceImpl { state: state.clone() }); let graph_svc = GraphServiceServer::new(service::GraphServiceImpl { state: state.clone() }); @@ -23,6 +25,7 @@ pub async fn serve(addr: std::net::SocketAddr, state: Arc) -> Result<( let event_svc = EventServiceServer::new(service::EventServiceImpl { state: state.clone() }); let health_svc = HealthServiceServer::new(service::HealthServiceImpl { network: state.network.clone() }); + let addr = listener.local_addr()?; tracing::info!("gRPC server listening on {addr}"); Server::builder() @@ -32,7 +35,7 @@ pub async fn serve(addr: std::net::SocketAddr, state: Arc) -> Result<( .add_service(graph_svc) .add_service(file_svc) .add_service(event_svc) - .serve(addr) + .serve_with_incoming(TcpListenerStream::new(listener)) .await?; Ok(()) diff --git a/antd/src/main.rs b/antd/src/main.rs index 7376704..a523ea9 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -8,6 +8,7 @@ use ant_node::core::{CoreNodeConfig, MultiAddr, NodeMode, P2PNode}; mod config; mod error; mod grpc; +mod port_file; mod rest; mod state; mod types; @@ -23,16 +24,36 @@ async fn main() -> Result<(), Box> { let config = Config::parse(); + // Resolve listen addresses (applying --rest-port / --grpc-port overrides) + let rest_addr = config.resolved_rest_addr()?; + let grpc_addr = config.resolved_grpc_addr()?; + + // Bind listeners early to capture actual ports (important for port 0) + let rest_listener = tokio::net::TcpListener::bind(rest_addr).await + .map_err(|e| format!("failed to bind REST listener on {rest_addr}: {e}"))?; + let grpc_listener = tokio::net::TcpListener::bind(grpc_addr).await + .map_err(|e| format!("failed to bind gRPC listener on {grpc_addr}: {e}"))?; + + let actual_rest_addr = rest_listener.local_addr()?; + let actual_grpc_addr = grpc_listener.local_addr()?; + // Banner println!(); println!(" antd — Autonomi REST + gRPC Gateway"); println!(" =================================="); - println!(" REST: http://{}", config.rest_addr); - println!(" gRPC: {}", config.grpc_addr); + println!(" REST: http://{}", actual_rest_addr); + println!(" gRPC: {}", actual_grpc_addr); println!(" Network: {}", config.network); println!(" CORS: {}", if config.cors { "enabled" } else { "disabled" }); println!(); + // Write port file for SDK discovery + let port_file_path = port_file::write(actual_rest_addr.port(), actual_grpc_addr.port()); + match &port_file_path { + Some(p) => tracing::info!(path = %p.display(), "port file written"), + None => tracing::warn!("could not determine data directory — port file not written"), + } + // Parse bootstrap peers let bootstrap_peers: Vec = config .peers @@ -145,31 +166,20 @@ async fn main() -> Result<(), Box> { wallet, }); - // Parse addresses - let rest_addr: std::net::SocketAddr = config - .rest_addr - .parse() - .map_err(|e| format!("invalid REST address: {e}"))?; - let grpc_addr: std::net::SocketAddr = config - .grpc_addr - .parse() - .map_err(|e| format!("invalid gRPC address: {e}"))?; - // Build REST router let app = rest::router(state.clone(), config.cors); // Spawn both servers let grpc_state = state.clone(); let grpc_handle = tokio::spawn(async move { - if let Err(e) = grpc::serve(grpc_addr, grpc_state).await { + if let Err(e) = grpc::serve(grpc_listener, grpc_state).await { tracing::error!("gRPC server error: {e}"); } }); let rest_handle = tokio::spawn(async move { - tracing::info!("REST server listening on {rest_addr}"); - let listener = tokio::net::TcpListener::bind(rest_addr).await.unwrap(); - axum::serve(listener, app) + tracing::info!("REST server listening on {actual_rest_addr}"); + axum::serve(rest_listener, app) .with_graceful_shutdown(shutdown_signal()) .await .unwrap(); @@ -180,6 +190,10 @@ async fn main() -> Result<(), Box> { _ = grpc_handle => tracing::info!("gRPC server shut down"), } + // Cleanup port file on shutdown + port_file::remove(); + tracing::info!("port file removed"); + Ok(()) } diff --git a/antd/src/port_file.rs b/antd/src/port_file.rs new file mode 100644 index 0000000..fadd6cb --- /dev/null +++ b/antd/src/port_file.rs @@ -0,0 +1,65 @@ +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +const PORT_FILE_NAME: &str = "daemon.port"; +const DATA_DIR_NAME: &str = "ant"; + +/// Returns the platform-specific data directory for ant. +/// +/// - Windows: `%APPDATA%\ant` +/// - macOS: `~/Library/Application Support/ant` +/// - Linux: `$XDG_DATA_HOME/ant` or `~/.local/share/ant` +fn data_dir() -> Option { + dirs::data_dir().map(|d| d.join(DATA_DIR_NAME)) +} + +/// Returns the full path to the port file. +fn port_file_path() -> Option { + data_dir().map(|d| d.join(PORT_FILE_NAME)) +} + +/// Writes the port file atomically. +/// +/// Format: two lines — REST port on line 1, gRPC port on line 2. +/// Writes to a temp file first then renames for atomicity. +pub fn write(rest_port: u16, grpc_port: u16) -> Option { + let dir = data_dir()?; + if let Err(e) = fs::create_dir_all(&dir) { + tracing::warn!(path = %dir.display(), error = %e, "failed to create data directory"); + return None; + } + + let target = dir.join(PORT_FILE_NAME); + let tmp = dir.join(format!("{PORT_FILE_NAME}.tmp")); + + let contents = format!("{rest_port}\n{grpc_port}\n"); + + let result = (|| -> std::io::Result<()> { + let mut f = fs::File::create(&tmp)?; + f.write_all(contents.as_bytes())?; + f.sync_all()?; + fs::rename(&tmp, &target)?; + Ok(()) + })(); + + match result { + Ok(()) => Some(target), + Err(e) => { + tracing::warn!(path = %target.display(), error = %e, "failed to write port file"); + let _ = fs::remove_file(&tmp); + None + } + } +} + +/// Removes the port file. Best-effort; logs on failure. +pub fn remove() { + if let Some(path) = port_file_path() { + if path.exists() { + if let Err(e) = fs::remove_file(&path) { + tracing::warn!(path = %path.display(), error = %e, "failed to remove port file"); + } + } + } +} diff --git a/llms-full.txt b/llms-full.txt index bfee75d..fc04fca 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,15 +1,45 @@ # antd SDK — Complete API Reference -> JavaScript/TypeScript, Python, C#, Kotlin, Swift, Go, Java, Rust, C++, Ruby, PHP, Dart, Lua, Elixir, and Zig SDKs for the Autonomi network via the antd daemon. The daemon manages wallet, payments, and network connectivity. Clients connect over REST (default port 8080) or gRPC (default port 50051). +> JavaScript/TypeScript, Python, C#, Kotlin, Swift, Go, Java, Rust, C++, Ruby, PHP, Dart, Lua, Elixir, and Zig SDKs for the Autonomi network via the antd daemon. The daemon manages wallet, payments, and network connectivity. Clients connect over REST (default port 8082) or gRPC (default port 50051). All SDKs support automatic port discovery. + +## Port Discovery + +All SDKs can automatically discover a running antd daemon. On startup, antd writes a `daemon.port` file containing the REST port (line 1) and gRPC port (line 2) to a platform-specific location: + +- **Windows:** `%APPDATA%\ant\daemon.port` +- **Linux:** `$XDG_DATA_HOME/ant/daemon.port` (or `~/.local/share/ant/daemon.port`) +- **macOS:** `~/Library/Application Support/ant/daemon.port` + +Every SDK provides auto-discover constructors that read this file and connect automatically, falling back to the default ports if no file is found: + +| Language | REST Auto-Discover | gRPC Auto-Discover | +|----------|-------------------|-------------------| +| Python | `RestClient.auto_discover()` | `GrpcClient.auto_discover()` | +| Go | `antd.NewClientAutoDiscover()` | `antd.NewGrpcClientAutoDiscover()` | +| TypeScript | `RestClient.autoDiscover()` | N/A | +| C# | `AntdRestClient.AutoDiscover()` | `AntdGrpcClient.AutoDiscover()` | +| Java | `AntdClient.autoDiscover()` | `GrpcAntdClient.autoDiscover()` | +| Rust | `Client::auto_discover()` | `GrpcClient::auto_discover()` | +| Kotlin | `AntdRestClient.autoDiscover()` | `AntdGrpcClient.autoDiscover()` | +| C++ | `Client::auto_discover()` | `GrpcClient::auto_discover()` | +| Dart | `AntdClient.autoDiscover()` | `GrpcAntdClient.autoDiscover()` | +| Elixir | `Antd.Client.auto_discover()` | `Antd.GrpcClient.auto_discover()` | +| Ruby | `Antd::Client.auto_discover` | `Antd::GrpcClient.auto_discover` | +| Swift | `DaemonDiscovery.autoDiscover()` | `DaemonDiscovery.autoDiscoverGrpc()` | +| PHP | `AntdClient::autoDiscover()` | N/A | +| Lua | `Client.auto_discover()` | N/A | +| Zig | `Client.autoDiscover(allocator)` | N/A | + +This is especially useful when antd is spawned with `--rest-port 0` (OS assigns a free port), as in managed mode. ## Quick Start ```python from antd import AntdClient -client = AntdClient() # REST, localhost:8080 +client = AntdClient() # REST, localhost:8082 client = AntdClient(transport="grpc") # gRPC, localhost:50051 -client = AntdClient(base_url="http://remote:8080") +client = AntdClient(base_url="http://remote:8082") # Store and retrieve data result = client.data_put_public(b"hello world") @@ -20,8 +50,8 @@ data = client.data_get_public(result.address) # b"hello world" ```typescript import { createClient } from "antd"; -const client = createClient(); // REST, localhost:8080 -const client = createClient({ baseUrl: "http://remote:8080" }); +const client = createClient(); // REST, localhost:8082 +const client = createClient({ baseUrl: "http://remote:8082" }); const result = await client.dataPutPublic(Buffer.from("hello")); const data = await client.dataGetPublic(result.address); @@ -30,7 +60,7 @@ const data = await client.dataGetPublic(result.address); ```csharp using Antd.Sdk; -var client = AntdClientFactory.CreateRest(); // REST, localhost:8080 +var client = AntdClientFactory.CreateRest(); // REST, localhost:8082 var client = AntdClientFactory.CreateGrpc(); // gRPC, localhost:50051 var result = await client.DataPutPublicAsync(Encoding.UTF8.GetBytes("hello")); @@ -42,7 +72,7 @@ import com.autonomi.sdk.* import kotlinx.coroutines.runBlocking fun main() = runBlocking { - val client = AntdClient.createRest() // REST, localhost:8080 + val client = AntdClient.createRest() // REST, localhost:8082 val client = AntdClient.createGrpc() // gRPC, localhost:50051 val result = client.dataPutPublic("hello".toByteArray()) @@ -54,7 +84,7 @@ fun main() = runBlocking { // Swift (macOS only — iOS apps should use the FFI bindings) import AntdSdk -let client = try AntdClient.createRest() // REST, localhost:8080 +let client = try AntdClient.createRest() // REST, localhost:8082 let client = try AntdClient.createGrpc() // gRPC, localhost:50051 let result = try await client.dataPutPublic("hello".data(using: .utf8)!) @@ -65,7 +95,7 @@ let data = try await client.dataGetPublic(address: result.address) // Go import antd "github.com/WithAutonomi/ant-sdk/antd-go" -client := antd.NewClient(antd.DefaultBaseURL) // REST, localhost:8080 +client := antd.NewClient(antd.DefaultBaseURL) // REST, localhost:8082 ctx := context.Background() result, _ := client.DataPutPublic(ctx, []byte("hello")) @@ -77,7 +107,7 @@ data, _ := client.DataGetPublic(ctx, result.Address) // Maven: com.autonomiantd-java0.1.0 import com.autonomi.antd.AntdClient; -try (AntdClient client = AntdClient.create()) { // REST, localhost:8080 +try (AntdClient client = AntdClient.create()) { // REST, localhost:8082 PutResult result = client.dataPutPublic("hello".getBytes()); byte[] data = client.dataGetPublic(result.address()); } @@ -89,7 +119,7 @@ use antd_client::Client; #[tokio::main] async fn main() -> Result<(), antd_client::AntdError> { - let client = Client::new("http://localhost:8080"); // REST, localhost:8080 + let client = Client::new("http://localhost:8082"); // REST, localhost:8082 let result = client.data_put_public(b"hello").await?; let data = client.data_get_public(&result.address).await?; @@ -101,8 +131,8 @@ async fn main() -> Result<(), antd_client::AntdError> { // C++ — CMake FetchContent from GitHub #include -antd::Client client; // REST, localhost:8080 -antd::Client client("http://remote:8080"); +antd::Client client; // REST, localhost:8082 +antd::Client client("http://remote:8082"); auto result = client.dataPutPublic("hello"); auto data = client.dataGetPublic(result.address); @@ -112,8 +142,8 @@ auto data = client.dataGetPublic(result.address); # Ruby — gem install antd require "antd" -client = Antd::Client.new # REST, localhost:8080 -client = Antd::Client.new(base_url: "http://remote:8080") +client = Antd::Client.new # REST, localhost:8082 +client = Antd::Client.new(base_url: "http://remote:8082") result = client.data_put_public("hello") data = client.data_get_public(result.address) @@ -124,8 +154,8 @@ data = client.data_get_public(result.address) dataPutPublic("hello"); $data = $client->dataGetPublic($result->address); @@ -135,8 +165,8 @@ $data = $client->dataGetPublic($result->address); // Dart — dart pub add antd import 'package:antd/antd.dart'; -final client = AntdClient(); // REST, localhost:8080 -final client = AntdClient(baseUrl: 'http://remote:8080'); +final client = AntdClient(); // REST, localhost:8082 +final client = AntdClient(baseUrl: 'http://remote:8082'); final result = await client.dataPutPublic('hello'.codeUnits); final data = await client.dataGetPublic(result.address); @@ -146,8 +176,8 @@ final data = await client.dataGetPublic(result.address); -- Lua — luarocks install antd local antd = require("antd") -local client = antd.Client.new() -- REST, localhost:8080 -local client = antd.Client.new({base_url = "http://remote:8080"}) +local client = antd.Client.new() -- REST, localhost:8082 +local client = antd.Client.new({base_url = "http://remote:8082"}) local result = client:data_put_public("hello") local data = client:data_get_public(result.address) @@ -155,8 +185,8 @@ local data = client:data_get_public(result.address) ```elixir # Elixir — {:antd, "~> 0.1"} in mix.exs deps -client = Antd.Client.new() # REST, localhost:8080 -client = Antd.Client.new(base_url: "http://remote:8080") +client = Antd.Client.new() # REST, localhost:8082 +client = Antd.Client.new(base_url: "http://remote:8082") {:ok, result} = Antd.Client.data_put_public(client, "hello") {:ok, data} = Antd.Client.data_get_public(client, result.address) @@ -166,7 +196,7 @@ client = Antd.Client.new(base_url: "http://remote:8080") // Zig — add dependency in build.zig.zon const antd = @import("antd"); -var client = try antd.Client.init(.{}); // REST, localhost:8080 +var client = try antd.Client.init(.{}); // REST, localhost:8082 defer client.deinit(); const result = try client.dataPutPublic("hello"); @@ -386,8 +416,8 @@ JS/TS, PHP, Lua, and Zig are REST-only. ```typescript import { createClient } from "antd"; -const client = createClient(); // REST, localhost:8080 -const client = createClient({ baseUrl: "http://remote:8080" }); // custom URL +const client = createClient(); // REST, localhost:8082 +const client = createClient({ baseUrl: "http://remote:8082" }); // custom URL const client = createClient({ timeout: 60_000 }); // custom timeout (ms) // Health @@ -427,7 +457,7 @@ client.fileCost(path: string, isPublic?: boolean, includeArchive?: boolean): Pro ```python from antd import AntdClient, AsyncAntdClient -client = AntdClient(transport="rest", base_url="http://localhost:8080") +client = AntdClient(transport="rest", base_url="http://localhost:8082") client = AntdClient(transport="grpc", target="localhost:50051") # Health @@ -467,7 +497,7 @@ client.file_cost(path: str, is_public: bool = True, include_archive: bool = Fals ```csharp using Antd.Sdk; -IAntdClient client = AntdClientFactory.CreateRest(); // localhost:8080 +IAntdClient client = AntdClientFactory.CreateRest(); // localhost:8082 IAntdClient client = AntdClientFactory.CreateGrpc(); // localhost:50051 // Health @@ -507,7 +537,7 @@ Task FileCostAsync(string path, bool isPublic = true, bool includeArchiv ```kotlin import com.autonomi.sdk.* -val client = AntdClient.createRest() // localhost:8080 +val client = AntdClient.createRest() // localhost:8082 val client = AntdClient.createGrpc() // localhost:50051 // Health @@ -549,7 +579,7 @@ suspend fun fileCost(path: String, isPublic: Boolean = true, includeArchive: Boo ```swift import AntdSdk -let client = try AntdClient.createRest() // localhost:8080 +let client = try AntdClient.createRest() // localhost:8082 let client = try AntdClient.createGrpc() // localhost:50051 // Health @@ -589,7 +619,7 @@ func fileCost(path: String, isPublic: Bool, includeArchive: Bool) async throws - ```go import antd "github.com/WithAutonomi/ant-sdk/antd-go" -client := antd.NewClient(antd.DefaultBaseURL) // localhost:8080 +client := antd.NewClient(antd.DefaultBaseURL) // localhost:8082 // Health func (c *Client) Health(ctx context.Context) (*HealthStatus, error) @@ -630,8 +660,8 @@ func (c *Client) FileCost(ctx context.Context, path string, isPublic bool, inclu // Maven: com.autonomiantd-java0.1.0 import com.autonomi.antd.AntdClient; -AntdClient client = AntdClient.create(); // REST, localhost:8080 -AntdClient client = AntdClient.create("http://remote:8080"); +AntdClient client = AntdClient.create(); // REST, localhost:8082 +AntdClient client = AntdClient.create("http://remote:8082"); // Health HealthStatus health() throws AntdException @@ -673,7 +703,7 @@ String fileCost(String path, boolean isPublic, boolean includeArchive) throws An // cargo add antd-client use antd_client::{Client, PutResult, HealthStatus, GraphEntry, GraphDescendant, Archive, AntdError}; -let client = Client::new("http://localhost:8080"); // REST, localhost:8080 +let client = Client::new("http://localhost:8082"); // REST, localhost:8082 // Health async fn health(&self) -> Result @@ -715,8 +745,8 @@ async fn file_cost(&self, path: &str, is_public: bool, include_archive: bool) -> // CMake FetchContent from GitHub #include -antd::Client client; // REST, localhost:8080 -antd::Client client("http://remote:8080"); +antd::Client client; // REST, localhost:8082 +antd::Client client("http://remote:8082"); // Health antd::HealthStatus health() // throws antd::AntdError @@ -758,8 +788,8 @@ std::string fileCost(const std::string& path, bool isPublic = true, bool include ```ruby require "antd" -client = Antd::Client.new # localhost:8080 -client = Antd::Client.new(base_url: "http://remote:8080") +client = Antd::Client.new # localhost:8082 +client = Antd::Client.new(base_url: "http://remote:8082") # Health client.health → HealthStatus @@ -798,8 +828,8 @@ client.file_cost(path, is_public: true, include_archive: false) → String ```php use Autonomi\AntdClient; -$client = AntdClient::create(); // localhost:8080 -$client = AntdClient::create("http://remote:8080"); +$client = AntdClient::create(); // localhost:8082 +$client = AntdClient::create("http://remote:8082"); // Health $client->health(): HealthStatus @@ -838,8 +868,8 @@ $client->fileCost(string $path, bool $isPublic = true, bool $includeArchive = fa ```dart import 'package:antd/antd.dart'; -final client = AntdClient(); // localhost:8080 -final client = AntdClient(baseUrl: 'http://remote:8080'); +final client = AntdClient(); // localhost:8082 +final client = AntdClient(baseUrl: 'http://remote:8082'); // Health Future health() @@ -878,8 +908,8 @@ Future fileCost(String path, {bool isPublic = true, bool includeArchive ```lua local antd = require("antd") -local client = antd.Client.new() -- localhost:8080 -local client = antd.Client.new({base_url = "http://remote:8080"}) +local client = antd.Client.new() -- localhost:8082 +local client = antd.Client.new({base_url = "http://remote:8082"}) -- Health client:health() → HealthStatus @@ -916,8 +946,8 @@ client:file_cost(path, is_public, include_archive) → string ## Elixir SDK Method Signatures ```elixir -client = Antd.Client.new() # localhost:8080 -client = Antd.Client.new(base_url: "http://remote:8080") +client = Antd.Client.new() # localhost:8082 +client = Antd.Client.new(base_url: "http://remote:8082") # Health Antd.Client.health(client) :: {:ok, HealthStatus.t()} | {:error, term()} @@ -956,8 +986,8 @@ Antd.Client.file_cost(client, path, is_public \\ true, include_archive \\ false) ```zig const antd = @import("antd"); -var client = try antd.Client.init(.{}); // localhost:8080 -var client = try antd.Client.init(.{ .base_url = "http://remote:8080" }); +var client = try antd.Client.init(.{}); // localhost:8082 +var client = try antd.Client.init(.{ .base_url = "http://remote:8082" }); defer client.deinit(); // Health @@ -1128,7 +1158,7 @@ use antd_client::Client; #[tokio::main] async fn main() -> Result<(), antd_client::AntdError> { - let client = Client::new("http://localhost:8080"); + let client = Client::new("http://localhost:8082"); let result = client.data_put_public(b"Hello, Autonomi!").await?; println!("Stored at: {}, cost: {}", result.address, result.cost); diff --git a/llms.txt b/llms.txt index 4f34e35..76e1e3f 100644 --- a/llms.txt +++ b/llms.txt @@ -6,7 +6,7 @@ ```python from antd import AntdClient -client = AntdClient() # REST on localhost:8080 +client = AntdClient() # REST on localhost:8082 result = client.data_put_public(b"hello world") data = client.data_get_public(result.address) ``` @@ -102,7 +102,23 @@ data = client.data_get_public(result.address) - [C++ examples](antd-cpp/examples/) — runnable example files - [Java examples](antd-java/examples/) — runnable example files -## Default Ports +## Port Discovery -- REST: `http://localhost:8080` +All SDKs support automatic daemon discovery via a `daemon.port` file written by antd on startup. Every SDK provides an auto-discover constructor: + +```python +client, url = RestClient.auto_discover() # Python +``` +```go +client, url := antd.NewClientAutoDiscover() // Go +``` +```typescript +const { client, url } = RestClient.autoDiscover(); // TypeScript +``` + +Port file locations: `%APPDATA%\ant\daemon.port` (Windows), `~/.local/share/ant/daemon.port` (Linux), `~/Library/Application Support/ant/daemon.port` (macOS). File format: two lines — REST port, gRPC port. + +## Default Ports (fallback when no port file) + +- REST: `http://localhost:8082` - gRPC: `localhost:50051` diff --git a/skill.md b/skill.md index 1759813..54eb41f 100644 --- a/skill.md +++ b/skill.md @@ -6,12 +6,14 @@ You are helping a developer build an application on the **Autonomi** decentraliz Autonomi is a permanent, decentralized data network. Data is content-addressed (immutable). Storage is pay-once, reads are free. -**How it works:** A local Rust daemon (`antd`) connects to the network and exposes REST + gRPC APIs. Your app talks to antd through a language SDK. The developer never touches the network directly. +**How it works:** A local Rust daemon (`antd`) connects to the network and exposes REST + gRPC APIs. Your app talks to antd through a language SDK. The developer never touches the network directly. All SDKs support automatic daemon discovery via a port file written by antd on startup. ``` App → SDK → antd daemon (localhost) → Autonomi Network ``` +**Port discovery:** antd writes `daemon.port` to the platform data dir (`%APPDATA%\ant\` on Windows, `~/.local/share/ant/` on Linux, `~/Library/Application Support/ant/` on macOS). All SDKs provide auto-discover constructors that read this file. When generating client code, prefer the auto-discover constructor (e.g. `NewClientAutoDiscover()` in Go, `RestClient.auto_discover()` in Python) over hardcoded URLs. Default fallback: REST on `localhost:8082`, gRPC on `localhost:50051`. + For detailed API signatures and endpoint documentation, see: - **[llms.txt](llms.txt)** — concise overview of all REST endpoints, gRPC services, error codes, and SDK links - **[llms-full.txt](llms-full.txt)** — complete reference with method signatures for all 12 languages, request/response formats, and runnable examples @@ -128,7 +130,7 @@ When a developer asks to build something, follow this sequence: 1. **Pick the language** — ask if not obvious from context 2. **Start the daemon** — remind them: `ant dev start` (or `pip install -e ant-dev/ && ant dev start`) -3. **Create the client** — show the 2-line connection code for their language +3. **Create the client** — use the auto-discover constructor for their language (falls back to defaults if antd port file isn't present) 4. **Check health** — `client.health()` to verify the daemon is running 5. **Match their use case to a primitive** — use the tables above 6. **Estimate cost** — call the `*_cost` method before any write From d1b2fd8c987319224ead2f6d6acf9c0bec0ad05e Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Wed, 25 Mar 2026 11:26:03 +0000 Subject: [PATCH 02/20] Fix cross-platform issues in port discovery - Windows: remove target before rename (fs::rename fails if target exists on Windows, unlike Unix atomic replace) - Stale port file detection: antd now writes its PID as line 3 of the port file. Go SDK checks if the process is still alive before trusting the discovered ports (platform-specific: signal 0 on Unix, FindProcess on Windows) - Split Go processAlive into discover_unix.go / discover_windows.go with build tags for clean cross-compilation Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-go/discover.go | 15 ++++++++++++++- antd-go/discover_unix.go | 19 +++++++++++++++++++ antd-go/discover_windows.go | 18 ++++++++++++++++++ antd/src/port_file.rs | 10 +++++++++- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 antd-go/discover_unix.go create mode 100644 antd-go/discover_windows.go diff --git a/antd-go/discover.go b/antd-go/discover.go index b11b3fb..b752140 100644 --- a/antd-go/discover.go +++ b/antd-go/discover.go @@ -35,8 +35,10 @@ func DiscoverGrpcTarget() string { } // readPortFile reads the daemon.port file and returns the REST and gRPC ports. -// The file format is two lines: REST port on line 1, gRPC port on line 2. +// The file format is: REST port (line 1), gRPC port (line 2), PID (line 3). // A single-line file is valid (gRPC port will be 0). +// If a PID is present and the process is not running, the file is considered +// stale and both ports are returned as 0. func readPortFile() (restPort, grpcPort uint16) { dir := dataDir() if dir == "" { @@ -53,6 +55,15 @@ func readPortFile() (restPort, grpcPort uint16) { return 0, 0 } + // Check PID on line 3 — if present and process is dead, file is stale + if len(lines) >= 3 { + if pid, err := strconv.Atoi(strings.TrimSpace(lines[2])); err == nil && pid > 0 { + if !processAlive(pid) { + return 0, 0 + } + } + } + restPort = parsePort(lines[0]) if len(lines) >= 2 { grpcPort = parsePort(lines[1]) @@ -60,6 +71,8 @@ func readPortFile() (restPort, grpcPort uint16) { return restPort, grpcPort } +// processAlive is implemented per-platform in discover_unix.go and discover_windows.go. + func parsePort(s string) uint16 { n, err := strconv.ParseUint(strings.TrimSpace(s), 10, 16) if err != nil { diff --git a/antd-go/discover_unix.go b/antd-go/discover_unix.go new file mode 100644 index 0000000..a0a8a83 --- /dev/null +++ b/antd-go/discover_unix.go @@ -0,0 +1,19 @@ +//go:build !windows + +package antd + +import ( + "os" + "syscall" +) + +// processAlive checks whether a process with the given PID exists +// by sending signal 0 (a no-op that checks process existence). +func processAlive(pid int) bool { + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + err = proc.Signal(syscall.Signal(0)) + return err == nil +} diff --git a/antd-go/discover_windows.go b/antd-go/discover_windows.go new file mode 100644 index 0000000..56643c7 --- /dev/null +++ b/antd-go/discover_windows.go @@ -0,0 +1,18 @@ +//go:build windows + +package antd + +import ( + "os" +) + +// processAlive checks whether a process with the given PID exists. +// On Windows, os.FindProcess opens a handle and fails if the process doesn't exist. +func processAlive(pid int) bool { + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + proc.Release() + return true +} diff --git a/antd/src/port_file.rs b/antd/src/port_file.rs index fadd6cb..b9aea07 100644 --- a/antd/src/port_file.rs +++ b/antd/src/port_file.rs @@ -23,6 +23,8 @@ fn port_file_path() -> Option { /// /// Format: two lines — REST port on line 1, gRPC port on line 2. /// Writes to a temp file first then renames for atomicity. +/// On Windows, removes the target first since rename fails if it exists. +/// Also removes any stale port file from a previous crashed instance. pub fn write(rest_port: u16, grpc_port: u16) -> Option { let dir = data_dir()?; if let Err(e) = fs::create_dir_all(&dir) { @@ -33,12 +35,18 @@ pub fn write(rest_port: u16, grpc_port: u16) -> Option { let target = dir.join(PORT_FILE_NAME); let tmp = dir.join(format!("{PORT_FILE_NAME}.tmp")); - let contents = format!("{rest_port}\n{grpc_port}\n"); + let pid = std::process::id(); + let contents = format!("{rest_port}\n{grpc_port}\n{pid}\n"); let result = (|| -> std::io::Result<()> { let mut f = fs::File::create(&tmp)?; f.write_all(contents.as_bytes())?; f.sync_all()?; + // On Windows, rename fails if target exists — remove it first. + // This is not atomic on Windows but is the best we can do. + if cfg!(windows) { + let _ = fs::remove_file(&target); + } fs::rename(&tmp, &target)?; Ok(()) })(); From 75fea7b00b1f8a9deb8830fdbbd12e53bf00693c Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Wed, 25 Mar 2026 11:30:25 +0000 Subject: [PATCH 03/20] Add stale port file detection via PID checking to all SDKs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit antd now writes its PID as line 3 of the port file. All SDKs validate the PID before trusting discovered ports, preventing connections to dead daemons after crashes. Platform-specific approaches per language: - Go: signal 0 (Unix), FindProcess (Windows) via build tags - Python/MCP: os.kill(pid, 0) — cross-platform on Python 3 - Rust: kill -0 (Unix), tasklist (Windows) via cfg - Java/Kotlin: ProcessHandle.of(pid).isPresent() — cross-platform - C#: Process.GetProcessById — cross-platform on .NET 8+ - TypeScript: process.kill(pid, 0) — cross-platform in Node.js - C++: kill(pid, 0) (Unix), OpenProcess (Windows) via ifdef - Ruby: Process.kill(0, pid) — cross-platform - Elixir: System.cmd("kill", ["-0", pid]) on Unix, trust on Windows - Swift: kill(pid_t, 0) via C interop - Dart: kill -0 on Unix, trust on Windows - Lua: os.execute("kill -0") on Unix, trust on Windows - PHP: posix_kill or /proc check on Unix, tasklist on Windows - Zig: /proc check on Linux, trust on other platforms All backward-compatible with 2-line port files (no PID). Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/include/antd/discover.hpp | 6 +- antd-cpp/src/discover.cpp | 42 +++++- antd-csharp/Antd.Sdk/DaemonDiscovery.cs | 29 +++- antd-dart/lib/src/discover.dart | 37 ++++- antd-elixir/lib/antd/discover.ex | 35 ++++- .../com/autonomi/antd/DaemonDiscovery.java | 29 +++- antd-js/src/discover.ts | 31 ++++- .../com/autonomi/sdk/DaemonDiscovery.kt | 20 ++- antd-lua/src/antd/discover.lua | 27 ++++ antd-mcp/src/antd_mcp/discover.py | 38 +++++- antd-php/src/DaemonDiscovery.php | 32 ++++- antd-py/src/antd/_discover.py | 34 +++++ antd-py/tests/test_discover.py | 44 ++++++ antd-ruby/lib/antd/discover.rb | 34 ++++- antd-rust/src/discover.rs | 128 +++++++++++++++--- .../Sources/AntdSdk/DaemonDiscovery.swift | 24 +++- antd-zig/src/discover.zig | 29 ++++ 17 files changed, 581 insertions(+), 38 deletions(-) diff --git a/antd-cpp/include/antd/discover.hpp b/antd-cpp/include/antd/discover.hpp index 6ac1695..2b8da74 100644 --- a/antd-cpp/include/antd/discover.hpp +++ b/antd-cpp/include/antd/discover.hpp @@ -6,12 +6,14 @@ namespace antd { /// Read the daemon.port file written by antd on startup and return the REST /// base URL (e.g. "http://127.0.0.1:8082"). -/// Returns an empty string if the port file is not found or unreadable. +/// Returns an empty string if the port file is missing, unreadable, or stale +/// (i.e. the recorded PID is no longer alive). std::string discover_daemon_url(); /// Read the daemon.port file written by antd on startup and return the gRPC /// target (e.g. "127.0.0.1:50051"). -/// Returns an empty string if the port file has no gRPC line or is unreadable. +/// Returns an empty string if the port file has no gRPC line, is unreadable, +/// or stale (i.e. the recorded PID is no longer alive). std::string discover_grpc_target(); } // namespace antd diff --git a/antd-cpp/src/discover.cpp b/antd-cpp/src/discover.cpp index 174a84a..ddf7d1b 100644 --- a/antd-cpp/src/discover.cpp +++ b/antd-cpp/src/discover.cpp @@ -5,6 +5,14 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + namespace fs = std::filesystem; namespace antd { @@ -13,6 +21,21 @@ namespace { constexpr const char* kPortFileName = "daemon.port"; constexpr const char* kDataDirName = "ant"; +/// Check whether a process with the given PID is alive. +bool process_alive(unsigned long pid) { +#ifdef _WIN32 + HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, + static_cast(pid)); + if (h == NULL) return false; + CloseHandle(h); + return true; +#else + // kill(pid, 0) checks existence without sending a signal. + // EPERM means the process exists but we lack permission to signal it. + return kill(static_cast(pid), 0) == 0 || errno == EPERM; +#endif +} + /// Parse a port number (1-65535) from a string. Returns 0 on failure. uint16_t parse_port(const std::string& s) { try { @@ -49,7 +72,9 @@ fs::path data_dir() { } /// Read the daemon.port file and return the REST and gRPC ports. -/// The file format is two lines: REST port on line 1, gRPC port on line 2. +/// The file format is: REST port (line 1), gRPC port (line 2), PID (line 3). +/// If a PID is present and the process is not alive, the file is stale and +/// {0, 0} is returned. std::pair read_port_file() { auto dir = data_dir(); if (dir.empty()) return {0, 0}; @@ -58,9 +83,20 @@ std::pair read_port_file() { std::ifstream ifs(path); if (!ifs.is_open()) return {0, 0}; - std::string line1, line2; + std::string line1, line2, line3; if (!std::getline(ifs, line1)) return {0, 0}; - std::getline(ifs, line2); // optional second line + std::getline(ifs, line2); // optional second line (gRPC port) + std::getline(ifs, line3); // optional third line (PID) + + // Stale-file detection: if a PID is recorded, verify the process is alive. + if (!line3.empty()) { + try { + unsigned long pid = std::stoul(line3); + if (pid > 0 && !process_alive(pid)) return {0, 0}; + } catch (...) { + // Malformed PID line — ignore and proceed without the check. + } + } return {parse_port(line1), parse_port(line2)}; } diff --git a/antd-csharp/Antd.Sdk/DaemonDiscovery.cs b/antd-csharp/Antd.Sdk/DaemonDiscovery.cs index 86ce519..73e54cb 100644 --- a/antd-csharp/Antd.Sdk/DaemonDiscovery.cs +++ b/antd-csharp/Antd.Sdk/DaemonDiscovery.cs @@ -4,8 +4,10 @@ namespace Antd.Sdk; /// /// Reads the daemon.port file written by antd on startup to auto-discover -/// the REST and gRPC ports. The file contains two lines: REST port on line 1, -/// gRPC port on line 2. +/// the REST and gRPC ports. The file contains up to three lines: REST port +/// on line 1, gRPC port on line 2, and daemon PID on line 3. If a PID is +/// present and the process is no longer alive, the file is considered stale +/// and discovery returns empty. /// public static class DaemonDiscovery { @@ -53,6 +55,16 @@ private static (ushort restPort, ushort grpcPort) ReadPortFile() if (lines.Length >= 2) grpc = ParsePort(lines[1]); + // Line 3 is the daemon PID. If present and the process is + // no longer running, the port file is stale. + if (lines.Length >= 3 + && int.TryParse(lines[2].Trim(), out var pid) + && pid > 0 + && !ProcessAlive(pid)) + { + return (0, 0); + } + return (rest, grpc); } catch @@ -61,6 +73,19 @@ private static (ushort restPort, ushort grpcPort) ReadPortFile() } } + private static bool ProcessAlive(int pid) + { + try + { + System.Diagnostics.Process.GetProcessById(pid); + return true; + } + catch (ArgumentException) + { + return false; + } + } + private static ushort ParsePort(string s) { return ushort.TryParse(s.Trim(), out var port) ? port : (ushort)0; diff --git a/antd-dart/lib/src/discover.dart b/antd-dart/lib/src/discover.dart index 51eb547..440d7e5 100644 --- a/antd-dart/lib/src/discover.dart +++ b/antd-dart/lib/src/discover.dart @@ -26,8 +26,13 @@ String discoverGrpcTarget() { } /// Reads the daemon.port file and returns (restPort, grpcPort). -/// The file format is two lines: REST port on line 1, gRPC port on line 2. -/// A single-line file is valid (gRPC port will be 0). +/// The file format is up to three lines: +/// line 1: REST port +/// line 2: gRPC port +/// line 3: PID of the antd process +/// A single-line file is valid (gRPC port will be 0, no PID check). +/// If a PID is present and the process is not alive, the port file is +/// considered stale and (0, 0) is returned. (int, int) _readPortFile() { final dir = _dataDir(); if (dir.isEmpty) { @@ -47,11 +52,39 @@ String discoverGrpcTarget() { return (0, 0); } + // If a PID is recorded on line 3, verify the process is still alive. + if (lines.length >= 3) { + final pid = int.tryParse(lines[2].trim()); + if (pid != null && pid > 0 && !_isProcessAlive(pid)) { + return (0, 0); + } + } + final rest = _parsePort(lines[0]); final grpc = lines.length >= 2 ? _parsePort(lines[1]) : 0; return (rest, grpc); } +/// Returns true if a process with the given [pid] is currently running. +/// +/// On non-Windows platforms, uses `kill -0 ` which sends no signal but +/// returns exit code 0 if the process exists. +/// On Windows, Dart lacks a clean way to probe a PID without side effects, +/// so we optimistically return true (trust the port file). +bool _isProcessAlive(int pid) { + if (Platform.isWindows) { + // No reliable non-destructive PID probe in Dart on Windows. + return true; + } + try { + final result = Process.runSync('kill', ['-0', pid.toString()]); + return result.exitCode == 0; + } catch (_) { + // If we can't run the check, assume alive to avoid false negatives. + return true; + } +} + int _parsePort(String s) { final n = int.tryParse(s.trim()); if (n == null || n < 1 || n > 65535) { diff --git a/antd-elixir/lib/antd/discover.ex b/antd-elixir/lib/antd/discover.ex index e34dea4..ba52b7c 100644 --- a/antd-elixir/lib/antd/discover.ex +++ b/antd-elixir/lib/antd/discover.ex @@ -3,7 +3,11 @@ defmodule Antd.Discover do Auto-discovers the antd daemon by reading the `daemon.port` file that antd writes on startup. - The file contains two lines: REST port on line 1, gRPC port on line 2. + The file contains up to three lines: REST port (line 1), gRPC port (line 2), + and optionally the daemon PID (line 3). + + If a PID is present and the process is no longer alive, the port file is + considered stale and discovery returns empty. Port file location is platform-specific: - Windows: `%APPDATA%\\ant\\daemon.port` @@ -63,7 +67,13 @@ defmodule Antd.Discover do rest_port = parse_port(Enum.at(lines, 0, "")) grpc_port = parse_port(Enum.at(lines, 1, "")) - {rest_port, grpc_port} + pid = parse_pid(Enum.at(lines, 2, "")) + + if pid > 0 and not process_alive?(pid) do + {0, 0} + else + {rest_port, grpc_port} + end {:error, _} -> {0, 0} @@ -78,6 +88,27 @@ defmodule Antd.Discover do end end + defp parse_pid(s) do + case Integer.parse(String.trim(s)) do + {n, ""} when n > 0 -> n + _ -> 0 + end + end + + defp process_alive?(pid) do + case :os.type() do + {:unix, _} -> + case System.cmd("kill", ["-0", to_string(pid)], stderr_to_stdout: true) do + {_, 0} -> true + _ -> false + end + + {:win32, _} -> + # On Windows, trust the port file — no reliable zero-signal check. + true + end + end + defp data_dir do case :os.type() do {:win32, _} -> diff --git a/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java b/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java index e9de895..29dcd00 100644 --- a/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java +++ b/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java @@ -10,7 +10,9 @@ * Discovers the antd daemon by reading the {@code daemon.port} file * that the daemon writes on startup. * - *

The file contains two lines: the REST port on line 1 and the gRPC port on line 2. + *

The file contains up to three lines: the REST port on line 1, the gRPC port + * on line 2, and an optional PID on line 3. If a PID is present and the process + * is no longer alive, the port file is considered stale and discovery returns empty. * *

Port file locations by platform: *

    @@ -56,6 +58,8 @@ public static String discoverGrpcTarget() { /** * Reads the specified line from the port file and parses it as a port number. + * If line 3 contains a PID and that process is no longer alive, the port file + * is considered stale and 0 is returned. * * @param lineIndex 0 for REST port, 1 for gRPC port * @return the port number, or 0 on failure @@ -72,12 +76,35 @@ private static int readPort(int lineIndex) { if (lines.size() <= lineIndex) { return 0; } + + // Check for stale port file via PID on line 3 + if (lines.size() >= 3) { + String pidStr = lines.get(2).trim(); + if (!pidStr.isEmpty()) { + try { + long pid = Long.parseLong(pidStr); + if (!processAlive(pid)) { + return 0; + } + } catch (NumberFormatException e) { + // Malformed PID line — ignore and continue + } + } + } + return parsePort(lines.get(lineIndex)); } catch (IOException e) { return 0; } } + /** + * Returns true if a process with the given PID is currently alive. + */ + private static boolean processAlive(long pid) { + return ProcessHandle.of(pid).isPresent(); + } + /** * Parses a port string into an integer in the valid port range (1-65535). * diff --git a/antd-js/src/discover.ts b/antd-js/src/discover.ts index 18879f0..4e7681d 100644 --- a/antd-js/src/discover.ts +++ b/antd-js/src/discover.ts @@ -20,8 +20,13 @@ export function discoverDaemonUrl(): string { /** * Reads the daemon.port file and returns the parsed REST and gRPC ports. - * The file format is two lines: REST port on line 1, gRPC port on line 2. - * A single-line file is valid (gRPC port will be 0). + * The file format is up to three lines: + * line 1: REST port + * line 2: gRPC port + * line 3: PID of the antd process + * A single-line file is valid (gRPC port will be 0, no PID check). + * If a PID is present and the process is not alive, the port file is + * considered stale and { rest: 0, grpc: 0 } is returned. */ function readPortFile(): { rest: number; grpc: number } { const dir = dataDir(); @@ -41,11 +46,33 @@ function readPortFile(): { rest: number; grpc: number } { return { rest: 0, grpc: 0 }; } + // If a PID is recorded on line 3, verify the process is still alive. + if (lines.length >= 3) { + const pid = parseInt(lines[2].trim(), 10); + if (!isNaN(pid) && pid > 0 && !isProcessAlive(pid)) { + return { rest: 0, grpc: 0 }; + } + } + const rest = parsePort(lines[0]); const grpc = lines.length >= 2 ? parsePort(lines[1]) : 0; return { rest, grpc }; } +/** + * Returns true if a process with the given PID is currently running. + * Uses process.kill(pid, 0) which sends no signal but throws if the + * process does not exist. Works on both Unix and Windows in Node.js. + */ +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + function parsePort(s: string): number { const n = parseInt(s.trim(), 10); if (isNaN(n) || n < 1 || n > 65535) { diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt index 1352fb8..70670a7 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt @@ -8,7 +8,9 @@ import java.nio.file.Paths * Reads the `daemon.port` file written by antd on startup to auto-discover * the REST and gRPC ports. * - * The file contains two lines: REST port on line 1, gRPC port on line 2. + * The file contains up to three lines: REST port on line 1, gRPC port on line 2, + * and an optional PID on line 3. If a PID is present and the process is no longer + * alive, the port file is considered stale and discovery returns empty. */ object DaemonDiscovery { @@ -40,6 +42,16 @@ object DaemonDiscovery { return try { val lines = file.readLines().map { it.trim() } + + // Check for stale port file via PID on line 3 + val pidStr = lines.getOrNull(2) + if (!pidStr.isNullOrEmpty()) { + val pid = pidStr.toLongOrNull() + if (pid != null && !processAlive(pid)) { + return 0 to 0 + } + } + val rest = lines.getOrNull(0)?.toIntOrNull()?.takeIf { it in 1..65535 } ?: 0 val grpc = lines.getOrNull(1)?.toIntOrNull()?.takeIf { it in 1..65535 } ?: 0 rest to grpc @@ -48,6 +60,12 @@ object DaemonDiscovery { } } + /** + * Returns true if a process with the given PID is currently alive. + */ + private fun processAlive(pid: Long): Boolean = + ProcessHandle.of(pid).isPresent + private fun dataDir(): Path? { val os = System.getProperty("os.name", "").lowercase() return when { diff --git a/antd-lua/src/antd/discover.lua b/antd-lua/src/antd/discover.lua index 19da82f..85d88cc 100644 --- a/antd-lua/src/antd/discover.lua +++ b/antd-lua/src/antd/discover.lua @@ -12,6 +12,25 @@ local function is_windows() return package.config:sub(1, 1) == "\\" end +--- Check if a process with the given PID is alive. +-- On Windows, always returns true (trust the port file). +-- On Unix, uses `kill -0 ` which succeeds if the process exists. +-- @param pid number +-- @return boolean +local function is_process_alive(pid) + if is_windows() then + return true + end + -- Validate pid is numeric to prevent command injection + local pid_str = tostring(math.floor(pid)) + if not pid_str:match("^%d+$") then + return true + end + local ok = os.execute("kill -0 " .. pid_str .. " >/dev/null 2>&1") + -- Lua 5.1 returns a number (0 = success), Lua 5.2+ returns true/nil + return ok == true or ok == 0 +end + --- Returns the platform-specific data directory for ant. -- @return string|nil directory path, or nil if not determinable local function data_dir() @@ -69,6 +88,14 @@ local function read_port_file() if #lines < 1 then return nil, nil end + -- Line 3: PID of the daemon process (optional stale-detection) + if #lines >= 3 then + local pid = tonumber(lines[3]) + if pid and pid > 0 and not is_process_alive(pid) then + return nil, nil + end + end + local rest_port = tonumber(lines[1]) local grpc_port = #lines >= 2 and tonumber(lines[2]) or nil diff --git a/antd-mcp/src/antd_mcp/discover.py b/antd-mcp/src/antd_mcp/discover.py index 613c1df..82e37d2 100644 --- a/antd-mcp/src/antd_mcp/discover.py +++ b/antd-mcp/src/antd_mcp/discover.py @@ -1,8 +1,14 @@ """Port discovery for the antd daemon. -The antd daemon writes a ``daemon.port`` file on startup containing two lines: +The antd daemon writes a ``daemon.port`` file on startup containing up to three +lines: - Line 1: REST port - Line 2: gRPC port + - Line 3: PID of the daemon process (optional) + +When line 3 is present, this module validates that the process is still alive. +If the PID refers to a dead process the port file is considered stale and +discovery returns empty results. This module reads that file using platform-specific data directory paths to auto-discover the daemon without requiring manual configuration. @@ -42,10 +48,29 @@ def _data_dir() -> Path | None: return Path(home) / ".local" / "share" / _DATA_DIR_NAME +def _is_pid_alive(pid: int) -> bool: + """Check whether a process with the given PID is still running. + + Uses ``os.kill(pid, 0)`` which works cross-platform on Python 3. + """ + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + # Process exists but we lack permission to signal it. + return True + except OSError: + # Unable to determine — assume alive to avoid false negatives. + return True + return True + + def _read_port_file() -> tuple[int, int]: """Read the daemon.port file and return (rest_port, grpc_port). - Returns (0, 0) if the file is missing or unreadable. + Returns (0, 0) if the file is missing, unreadable, or stale (the + recorded PID no longer refers to a running process). A single-line file is valid; grpc_port will be 0 in that case. """ data_dir = _data_dir() @@ -62,6 +87,15 @@ def _read_port_file() -> tuple[int, int]: if not lines: return 0, 0 + # Line 3 (optional): PID of the daemon process. + if len(lines) >= 3: + try: + pid = int(lines[2].strip()) + except ValueError: + pid = None + if pid is not None and not _is_pid_alive(pid): + return 0, 0 + rest_port = _parse_port(lines[0]) grpc_port = _parse_port(lines[1]) if len(lines) >= 2 else 0 return rest_port, grpc_port diff --git a/antd-php/src/DaemonDiscovery.php b/antd-php/src/DaemonDiscovery.php index 4f521a0..5f1c6bd 100644 --- a/antd-php/src/DaemonDiscovery.php +++ b/antd-php/src/DaemonDiscovery.php @@ -7,7 +7,9 @@ /** * Discovers the antd daemon by reading the `daemon.port` file written on startup. * - * The port file contains two lines: REST port on line 1, gRPC port on line 2. + * The port file contains up to three lines: REST port (line 1), gRPC port (line 2), + * and PID of the daemon process (line 3). If a PID is present and the process is + * not alive, the file is considered stale and discovery returns empty. * File location is platform-specific: * - Windows: %APPDATA%\ant\daemon.port * - macOS: ~/Library/Application Support/ant/daemon.port @@ -83,11 +85,39 @@ private static function readPortFile(): array return [0, 0]; } + // Line 3: PID of the daemon process (optional stale-detection) + if (count($lines) >= 3) { + $pid = (int) trim($lines[2]); + if ($pid > 0 && !self::isProcessAlive($pid)) { + return [0, 0]; + } + } + $rest = self::parsePort($lines[0]); $grpc = count($lines) >= 2 ? self::parsePort($lines[1]) : 0; return [$rest, $grpc]; } + /** + * Check if a process with the given PID is alive. + * Uses posix_kill if available, falls back to /proc on Linux, + * or tasklist on Windows. + */ + private static function isProcessAlive(int $pid): bool + { + if (PHP_OS_FAMILY === 'Windows') { + $out = shell_exec("tasklist /FI \"PID eq {$pid}\" /NH 2>NUL"); + return $out !== null && stripos($out, (string) $pid) !== false; + } + + // Unix: prefer posix_kill, fall back to /proc + if (function_exists('posix_kill')) { + return posix_kill($pid, 0); + } + + return file_exists("/proc/{$pid}"); + } + private static function parsePort(string $s): int { $n = (int) trim($s); diff --git a/antd-py/src/antd/_discover.py b/antd-py/src/antd/_discover.py index 8119538..150b3be 100644 --- a/antd-py/src/antd/_discover.py +++ b/antd-py/src/antd/_discover.py @@ -3,8 +3,11 @@ The antd daemon writes a ``daemon.port`` file on startup containing: - Line 1: REST port - Line 2: gRPC port + - Line 3: PID of the daemon process (optional, for staleness detection) This module reads that file to auto-discover the daemon's listen addresses. +If a PID is present and the process is no longer running, the port file is +considered stale and discovery returns empty results. """ from __future__ import annotations @@ -53,11 +56,42 @@ def _read_port_file() -> tuple[int, int]: if not lines: return 0, 0 + # Check PID staleness (line 3, if present) + if len(lines) >= 3: + pid = _parse_pid(lines[2]) + if pid is not None and not _is_process_alive(pid): + return 0, 0 + rest_port = _parse_port(lines[0]) grpc_port = _parse_port(lines[1]) if len(lines) >= 2 else 0 return rest_port, grpc_port +def _parse_pid(s: str) -> int | None: + """Parse a PID string, returning ``None`` if absent or invalid.""" + try: + n = int(s.strip()) + except (ValueError, TypeError): + return None + if n > 0: + return n + return None + + +def _is_process_alive(pid: int) -> bool: + """Return ``True`` if a process with *pid* is currently running.""" + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + # Process exists but we lack permission to signal it — still alive. + return True + except OSError: + return False + return True + + def _parse_port(s: str) -> int: """Parse a port string, returning 0 on failure.""" try: diff --git a/antd-py/tests/test_discover.py b/antd-py/tests/test_discover.py index 330c9c4..d457ad8 100644 --- a/antd-py/tests/test_discover.py +++ b/antd-py/tests/test_discover.py @@ -9,6 +9,7 @@ from antd._discover import ( _data_dir, + _is_process_alive, _read_port_file, discover_daemon_url, discover_grpc_target, @@ -85,6 +86,49 @@ def test_whitespace_handling(self, tmp_path, monkeypatch): assert discover_grpc_target() == "127.0.0.1:50051" +class TestStalePidDetection: + """Port file with a PID that doesn't correspond to a running process.""" + + def test_stale_pid_returns_empty_url(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\n50051\n99999999\n", monkeypatch) + assert discover_daemon_url() == "" + + def test_stale_pid_returns_empty_grpc(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\n50051\n99999999\n", monkeypatch) + assert discover_grpc_target() == "" + + def test_stale_pid_read_port_file(self, tmp_path, monkeypatch): + _write_port_file(tmp_path, "8082\n50051\n99999999\n", monkeypatch) + assert _read_port_file() == (0, 0) + + def test_alive_pid_returns_url(self, tmp_path, monkeypatch): + """Use our own PID, which is guaranteed to be alive.""" + pid = os.getpid() + _write_port_file(tmp_path, f"8082\n50051\n{pid}\n", monkeypatch) + assert discover_daemon_url() == "http://127.0.0.1:8082" + + def test_alive_pid_returns_grpc(self, tmp_path, monkeypatch): + pid = os.getpid() + _write_port_file(tmp_path, f"8082\n50051\n{pid}\n", monkeypatch) + assert discover_grpc_target() == "127.0.0.1:50051" + + def test_no_pid_line_still_works(self, tmp_path, monkeypatch): + """Old two-line format without PID should still work.""" + _write_port_file(tmp_path, "8082\n50051\n", monkeypatch) + assert discover_daemon_url() == "http://127.0.0.1:8082" + + def test_invalid_pid_line_treated_as_absent(self, tmp_path, monkeypatch): + """Non-numeric PID line is ignored (not treated as stale).""" + _write_port_file(tmp_path, "8082\n50051\nnotapid\n", monkeypatch) + assert discover_daemon_url() == "http://127.0.0.1:8082" + + def test_is_process_alive_dead(self): + assert _is_process_alive(99999999) is False + + def test_is_process_alive_self(self): + assert _is_process_alive(os.getpid()) is True + + class TestDataDir: def test_windows(self, monkeypatch): monkeypatch.setattr("sys.platform", "win32") diff --git a/antd-ruby/lib/antd/discover.rb b/antd-ruby/lib/antd/discover.rb index c76d768..92f033d 100644 --- a/antd-ruby/lib/antd/discover.rb +++ b/antd-ruby/lib/antd/discover.rb @@ -6,7 +6,11 @@ module Antd # Auto-discovers the antd daemon by reading the +daemon.port+ file that antd # writes on startup. # - # The file contains two lines: REST port on line 1, gRPC port on line 2. + # The file contains up to three lines: REST port (line 1), gRPC port (line 2), + # and optionally the daemon PID (line 3). + # + # If a PID is present and the process is no longer alive, the port file is + # considered stale and discovery returns empty. # # Port file location is platform-specific: # - Windows: %APPDATA%\ant\daemon.port @@ -49,6 +53,10 @@ def self.read_port_file lines = File.read(path).strip.split("\n") rest_port = parse_port(lines[0]) grpc_port = parse_port(lines[1]) + pid = parse_pid(lines[2]) + + return [0, 0] if pid > 0 && !process_alive?(pid) + [rest_port, grpc_port] rescue StandardError [0, 0] @@ -93,6 +101,28 @@ def self.data_dir end end - private_class_method :read_port_file, :parse_port, :data_dir + # @api private + def self.parse_pid(str) + return 0 if str.nil? + + s = str.strip + return 0 unless s.match?(/\A\d+\z/) + + n = s.to_i + n > 0 ? n : 0 + end + + # @api private + def self.process_alive?(pid) + Process.kill(0, pid) + true + rescue Errno::ESRCH + false + rescue Errno::EPERM + # Process exists but we lack permission to signal it — still alive. + true + end + + private_class_method :read_port_file, :parse_port, :parse_pid, :process_alive?, :data_dir end end diff --git a/antd-rust/src/discover.rs b/antd-rust/src/discover.rs index 52d70ef..7600228 100644 --- a/antd-rust/src/discover.rs +++ b/antd-rust/src/discover.rs @@ -1,8 +1,10 @@ //! Port discovery for the antd daemon. //! //! The antd daemon writes a `daemon.port` file on startup containing the REST -//! port on line 1 and the gRPC port on line 2. These helpers read that file -//! to auto-discover the daemon without hard-coding a port. +//! port on line 1, the gRPC port on line 2, and its PID on line 3. These +//! helpers read that file to auto-discover the daemon without hard-coding a +//! port. If a PID is present and the process is no longer alive, the port +//! file is considered stale and discovery returns `None`. use std::env; use std::fs; @@ -12,16 +14,16 @@ const PORT_FILE_NAME: &str = "daemon.port"; const DATA_DIR_NAME: &str = "ant"; /// Reads the daemon port file and returns the REST base URL -/// (e.g. `"http://127.0.0.1:8082"`), or `None` if the file is missing or -/// unreadable. +/// (e.g. `"http://127.0.0.1:8082"`), or `None` if the file is missing, +/// unreadable, or stale (PID no longer alive). pub fn discover_daemon_url() -> Option { let (rest, _) = read_port_file()?; Some(format!("http://127.0.0.1:{rest}")) } /// Reads the daemon port file and returns the gRPC target URL -/// (e.g. `"http://127.0.0.1:50051"`), or `None` if the file is missing or -/// has no gRPC line. +/// (e.g. `"http://127.0.0.1:50051"`), or `None` if the file is missing, +/// has no gRPC line, or is stale (PID no longer alive). pub fn discover_grpc_target() -> Option { let (_, grpc) = read_port_file()?; let grpc = grpc?; @@ -29,19 +31,63 @@ pub fn discover_grpc_target() -> Option { } /// Reads the `daemon.port` file and returns `(rest_port, Option)`. +/// +/// If the file contains a PID on line 3 and that process is not alive, the +/// port file is stale and this returns `None`. fn read_port_file() -> Option<(u16, Option)> { let dir = data_dir()?; let path = dir.join(PORT_FILE_NAME); let contents = fs::read_to_string(path).ok()?; + parse_port_contents_checked(&contents, process_alive) +} + +/// Parses port file contents and validates the PID using the supplied checker. +/// +/// The `pid_checker` callback allows tests to substitute their own liveness +/// logic without spawning real processes. +fn parse_port_contents_checked( + contents: &str, + pid_checker: fn(u32) -> bool, +) -> Option<(u16, Option)> { let mut lines = contents.trim().lines(); let rest: u16 = lines.next()?.trim().parse().ok()?; let grpc: Option = lines.next().and_then(|l| l.trim().parse().ok()); + // Line 3: optional PID — if present and the process is dead, file is stale. + if let Some(pid_line) = lines.next() { + if let Ok(pid) = pid_line.trim().parse::() { + if !pid_checker(pid) { + return None; + } + } + } + Some((rest, grpc)) } +/// Checks whether a process with the given PID is currently alive. +#[cfg(unix)] +fn process_alive(pid: u32) -> bool { + // `kill -0` checks process existence without sending a signal. + std::process::Command::new("kill") + .args(["-0", &pid.to_string()]) + .output() + .map(|o| o.status.success()) + .unwrap_or(true) // if we can't check, trust the file +} + +/// Checks whether a process with the given PID is currently alive. +#[cfg(windows)] +fn process_alive(pid: u32) -> bool { + std::process::Command::new("tasklist") + .args(["/FI", &format!("PID eq {}", pid), "/NH"]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).contains(&pid.to_string())) + .unwrap_or(true) // if we can't check, trust the file +} + /// Returns the platform-specific data directory for ant. /// /// - Windows: `%APPDATA%\ant` @@ -72,41 +118,89 @@ fn data_dir() -> Option { #[cfg(test)] mod tests { - /// Simulate the same parsing logic used in `read_port_file`. - fn parse_port_contents(contents: &str) -> Option<(u16, Option)> { - let mut lines = contents.trim().lines(); - let rest: u16 = lines.next()?.trim().parse().ok()?; - let grpc: Option = lines.next().and_then(|l| l.trim().parse().ok()); - Some((rest, grpc)) + use super::parse_port_contents_checked; + + /// Stub: process is always alive. + fn alive(_pid: u32) -> bool { + true + } + + /// Stub: process is always dead. + fn dead(_pid: u32) -> bool { + false + } + + fn parse(contents: &str) -> Option<(u16, Option)> { + parse_port_contents_checked(contents, alive) } #[test] fn parse_two_line_port_file() { - let result = parse_port_contents("8082\n50051\n"); + let result = parse("8082\n50051\n"); assert_eq!(result, Some((8082, Some(50051)))); } #[test] fn parse_single_line_port_file() { - let result = parse_port_contents("8082\n"); + let result = parse("8082\n"); assert_eq!(result, Some((8082, None))); } #[test] fn parse_empty_returns_none() { - let result = parse_port_contents(""); + let result = parse(""); assert_eq!(result, None); } #[test] fn parse_invalid_port_returns_none() { - let result = parse_port_contents("notanumber\n"); + let result = parse("notanumber\n"); assert_eq!(result, None); } #[test] fn parse_with_whitespace() { - let result = parse_port_contents(" 8082 \n 50051 \n"); + let result = parse(" 8082 \n 50051 \n"); + assert_eq!(result, Some((8082, Some(50051)))); + } + + #[test] + fn parse_three_line_with_pid_alive() { + let result = parse_port_contents_checked("8082\n50051\n12345\n", alive); + assert_eq!(result, Some((8082, Some(50051)))); + } + + #[test] + fn parse_three_line_with_pid_dead_returns_none() { + let result = parse_port_contents_checked("8082\n50051\n12345\n", dead); + assert_eq!(result, None); + } + + #[test] + fn parse_pid_only_rest_port_alive() { + // Two lines: rest port + PID (no gRPC). The PID occupies line 2 but + // it won't parse as a valid port (PIDs are typically > 65535 or the + // daemon uses a known range). However if the PID *does* parse as a + // u16, it would be treated as the gRPC port and line 3 would be + // absent. This test verifies the three-line format specifically. + let result = parse_port_contents_checked("8082\n50051\n99999\n", alive); + assert_eq!(result, Some((8082, Some(50051)))); + } + + #[test] + fn stale_file_no_grpc_port() { + // rest port, no gRPC, PID dead — but PID is on line 3 so we need + // something on line 2. If line 2 is not a valid port, grpc is None + // and line 2's value is consumed. Line 3 is the PID. + let result = parse_port_contents_checked("8082\n\n12345\n", dead); + assert_eq!(result, None); + } + + #[test] + fn no_pid_line_always_succeeds() { + // Legacy two-line format — no PID check performed. + let result = parse_port_contents_checked("8082\n50051\n", dead); + // `dead` is never called because there is no third line. assert_eq!(result, Some((8082, Some(50051)))); } } diff --git a/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift b/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift index 01a0150..1eda050 100644 --- a/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift +++ b/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift @@ -2,7 +2,9 @@ import Foundation /// Discovers the antd daemon by reading the `daemon.port` file written on startup. /// -/// The port file contains two lines: REST port on line 1, gRPC port on line 2. +/// The port file contains up to three lines: REST port (line 1), gRPC port (line 2), +/// and PID of the daemon process (line 3). If a PID is present and the process is +/// not alive, the file is considered stale and discovery returns empty. /// File location is platform-specific: /// - macOS: `~/Library/Application Support/ant/daemon.port` /// - Linux: `$XDG_DATA_HOME/ant/daemon.port` or `~/.local/share/ant/daemon.port` @@ -64,11 +66,31 @@ public enum DaemonDiscovery { .components(separatedBy: "\n") guard !lines.isEmpty else { return nil } + // Line 3: PID of the daemon process (optional) + if lines.count >= 3, let pid = Int32(lines[2].trimmingCharacters(in: .whitespaces)), pid > 0 { + if !isProcessAlive(pid) { + return nil + } + } + let rest = parsePort(lines[0]) let grpc = lines.count >= 2 ? parsePort(lines[1]) : 0 return (rest, grpc) } + /// Check if a process with the given PID is alive. + /// Uses the C `kill` function with signal 0 — this doesn't send a signal but + /// checks whether the process exists. Returns true if alive or if permission + /// is denied (EPERM means it exists but we can't signal it). + private static func isProcessAlive(_ pid: Int32) -> Bool { + #if os(Windows) + // On Windows, trust the port file — kill(pid, 0) is not available. + return true + #else + return kill(pid_t(pid), 0) == 0 || errno == EPERM + #endif + } + private static func parsePort(_ s: String) -> UInt16 { UInt16(s.trimmingCharacters(in: .whitespaces)) ?? 0 } diff --git a/antd-zig/src/discover.zig b/antd-zig/src/discover.zig index 68c7d15..c575489 100644 --- a/antd-zig/src/discover.zig +++ b/antd-zig/src/discover.zig @@ -48,6 +48,7 @@ fn readPortFile(allocator: Allocator) ?Ports { var rest: u16 = 0; var grpc: u16 = 0; + var pid_line: ?[]const u8 = null; var line_iter = std.mem.splitSequence(u8, contents, "\n"); if (line_iter.next()) |first_line| { @@ -56,10 +57,38 @@ fn readPortFile(allocator: Allocator) ?Ports { if (line_iter.next()) |second_line| { grpc = parsePort(second_line); } + if (line_iter.next()) |third_line| { + pid_line = third_line; + } + + // Line 3: PID of the daemon process (optional stale-detection) + if (pid_line) |pl| { + const trimmed = std.mem.trim(u8, pl, &.{ ' ', '\t', '\r' }); + if (trimmed.len > 0) { + const pid = std.fmt.parseInt(i32, trimmed, 10) catch 0; + if (pid > 0 and !isProcessAlive(pid)) { + return null; + } + } + } return .{ .rest = rest, .grpc = grpc }; } +/// Check if a process with the given PID is alive. +/// On Linux, checks if /proc/{pid} exists. +/// On other platforms (Windows, macOS), trusts the port file. +fn isProcessAlive(pid: i32) bool { + if (builtin.os.tag == .linux) { + var path_buf: [32]u8 = undefined; + const path = std.fmt.bufPrint(&path_buf, "/proc/{d}", .{pid}) catch return true; + std.fs.accessAbsolute(path, .{}) catch return false; + return true; + } + // On Windows, macOS, and other platforms, trust the port file + return true; +} + fn parsePort(s: []const u8) u16 { const trimmed = std.mem.trim(u8, s, &.{ ' ', '\t', '\r' }); return std.fmt.parseInt(u16, trimmed, 10) catch 0; From 01466259c262d38e0596b7fbc1e09ea29c9d5ef2 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Wed, 25 Mar 2026 12:16:12 +0000 Subject: [PATCH 04/20] Add CORS hardening and wallet endpoints with SDK bindings CORS: Restrict allowed origin to http://127.0.0.1:{port} instead of permissive Any. Prevents cross-origin CSRF from malicious webpages. Non-browser clients (SDKs, CLI, AI agents) are unaffected as they don't send Origin headers. Matches approach from ant-client. Wallet: Add GET /v1/wallet/address and GET /v1/wallet/balance REST endpoints to antd. Returns 400 if no EVM wallet is configured. Also adds NotImplemented error variant (HTTP 501 / gRPC UNIMPLEMENTED) for stubbed endpoints. SDK bindings added for wallet_address() and wallet_balance() across all 15 language SDKs + MCP server (2 new MCP tools). Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/include/antd/client.hpp | 8 +++ antd-cpp/include/antd/models.hpp | 11 ++++ antd-cpp/src/client.cpp | 19 ++++++ antd-csharp/Antd.Sdk/AntdRestClient.cs | 21 +++++++ antd-csharp/Antd.Sdk/IAntdClient.cs | 4 ++ antd-csharp/Antd.Sdk/Models.cs | 6 ++ antd-dart/lib/src/client.dart | 14 +++++ antd-dart/lib/src/models.dart | 39 ++++++++++++ antd-elixir/lib/antd/client.ex | 36 +++++++++++ antd-elixir/lib/antd/models.ex | 23 +++++++ antd-go/client.go | 23 +++++++ antd-go/models.go | 11 ++++ .../java/com/autonomi/antd/AntdClient.java | 12 ++++ .../autonomi/antd/models/WalletAddress.java | 8 +++ .../autonomi/antd/models/WalletBalance.java | 9 +++ antd-js/src/index.ts | 2 + antd-js/src/models.ts | 11 ++++ antd-js/src/rest-client.ts | 14 +++++ .../kotlin/com/autonomi/sdk/AntdGrpcClient.kt | 10 +++ .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 12 ++++ .../kotlin/com/autonomi/sdk/IAntdClient.kt | 4 ++ .../main/kotlin/com/autonomi/sdk/Models.kt | 17 +++++ antd-lua/src/antd/client.lua | 18 ++++++ antd-lua/src/antd/models.lua | 20 ++++++ antd-mcp/src/antd_mcp/server.py | 62 ++++++++++++++++--- antd-php/src/AntdClient.php | 56 +++++++++++++++++ antd-py/src/antd/__init__.py | 4 ++ antd-py/src/antd/_rest.py | 30 +++++++++ antd-py/src/antd/models.py | 13 ++++ antd-ruby/lib/antd/client.rb | 16 +++++ antd-ruby/lib/antd/models.rb | 6 ++ antd-rust/src/client.rs | 25 ++++++++ antd-rust/src/models.rs | 16 +++++ .../Sources/AntdSdk/AntdClientProtocol.swift | 4 ++ .../Sources/AntdSdk/AntdGrpcClient.swift | 2 + .../Sources/AntdSdk/AntdRestClient.swift | 21 +++++++ antd-swift/Sources/AntdSdk/Models.swift | 20 ++++++ antd-zig/src/antd.zig | 18 ++++++ antd-zig/src/json_helpers.zig | 38 ++++++++++++ antd-zig/src/models.zig | 20 ++++++ antd/src/error.rs | 5 ++ antd/src/main.rs | 2 +- antd/src/rest/mod.rs | 19 +++++- antd/src/rest/wallet.rs | 37 +++++++++++ antd/src/types.rs | 16 +++++ 45 files changed, 771 insertions(+), 11 deletions(-) create mode 100644 antd-java/src/main/java/com/autonomi/antd/models/WalletAddress.java create mode 100644 antd-java/src/main/java/com/autonomi/antd/models/WalletBalance.java create mode 100644 antd/src/rest/wallet.rs diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index a759289..5f71c22 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -113,6 +113,14 @@ class Client { /// Estimate the cost of uploading a file. std::string file_cost(std::string_view path, bool is_public, bool include_archive); + // --- Wallet --- + + /// Get the wallet address configured on the daemon. + WalletAddress wallet_address(); + + /// Get the wallet balance (tokens and gas). + WalletBalance wallet_balance(); + private: struct Impl; std::unique_ptr impl_; diff --git a/antd-cpp/include/antd/models.hpp b/antd-cpp/include/antd/models.hpp index 3a1ea1c..4311237 100644 --- a/antd-cpp/include/antd/models.hpp +++ b/antd-cpp/include/antd/models.hpp @@ -46,4 +46,15 @@ struct Archive { std::vector entries; }; +/// Wallet address response. +struct WalletAddress { + std::string address; // 0x-prefixed hex +}; + +/// Wallet balance response. +struct WalletBalance { + std::string balance; // atto tokens as string + std::string gas_balance; // atto tokens as string +}; + } // namespace antd diff --git a/antd-cpp/src/client.cpp b/antd-cpp/src/client.cpp index 264f81c..a725d6c 100644 --- a/antd-cpp/src/client.cpp +++ b/antd-cpp/src/client.cpp @@ -325,4 +325,23 @@ std::string Client::file_cost(std::string_view path, bool is_public, bool includ return j.value("cost", ""); } +// --------------------------------------------------------------------------- +// Wallet +// --------------------------------------------------------------------------- + +WalletAddress Client::wallet_address() { + auto j = impl_->do_json("GET", "/v1/wallet/address"); + return WalletAddress{ + .address = j.value("address", ""), + }; +} + +WalletBalance Client::wallet_balance() { + auto j = impl_->do_json("GET", "/v1/wallet/balance"); + return WalletBalance{ + .balance = j.value("balance", ""), + .gas_balance = j.value("gas_balance", ""), + }; +} + } // namespace antd diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index 64d78a9..61dc465 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -220,6 +220,20 @@ public async Task FileCostAsync(string path, bool isPublic = true, bool return resp.Cost; } + // ── Wallet ── + + public async Task WalletAddressAsync() + { + var resp = await GetJsonAsync("/v1/wallet/address"); + return new WalletAddress(resp.Address); + } + + public async Task WalletBalanceAsync() + { + var resp = await GetJsonAsync("/v1/wallet/balance"); + return new WalletBalance(resp.Balance, resp.GasBalance); + } + // ── Internal DTOs for JSON deserialization ── private sealed record HealthResponseDto( @@ -259,4 +273,11 @@ private sealed record ArchiveEntryDto( private sealed record ArchiveDto( [property: JsonPropertyName("entries")] List? Entries); + + private sealed record WalletAddressDto( + [property: JsonPropertyName("address")] string Address); + + private sealed record WalletBalanceDto( + [property: JsonPropertyName("balance")] string Balance, + [property: JsonPropertyName("gas_balance")] string GasBalance); } diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index 4ba37e8..ef1ffdf 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -30,4 +30,8 @@ public interface IAntdClient : IDisposable Task ArchiveGetPublicAsync(string address); Task ArchivePutPublicAsync(Archive archive); Task FileCostAsync(string path, bool isPublic = true, bool includeArchive = false); + + // Wallet + Task WalletAddressAsync(); + Task WalletBalanceAsync(); } diff --git a/antd-csharp/Antd.Sdk/Models.cs b/antd-csharp/Antd.Sdk/Models.cs index 8ff377c..6f67c80 100644 --- a/antd-csharp/Antd.Sdk/Models.cs +++ b/antd-csharp/Antd.Sdk/Models.cs @@ -17,3 +17,9 @@ public sealed record ArchiveEntry(string Path, string Address, ulong Created, ul /// An archive manifest containing file entries. public sealed record Archive(List Entries); + +/// Wallet address from the antd daemon. +public sealed record WalletAddress(string Address); + +/// Wallet balance from the antd daemon. +public sealed record WalletBalance(string Balance, string GasBalance); diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index 0798ed5..f74b619 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -300,4 +300,18 @@ class AntdClient { }); return json!['cost'] as String; } + + // --- Wallet --- + + /// Returns the wallet address configured on the daemon. + Future walletAddress() async { + final json = await _doJson('GET', '/v1/wallet/address'); + return WalletAddress.fromJson(json!); + } + + /// Returns the wallet balance (tokens and gas). + Future walletBalance() async { + final json = await _doJson('GET', '/v1/wallet/balance'); + return WalletBalance.fromJson(json!); + } } diff --git a/antd-dart/lib/src/models.dart b/antd-dart/lib/src/models.dart index 3c2b503..7e28774 100644 --- a/antd-dart/lib/src/models.dart +++ b/antd-dart/lib/src/models.dart @@ -177,3 +177,42 @@ class Archive { @override String toString() => 'Archive(entries: $entries)'; } + +/// WalletAddress is the wallet address response. +class WalletAddress { + /// The 0x-prefixed hex address. + final String address; + + const WalletAddress({required this.address}); + + factory WalletAddress.fromJson(Map json) { + return WalletAddress( + address: json['address'] as String? ?? '', + ); + } + + @override + String toString() => 'WalletAddress(address: $address)'; +} + +/// WalletBalance is the wallet balance response. +class WalletBalance { + /// Token balance in atto tokens as a string. + final String balance; + + /// Gas balance in atto tokens as a string. + final String gasBalance; + + const WalletBalance({required this.balance, required this.gasBalance}); + + factory WalletBalance.fromJson(Map json) { + return WalletBalance( + balance: json['balance'] as String? ?? '', + gasBalance: json['gas_balance'] as String? ?? '', + ); + } + + @override + String toString() => + 'WalletBalance(balance: $balance, gasBalance: $gasBalance)'; +} diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index a9e8229..d5eee9a 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -424,6 +424,42 @@ defmodule Antd.Client do unwrap!(file_cost(client, path, is_public, include_archive)) end + # --------------------------------------------------------------------------- + # Wallet + # --------------------------------------------------------------------------- + + @doc "Returns the wallet address configured on the daemon." + @spec wallet_address(t()) :: {:ok, Antd.WalletAddress.t()} | {:error, Exception.t()} + def wallet_address(%__MODULE__{} = client) do + case do_json(client, :get, "/v1/wallet/address", nil) do + {:ok, body} -> + {:ok, %Antd.WalletAddress{address: body["address"]}} + + {:error, _} = err -> + err + end + end + + @doc "Like `wallet_address/1` but raises on error." + @spec wallet_address!(t()) :: Antd.WalletAddress.t() + def wallet_address!(client), do: unwrap!(wallet_address(client)) + + @doc "Returns the wallet balance and gas balance." + @spec wallet_balance(t()) :: {:ok, Antd.WalletBalance.t()} | {:error, Exception.t()} + def wallet_balance(%__MODULE__{} = client) do + case do_json(client, :get, "/v1/wallet/balance", nil) do + {:ok, body} -> + {:ok, %Antd.WalletBalance{balance: body["balance"], gas_balance: body["gas_balance"]}} + + {:error, _} = err -> + err + end + end + + @doc "Like `wallet_balance/1` but raises on error." + @spec wallet_balance!(t()) :: Antd.WalletBalance.t() + def wallet_balance!(client), do: unwrap!(wallet_balance(client)) + # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- diff --git a/antd-elixir/lib/antd/models.ex b/antd-elixir/lib/antd/models.ex index 450c3cf..655493e 100644 --- a/antd-elixir/lib/antd/models.ex +++ b/antd-elixir/lib/antd/models.ex @@ -73,3 +73,26 @@ defmodule Antd.Archive do entries: [Antd.ArchiveEntry.t()] } end + +defmodule Antd.WalletAddress do + @moduledoc "Wallet address result." + + @enforce_keys [:address] + defstruct [:address] + + @type t :: %__MODULE__{ + address: String.t() + } +end + +defmodule Antd.WalletBalance do + @moduledoc "Wallet balance result." + + @enforce_keys [:balance, :gas_balance] + defstruct [:balance, :gas_balance] + + @type t :: %__MODULE__{ + balance: String.t(), + gas_balance: String.t() + } +end diff --git a/antd-go/client.go b/antd-go/client.go index 04e96ba..d425f2a 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -426,3 +426,26 @@ func (c *Client) FileCost(ctx context.Context, path string, isPublic bool, inclu } return str(j, "cost"), nil } + +// --- Wallet --- + +// WalletAddress returns the wallet's public address. +func (c *Client) WalletAddress(ctx context.Context) (*WalletAddress, error) { + j, _, err := c.doJSON(ctx, http.MethodGet, "/v1/wallet/address", nil) + if err != nil { + return nil, err + } + return &WalletAddress{Address: str(j, "address")}, nil +} + +// WalletBalance returns the wallet's token and gas balances. +func (c *Client) WalletBalance(ctx context.Context) (*WalletBalance, error) { + j, _, err := c.doJSON(ctx, http.MethodGet, "/v1/wallet/balance", nil) + if err != nil { + return nil, err + } + return &WalletBalance{ + Balance: str(j, "balance"), + GasBalance: str(j, "gas_balance"), + }, nil +} diff --git a/antd-go/models.go b/antd-go/models.go index 84d128b..d8d9d74 100644 --- a/antd-go/models.go +++ b/antd-go/models.go @@ -39,3 +39,14 @@ type ArchiveEntry struct { type Archive struct { Entries []ArchiveEntry `json:"entries"` } + +// WalletAddress is the result of a wallet address query. +type WalletAddress struct { + Address string `json:"address"` // hex with 0x prefix +} + +// WalletBalance is the result of a wallet balance query. +type WalletBalance struct { + Balance string `json:"balance"` // token balance in atto + GasBalance string `json:"gas_balance"` // gas balance in wei +} diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index f592c35..de41c69 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -332,4 +332,16 @@ public String fileCost(String path, boolean isPublic, boolean includeArchive) { Map j = doJson("POST", "/v1/cost/file", body); return str(j, "cost"); } + + // ── Wallet ── + + public WalletAddress walletAddress() { + Map j = doJson("GET", "/v1/wallet/address", null); + return new WalletAddress(str(j, "address")); + } + + public WalletBalance walletBalance() { + Map j = doJson("GET", "/v1/wallet/balance", null); + return new WalletBalance(str(j, "balance"), str(j, "gas_balance")); + } } diff --git a/antd-java/src/main/java/com/autonomi/antd/models/WalletAddress.java b/antd-java/src/main/java/com/autonomi/antd/models/WalletAddress.java new file mode 100644 index 0000000..bd3a53a --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/models/WalletAddress.java @@ -0,0 +1,8 @@ +package com.autonomi.antd.models; + +/** + * Wallet address from the antd daemon. + * + * @param address hex-encoded address, e.g. "0x..." + */ +public record WalletAddress(String address) {} diff --git a/antd-java/src/main/java/com/autonomi/antd/models/WalletBalance.java b/antd-java/src/main/java/com/autonomi/antd/models/WalletBalance.java new file mode 100644 index 0000000..614d14b --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/models/WalletBalance.java @@ -0,0 +1,9 @@ +package com.autonomi.antd.models; + +/** + * Wallet balance from the antd daemon. + * + * @param balance balance in atto tokens (as a string to preserve precision) + * @param gasBalance gas balance in atto tokens (as a string to preserve precision) + */ +public record WalletBalance(String balance, String gasBalance) {} diff --git a/antd-js/src/index.ts b/antd-js/src/index.ts index 5266fa0..ac2f5e1 100644 --- a/antd-js/src/index.ts +++ b/antd-js/src/index.ts @@ -5,6 +5,8 @@ export type { GraphEntry, ArchiveEntry, Archive, + WalletAddress, + WalletBalance, } from "./models.js"; export { diff --git a/antd-js/src/models.ts b/antd-js/src/models.ts index 5ef6440..0a423e4 100644 --- a/antd-js/src/models.ts +++ b/antd-js/src/models.ts @@ -37,3 +37,14 @@ export interface ArchiveEntry { export interface Archive { entries: ArchiveEntry[]; } + +/** Wallet address response. */ +export interface WalletAddress { + address: string; // 0x-prefixed hex +} + +/** Wallet balance response. */ +export interface WalletBalance { + balance: string; // atto tokens as string + gasBalance: string; // atto tokens as string +} diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index add8b29..9006cf7 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -7,6 +7,8 @@ import type { GraphEntry, HealthStatus, PutResult, + WalletAddress, + WalletBalance, } from "./models.js"; /** Options for creating a REST client. */ @@ -289,4 +291,16 @@ export class RestClient { }); return j.cost; } + + // ---- Wallet ---- + + async walletAddress(): Promise { + const j = await this.getJson<{ address: string }>("/v1/wallet/address"); + return { address: j.address }; + } + + async walletBalance(): Promise { + const j = await this.getJson<{ balance: string; gas_balance: string }>("/v1/wallet/balance"); + return { balance: j.balance, gasBalance: j.gas_balance }; + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt index b34f5f8..9904687 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt @@ -169,4 +169,14 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { }) resp.attoTokens } catch (ex: StatusRuntimeException) { throw wrap(ex) } + + // ── Wallet ── + + override suspend fun walletAddress(): WalletAddress { + throw UnsupportedOperationException("walletAddress is not yet supported via gRPC") + } + + override suspend fun walletBalance(): WalletBalance { + throw UnsupportedOperationException("walletBalance is not yet supported via gRPC") + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index b17139e..afa1039 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -246,4 +246,16 @@ class AntdRestClient( val resp = postJson("/v1/cost/file", body) return resp.cost } + + // ── Wallet ── + + override suspend fun walletAddress(): WalletAddress { + val resp = getJson("/v1/wallet/address") + return WalletAddress(resp.address) + } + + override suspend fun walletBalance(): WalletBalance { + val resp = getJson("/v1/wallet/balance") + return WalletBalance(resp.balance, resp.gasBalance) + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt index 80959aa..8a22f90 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt @@ -38,4 +38,8 @@ interface IAntdClient : Closeable { suspend fun archiveGetPublic(address: String): Archive suspend fun archivePutPublic(archive: Archive): PutResult suspend fun fileCost(path: String, isPublic: Boolean = true, includeArchive: Boolean = false): String + + // Wallet + suspend fun walletAddress(): WalletAddress + suspend fun walletBalance(): WalletBalance } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt index e229a0c..95700a3 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt @@ -21,6 +21,12 @@ data class ArchiveEntry(val path: String, val address: String, val created: ULon /** An archive manifest containing file entries. */ data class Archive(val entries: List) +/** Wallet address response. */ +data class WalletAddress(val address: String) + +/** Wallet balance response. */ +data class WalletBalance(val balance: String, val gasBalance: String) + // ── Internal DTOs for JSON deserialization ── @Serializable @@ -78,3 +84,14 @@ internal data class ArchiveEntryDto( internal data class ArchiveDto( val entries: List? = null, ) + +@Serializable +internal data class WalletAddressDto( + val address: String, +) + +@Serializable +internal data class WalletBalanceDto( + val balance: String, + @SerialName("gas_balance") val gasBalance: String, +) diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index bf6e526..bb0eb6a 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -402,6 +402,24 @@ function Client:file_cost(path, is_public, include_archive) return str(j, "cost"), nil end +-- ── Wallet ── + +--- Get the wallet's public address. +-- @return table|nil {address=string}, error|nil +function Client:wallet_address() + local j, _, err = self:_do_json("GET", "/v1/wallet/address", nil) + if err then return nil, err end + return { address = str(j, "address") }, nil +end + +--- Get the wallet's token and gas balances. +-- @return table|nil {balance=string, gas_balance=string}, error|nil +function Client:wallet_balance() + local j, _, err = self:_do_json("GET", "/v1/wallet/balance", nil) + if err then return nil, err end + return { balance = str(j, "balance"), gas_balance = str(j, "gas_balance") }, nil +end + --- Create a client using daemon port discovery. -- Falls back to the default base URL if discovery fails. -- @param opts table optional settings: { timeout = number } diff --git a/antd-lua/src/antd/models.lua b/antd-lua/src/antd/models.lua index ce1394e..06a21ec 100644 --- a/antd-lua/src/antd/models.lua +++ b/antd-lua/src/antd/models.lua @@ -78,4 +78,24 @@ function M.new_archive(entries) } end +--- Create a WalletAddress table. +-- @param address string wallet address (e.g. "0x...") +-- @return table +function M.new_wallet_address(address) + return { + address = address, + } +end + +--- Create a WalletBalance table. +-- @param balance string token balance in atto tokens +-- @param gas_balance string gas balance in atto tokens +-- @return table +function M.new_wallet_balance(balance, gas_balance) + return { + balance = balance, + gas_balance = gas_balance, + } +end + return M diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index 4168323..9a71705 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -269,7 +269,53 @@ async def check_balance() -> str: # --------------------------------------------------------------------------- -# Tool 7: chunk_put +# Tool 7: wallet_address +# --------------------------------------------------------------------------- + + +@mcp.tool() +async def wallet_address() -> str: + """Get the wallet's public address from the antd daemon. + + Returns: + JSON with the wallet address (e.g. "0x..."), or error details. + Returns an error if no wallet is configured. + """ + client, network = _get_ctx() + try: + result = await client.wallet_address() + return _ok({"address": result.address}, network) + except AntdError as exc: + return _err_antd(exc, network) + except Exception as exc: + return _err(exc, network) + + +# --------------------------------------------------------------------------- +# Tool 8: wallet_balance +# --------------------------------------------------------------------------- + + +@mcp.tool() +async def wallet_balance() -> str: + """Get the wallet's token and gas balances from the antd daemon. + + Returns: + JSON with balance (token balance) and gas_balance (gas token balance), + both as strings in atto units. Returns an error if no wallet is configured. + """ + client, network = _get_ctx() + try: + result = await client.wallet_balance() + return _ok({"balance": result.balance, "gas_balance": result.gas_balance}, network) + except AntdError as exc: + return _err_antd(exc, network) + except Exception as exc: + return _err(exc, network) + + +# --------------------------------------------------------------------------- +# Tool 9: chunk_put # --------------------------------------------------------------------------- @@ -297,7 +343,7 @@ async def chunk_put( # --------------------------------------------------------------------------- -# Tool 8: chunk_get +# Tool 10: chunk_get # --------------------------------------------------------------------------- @@ -324,7 +370,7 @@ async def chunk_get( # --------------------------------------------------------------------------- -# Tool 9: create_graph_entry +# Tool 11: create_graph_entry # --------------------------------------------------------------------------- @@ -364,7 +410,7 @@ async def create_graph_entry( # --------------------------------------------------------------------------- -# Tool 10: get_graph_entry +# Tool 12: get_graph_entry # --------------------------------------------------------------------------- @@ -400,7 +446,7 @@ async def get_graph_entry( # --------------------------------------------------------------------------- -# Tool 11: graph_entry_exists +# Tool 13: graph_entry_exists # --------------------------------------------------------------------------- @@ -427,7 +473,7 @@ async def graph_entry_exists( # --------------------------------------------------------------------------- -# Tool 12: graph_entry_cost +# Tool 14: graph_entry_cost # --------------------------------------------------------------------------- @@ -454,7 +500,7 @@ async def graph_entry_cost( # --------------------------------------------------------------------------- -# Tool 13: archive_get +# Tool 15: archive_get # --------------------------------------------------------------------------- @@ -493,7 +539,7 @@ async def archive_get( # --------------------------------------------------------------------------- -# Tool 14: archive_put +# Tool 16: archive_put # --------------------------------------------------------------------------- diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index 7a73775..e70776d 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -768,6 +768,62 @@ public function archivePutPublicAsync(Archive $archive): PromiseInterface ); } + // --- Wallet --- + + /** + * Get the wallet's public address. + * + * @return array{address: string} + * @throws AntdError if no wallet is configured (HTTP 400) + */ + public function walletAddress(): array + { + $json = $this->doJson('GET', '/v1/wallet/address'); + return ['address' => $json['address'] ?? '']; + } + + /** + * Async: Get the wallet's public address. + * + * @return PromiseInterface + */ + public function walletAddressAsync(): PromiseInterface + { + return $this->doJsonAsync('GET', '/v1/wallet/address')->then( + fn(?array $json) => ['address' => $json['address'] ?? ''], + ); + } + + /** + * Get the wallet's token and gas balances. + * + * @return array{balance: string, gas_balance: string} + * @throws AntdError if no wallet is configured (HTTP 400) + */ + public function walletBalance(): array + { + $json = $this->doJson('GET', '/v1/wallet/balance'); + return [ + 'balance' => $json['balance'] ?? '', + 'gas_balance' => $json['gas_balance'] ?? '', + ]; + } + + /** + * Async: Get the wallet's token and gas balances. + * + * @return PromiseInterface + */ + public function walletBalanceAsync(): PromiseInterface + { + return $this->doJsonAsync('GET', '/v1/wallet/balance')->then( + fn(?array $json) => [ + 'balance' => $json['balance'] ?? '', + 'gas_balance' => $json['gas_balance'] ?? '', + ], + ); + } + /** * Estimate the cost of uploading a file. */ diff --git a/antd-py/src/antd/__init__.py b/antd-py/src/antd/__init__.py index 8f3b9e7..872ef66 100644 --- a/antd-py/src/antd/__init__.py +++ b/antd-py/src/antd/__init__.py @@ -18,6 +18,8 @@ GraphEntry, HealthStatus, PutResult, + WalletAddress, + WalletBalance, ) from ._discover import discover_daemon_url, discover_grpc_target from .exceptions import ( @@ -46,6 +48,8 @@ "GraphDescendant", "GraphEntry", "PutResult", + "WalletAddress", + "WalletBalance", # Exceptions "AntdError", "AlreadyExistsError", diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 3d8e6ff..19f64f7 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -15,6 +15,8 @@ GraphEntry, HealthStatus, PutResult, + WalletAddress, + WalletBalance, ) if TYPE_CHECKING: @@ -221,6 +223,20 @@ def file_cost(self, path: str, is_public: bool = True, include_archive: bool = F _check(resp) return resp.json()["cost"] + # --- Wallet --- + + def wallet_address(self) -> WalletAddress: + resp = self._http.get("/v1/wallet/address") + _check(resp) + j = resp.json() + return WalletAddress(address=j["address"]) + + def wallet_balance(self) -> WalletBalance: + resp = self._http.get("/v1/wallet/balance") + _check(resp) + j = resp.json() + return WalletBalance(balance=j["balance"], gas_balance=j["gas_balance"]) + class AsyncRestClient: """Asynchronous REST client for the antd daemon.""" @@ -402,3 +418,17 @@ async def file_cost(self, path: str, is_public: bool = True, include_archive: bo }) _check(resp) return resp.json()["cost"] + + # --- Wallet --- + + async def wallet_address(self) -> WalletAddress: + resp = await self._http.get("/v1/wallet/address") + _check(resp) + j = resp.json() + return WalletAddress(address=j["address"]) + + async def wallet_balance(self) -> WalletBalance: + resp = await self._http.get("/v1/wallet/balance") + _check(resp) + j = resp.json() + return WalletBalance(balance=j["balance"], gas_balance=j["gas_balance"]) diff --git a/antd-py/src/antd/models.py b/antd-py/src/antd/models.py index 21d6e8e..a3b460c 100644 --- a/antd-py/src/antd/models.py +++ b/antd-py/src/antd/models.py @@ -48,3 +48,16 @@ class ArchiveEntry: class Archive: """A collection of archive entries.""" entries: list[ArchiveEntry] = field(default_factory=list) + + +@dataclass(frozen=True) +class WalletAddress: + """Wallet address from the antd daemon.""" + address: str # hex, e.g. "0x..." + + +@dataclass(frozen=True) +class WalletBalance: + """Wallet balance from the antd daemon.""" + balance: str # atto tokens as string + gas_balance: str # atto gas tokens as string diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index 3a3f6ad..b10141a 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -232,6 +232,22 @@ def file_cost(path, is_public, include_archive) j["cost"] end + # --- Wallet --- + + # Get the wallet address configured on the daemon. + # @return [WalletAddress] + def wallet_address + j = do_json(:get, "/v1/wallet/address") + WalletAddress.new(address: j["address"]) + end + + # Get the wallet balance and gas balance. + # @return [WalletBalance] + def wallet_balance + j = do_json(:get, "/v1/wallet/balance") + WalletBalance.new(balance: j["balance"], gas_balance: j["gas_balance"]) + end + private def b64_encode(data) diff --git a/antd-ruby/lib/antd/models.rb b/antd-ruby/lib/antd/models.rb index 71dde71..c4f5c39 100644 --- a/antd-ruby/lib/antd/models.rb +++ b/antd-ruby/lib/antd/models.rb @@ -18,4 +18,10 @@ module Antd # A collection of archive entries. Archive = Struct.new(:entries, keyword_init: true) + + # Wallet address result. + WalletAddress = Struct.new(:address, keyword_init: true) + + # Wallet balance result. + WalletBalance = Struct.new(:balance, :gas_balance, keyword_init: true) end diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index 133928e..e5b316d 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -506,4 +506,29 @@ impl Client { let j = j.unwrap_or_default(); Ok(Self::str_field(&j, "cost")) } + + // --- Wallet --- + + /// Returns the wallet address configured in the daemon. + pub async fn wallet_address(&self) -> Result { + let (j, _) = self + .do_json(reqwest::Method::GET, "/v1/wallet/address", None) + .await?; + let j = j.unwrap_or_default(); + Ok(WalletAddress { + address: Self::str_field(&j, "address"), + }) + } + + /// Returns the wallet balance from the daemon. + pub async fn wallet_balance(&self) -> Result { + let (j, _) = self + .do_json(reqwest::Method::GET, "/v1/wallet/balance", None) + .await?; + let j = j.unwrap_or_default(); + Ok(WalletBalance { + balance: Self::str_field(&j, "balance"), + gas_balance: Self::str_field(&j, "gas_balance"), + }) + } } diff --git a/antd-rust/src/models.rs b/antd-rust/src/models.rs index e8bf682..bec5577 100644 --- a/antd-rust/src/models.rs +++ b/antd-rust/src/models.rs @@ -49,3 +49,19 @@ pub struct ArchiveEntry { pub struct Archive { pub entries: Vec, } + +/// Wallet address from the antd daemon. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WalletAddress { + /// Hex-encoded address, e.g. "0x...". + pub address: String, +} + +/// Wallet balance from the antd daemon. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WalletBalance { + /// Balance in atto tokens as a string. + pub balance: String, + /// Gas balance in atto tokens as a string. + pub gas_balance: String, +} diff --git a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift index 42be06b..a12d48d 100644 --- a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift +++ b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift @@ -34,4 +34,8 @@ public protocol AntdClientProtocol: Sendable { func archiveGetPublic(address: String) async throws -> Archive func archivePutPublic(archive: Archive) async throws -> PutResult func fileCost(path: String, isPublic: Bool, includeArchive: Bool) async throws -> String + + // Wallet + func walletAddress() async throws -> WalletAddress + func walletBalance() async throws -> WalletBalance } diff --git a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift index 8c36bba..c04f1a4 100644 --- a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift @@ -43,4 +43,6 @@ public final class AntdGrpcClient: AntdClientProtocol, @unchecked Sendable { public func archiveGetPublic(address: String) async throws -> Archive { throw notImplemented() } public func archivePutPublic(archive: Archive) async throws -> PutResult { throw notImplemented() } public func fileCost(path: String, isPublic: Bool = true, includeArchive: Bool = false) async throws -> String { throw notImplemented() } + public func walletAddress() async throws -> WalletAddress { throw notImplemented() } + public func walletBalance() async throws -> WalletBalance { throw notImplemented() } } diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index 6ae6250..bf50e1e 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -185,6 +185,18 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { let resp: CostDTO = try await postJSON("/v1/cost/file", body: body) return resp.cost } + + // MARK: - Wallet + + public func walletAddress() async throws -> WalletAddress { + let resp: WalletAddressDTO = try await getJSON("/v1/wallet/address") + return WalletAddress(address: resp.address) + } + + public func walletBalance() async throws -> WalletBalance { + let resp: WalletBalanceDTO = try await getJSON("/v1/wallet/balance") + return WalletBalance(balance: resp.balance, gasBalance: resp.gasBalance) + } } // MARK: - Internal DTOs @@ -236,6 +248,15 @@ private struct ArchiveDTO: Decodable { let entries: [ArchiveEntryDTO]? } +private struct WalletAddressDTO: Decodable { + let address: String +} + +private struct WalletBalanceDTO: Decodable { + let balance: String + let gasBalance: String +} + extension JSONDecoder { static let snakeCase: JSONDecoder = { let decoder = JSONDecoder() diff --git a/antd-swift/Sources/AntdSdk/Models.swift b/antd-swift/Sources/AntdSdk/Models.swift index 9ab921e..59c763a 100644 --- a/antd-swift/Sources/AntdSdk/Models.swift +++ b/antd-swift/Sources/AntdSdk/Models.swift @@ -73,3 +73,23 @@ public struct Archive: Sendable, Equatable { self.entries = entries } } + +/// Wallet address result. +public struct WalletAddress: Sendable, Equatable { + public let address: String + + public init(address: String) { + self.address = address + } +} + +/// Wallet balance result. +public struct WalletBalance: Sendable, Equatable { + public let balance: String + public let gasBalance: String + + public init(balance: String, gasBalance: String) { + self.balance = balance + self.gasBalance = gasBalance + } +} diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index 2c42927..4151593 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -13,6 +13,8 @@ pub const GraphDescendant = models.GraphDescendant; pub const GraphEntry = models.GraphEntry; pub const ArchiveEntry = models.ArchiveEntry; pub const Archive = models.Archive; +pub const WalletAddress = models.WalletAddress; +pub const WalletBalance = models.WalletBalance; pub const AntdError = errors.AntdError; pub const ErrorInfo = errors.ErrorInfo; pub const errorForStatus = errors.errorForStatus; @@ -295,6 +297,22 @@ pub const Client = struct { return json_helpers.parseCost(self.allocator, resp); } + // --- Wallet --- + + /// Get the wallet's public address. + pub fn walletAddress(self: *Client) !WalletAddress { + const resp = try self.doRequest(.GET, "/v1/wallet/address", null) orelse return error.JsonError; + defer self.allocator.free(resp); + return json_helpers.parseWalletAddress(self.allocator, resp); + } + + /// Get the wallet's token and gas balances. + pub fn walletBalance(self: *Client) !WalletBalance { + const resp = try self.doRequest(.GET, "/v1/wallet/balance", null) orelse return error.JsonError; + defer self.allocator.free(resp); + return json_helpers.parseWalletBalance(self.allocator, resp); + } + // --- Files --- /// Upload a local file to the network. diff --git a/antd-zig/src/json_helpers.zig b/antd-zig/src/json_helpers.zig index 17b4aac..7bfa235 100644 --- a/antd-zig/src/json_helpers.zig +++ b/antd-zig/src/json_helpers.zig @@ -374,6 +374,44 @@ pub fn buildJsonBody(allocator: Allocator, fields: []const struct { key: []const return buf.toOwnedSlice(allocator); } +/// Parse a WalletAddress from a JSON response body. +pub fn parseWalletAddress(allocator: Allocator, body: []const u8) !models.WalletAddress { + const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch + return error.JsonError; + defer parsed.deinit(); + const root = parsed.value; + + const obj = switch (root) { + .object => |o| o, + else => return error.JsonError, + }; + + return .{ + .address = dupeString(allocator, obj.get("address") orelse .null) catch + return error.JsonError, + }; +} + +/// Parse a WalletBalance from a JSON response body. +pub fn parseWalletBalance(allocator: Allocator, body: []const u8) !models.WalletBalance { + const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch + return error.JsonError; + defer parsed.deinit(); + const root = parsed.value; + + const obj = switch (root) { + .object => |o| o, + else => return error.JsonError, + }; + + return .{ + .balance = dupeString(allocator, obj.get("balance") orelse .null) catch + return error.JsonError, + .gas_balance = dupeString(allocator, obj.get("gas_balance") orelse .null) catch + return error.JsonError, + }; +} + /// Extract the "error" message from a JSON error response body. pub fn parseErrorMessage(allocator: Allocator, body: []const u8) ?[]const u8 { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return null; diff --git a/antd-zig/src/models.zig b/antd-zig/src/models.zig index 7d2aecb..dbe57a2 100644 --- a/antd-zig/src/models.zig +++ b/antd-zig/src/models.zig @@ -68,6 +68,26 @@ pub const ArchiveEntry = struct { } }; +/// Result of a wallet address query. +pub const WalletAddress = struct { + address: []const u8, + + pub fn deinit(self: WalletAddress, allocator: Allocator) void { + allocator.free(self.address); + } +}; + +/// Result of a wallet balance query. +pub const WalletBalance = struct { + balance: []const u8, + gas_balance: []const u8, + + pub fn deinit(self: WalletBalance, allocator: Allocator) void { + allocator.free(self.balance); + allocator.free(self.gas_balance); + } +}; + /// A collection of archive entries. pub const Archive = struct { entries: []const ArchiveEntry, diff --git a/antd/src/error.rs b/antd/src/error.rs index f6f6c79..b900f44 100644 --- a/antd/src/error.rs +++ b/antd/src/error.rs @@ -25,6 +25,9 @@ pub enum AntdError { #[error("Timeout: {0}")] Timeout(String), + #[error("Not implemented: {0}")] + NotImplemented(String), + #[error("Internal error: {0}")] Internal(String), } @@ -44,6 +47,7 @@ impl IntoResponse for AntdError { AntdError::Network(_) => StatusCode::BAD_GATEWAY, AntdError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE, AntdError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + AntdError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, AntdError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, }; let body = serde_json::to_string(&ErrorBody { @@ -64,6 +68,7 @@ impl From for tonic::Status { AntdError::Network(msg) => tonic::Status::unavailable(msg), AntdError::TooLarge => tonic::Status::resource_exhausted("too large for memory"), AntdError::Timeout(msg) => tonic::Status::deadline_exceeded(msg), + AntdError::NotImplemented(msg) => tonic::Status::unimplemented(msg), AntdError::Internal(msg) => tonic::Status::internal(msg), } } diff --git a/antd/src/main.rs b/antd/src/main.rs index a523ea9..f695a97 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -167,7 +167,7 @@ async fn main() -> Result<(), Box> { }); // Build REST router - let app = rest::router(state.clone(), config.cors); + let app = rest::router(state.clone(), config.cors, actual_rest_addr.port()); // Spawn both servers let grpc_state = state.clone(); diff --git a/antd/src/rest/mod.rs b/antd/src/rest/mod.rs index d53c5d0..d5db591 100644 --- a/antd/src/rest/mod.rs +++ b/antd/src/rest/mod.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use axum::extract::State; +use axum::http::{HeaderValue, Method}; use axum::routing::{get, head, post}; use axum::{Json, Router}; use tower_http::cors::CorsLayer; @@ -13,8 +14,9 @@ pub mod data; pub mod events; pub mod files; pub mod graph; +pub mod wallet; -pub fn router(state: Arc, enable_cors: bool) -> Router { +pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router { let app = Router::new() // Health .route("/health", get(health)) @@ -42,10 +44,23 @@ pub fn router(state: Arc, enable_cors: bool) -> Router { .route("/v1/archives/public", post(files::archive_put_public)) // Cost .route("/v1/cost/file", post(files::file_cost)) + // Wallet + .route("/v1/wallet/address", get(wallet::wallet_address)) + .route("/v1/wallet/balance", get(wallet::wallet_balance)) .with_state(state); if enable_cors { - app.layer(CorsLayer::permissive()) + // Restrict CORS to the daemon's own localhost origin to prevent + // cross-origin CSRF from malicious webpages. Non-browser clients + // (SDKs, CLI, AI agents) don't send Origin headers so are unaffected. + let origin: HeaderValue = format!("http://127.0.0.1:{rest_port}") + .parse() + .expect("valid origin header"); + let cors = CorsLayer::new() + .allow_origin(origin) + .allow_methods([Method::GET, Method::POST, Method::HEAD, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any); + app.layer(cors) } else { app } diff --git a/antd/src/rest/wallet.rs b/antd/src/rest/wallet.rs new file mode 100644 index 0000000..420f628 --- /dev/null +++ b/antd/src/rest/wallet.rs @@ -0,0 +1,37 @@ +use std::sync::Arc; + +use axum::extract::State; +use axum::Json; + +use crate::error::AntdError; +use crate::state::AppState; +use crate::types::*; + +pub async fn wallet_address( + State(state): State>, +) -> Result, AntdError> { + let wallet = state.wallet.as_ref() + .ok_or_else(|| AntdError::BadRequest("no EVM wallet configured".into()))?; + + Ok(Json(WalletAddressResponse { + address: format!("{:#x}", wallet.address()), + })) +} + +pub async fn wallet_balance( + State(state): State>, +) -> Result, AntdError> { + let wallet = state.wallet.as_ref() + .ok_or_else(|| AntdError::BadRequest("no EVM wallet configured".into()))?; + + let balance = wallet.balance_of_tokens().await + .map_err(|e| AntdError::Internal(format!("failed to get token balance: {e}")))?; + + let gas_balance = wallet.balance_of_gas_tokens().await + .map_err(|e| AntdError::Internal(format!("failed to get gas balance: {e}")))?; + + Ok(Json(WalletBalanceResponse { + balance: balance.to_string(), + gas_balance: gas_balance.to_string(), + })) +} diff --git a/antd/src/types.rs b/antd/src/types.rs index feeacb6..c40dde6 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -167,6 +167,22 @@ fn default_true() -> bool { true } +// ── Wallet ── + +#[derive(Serialize)] +pub struct WalletBalanceResponse { + /// Token balance in atto (smallest unit). + pub balance: String, + /// Gas token balance in wei. + pub gas_balance: String, +} + +#[derive(Serialize)] +pub struct WalletAddressResponse { + /// The wallet's public address (hex with 0x prefix). + pub address: String, +} + // ── Health ── #[derive(Serialize)] From 22cbbc1e8df92cbac763a809103d652f41dfcff9 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 26 Mar 2026 09:52:27 +0000 Subject: [PATCH 05/20] Refactor antd to use ant-core Client instead of raw ant-node Replace direct ant-node protocol handling with ant-core's high-level Client API. This eliminates ~200 lines of manual chunk orchestration (quote collection, payment proof building, protocol message encoding) and unblocks future data/file endpoint implementation. Changes: - Cargo.toml: ant-node path dep replaced with ant-core git dep - state.rs: AppState holds ant_core::data::Client instead of raw P2PNode + Wallet - main.rs: Construct Client::from_node().with_wallet() - chunks.rs: chunk_put/chunk_get use Client methods directly - grpc/service.rs: Same refactor for gRPC chunk handlers - error.rs: AntdError::from_core() replaces From - wallet.rs: Access wallet via client.wallet() Zero ant_node references remain in antd source code. Co-Authored-By: Claude Opus 4.6 (1M context) --- antd/Cargo.lock | 231 +++++++++++++++++++++++++++++++++- antd/Cargo.toml | 2 +- antd/src/error.rs | 41 +++--- antd/src/grpc/service.rs | 119 +++--------------- antd/src/main.rs | 64 +++++----- antd/src/rest/chunks.rs | 265 +++------------------------------------ antd/src/rest/wallet.rs | 4 +- antd/src/state.rs | 15 +-- 8 files changed, 320 insertions(+), 421 deletions(-) diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 798f6ae..6e739c4 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -826,6 +826,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ant-core" +version = "0.1.0" +source = "git+https://github.com/WithAutonomi/ant-client#1a90ca8edd66b22b35526f8575440d1e2f10554a" +dependencies = [ + "ant-evm", + "ant-node", + "async-stream", + "axum 0.8.8", + "bytes", + "evmlib", + "flate2", + "fs2", + "futures", + "futures-core", + "futures-util", + "hex", + "libc", + "libp2p", + "lru", + "multihash", + "postcard", + "rand 0.8.5", + "reqwest 0.12.28", + "rmp-serde", + "saorsa-pqc 0.5.0", + "self_encryption", + "serde", + "serde_json", + "tar", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-subscriber", + "utoipa", + "windows-sys 0.61.2", + "xor_name", + "zip", +] + [[package]] name = "ant-evm" version = "0.1.21" @@ -860,7 +902,9 @@ dependencies = [ [[package]] name = "ant-node" -version = "0.5.0" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9161d53c72cfcc0dd8fee14bff64c0bad06642f074d52c5c91d9ee203acb4042" dependencies = [ "aes-gcm-siv", "ant-evm", @@ -912,8 +956,8 @@ dependencies = [ name = "antd" version = "0.2.0" dependencies = [ + "ant-core", "ant-evm", - "ant-node", "axum 0.8.8", "base64", "blake3", @@ -2541,6 +2585,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -2756,6 +2809,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -3268,6 +3336,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -3286,9 +3370,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -4064,6 +4150,23 @@ dependencies = [ "unsigned-varint 0.7.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.29.0" @@ -4260,12 +4363,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -5069,15 +5210,21 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", + "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -5088,13 +5235,16 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", + "tokio-util", "tower 0.5.3", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] @@ -5590,7 +5740,7 @@ dependencies = [ "serde_yaml", "slab", "socket2 0.5.10", - "system-configuration", + "system-configuration 0.6.1", "thiserror 2.0.18", "time", "tinyvec", @@ -6160,6 +6310,17 @@ dependencies = [ "system-configuration-sys", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration-sys" version = "0.6.0" @@ -6353,6 +6514,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -6800,6 +6971,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + [[package]] name = "uuid" version = "1.21.0" @@ -6818,6 +7013,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -6957,6 +7158,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -7148,6 +7362,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.2.0" diff --git a/antd/Cargo.toml b/antd/Cargo.toml index 8fa86b5..eed9b6f 100644 --- a/antd/Cargo.toml +++ b/antd/Cargo.toml @@ -4,7 +4,7 @@ version = "0.2.0" edition = "2021" [dependencies] -ant-node = { path = "../../ant-node" } +ant-core = { git = "https://github.com/WithAutonomi/ant-client" } ant-evm = "0.1.19" evmlib = "0.4.9" axum = { version = "0.8", features = ["macros"] } diff --git a/antd/src/error.rs b/antd/src/error.rs index b900f44..9ca332a 100644 --- a/antd/src/error.rs +++ b/antd/src/error.rs @@ -32,6 +32,25 @@ pub enum AntdError { Internal(String), } +impl AntdError { + /// Convert an ant-core error into an AntdError. + pub fn from_core(e: ant_core::data::Error) -> Self { + use ant_core::data::Error; + match e { + Error::AlreadyStored => AntdError::AlreadyExists("already stored".into()), + Error::InvalidData(msg) => AntdError::BadRequest(msg), + Error::Payment(msg) => AntdError::Payment(msg), + Error::Network(msg) => AntdError::Network(msg), + Error::Timeout(msg) => AntdError::Timeout(msg), + Error::InsufficientPeers(msg) => AntdError::Network(msg), + Error::Protocol(msg) => AntdError::Internal(msg), + Error::Encryption(msg) => AntdError::Internal(msg), + Error::Serialization(msg) => AntdError::Internal(msg), + other => AntdError::Internal(other.to_string()), + } + } +} + #[derive(Serialize)] struct ErrorBody { error: String, @@ -73,25 +92,3 @@ impl From for tonic::Status { } } } - -// Conversion from ant-node protocol errors - -impl From for AntdError { - fn from(e: ant_node::ant_protocol::ProtocolError) -> Self { - use ant_node::ant_protocol::ProtocolError; - match e { - ProtocolError::ChunkTooLarge { size, max_size } => { - AntdError::BadRequest(format!("chunk size {size} exceeds maximum {max_size}")) - } - ProtocolError::MessageTooLarge { size, max_size } => { - AntdError::BadRequest(format!("message size {size} exceeds maximum {max_size}")) - } - ProtocolError::AddressMismatch { .. } => { - AntdError::BadRequest(format!("address mismatch: {e}")) - } - ProtocolError::PaymentFailed(msg) => AntdError::Payment(msg), - ProtocolError::StorageFailed(msg) => AntdError::Internal(msg), - other => AntdError::Internal(other.to_string()), - } - } -} diff --git a/antd/src/grpc/service.rs b/antd/src/grpc/service.rs index c02c135..a9723f9 100644 --- a/antd/src/grpc/service.rs +++ b/antd/src/grpc/service.rs @@ -1,7 +1,9 @@ use std::sync::Arc; +use bytes::Bytes; use tonic::{Request, Response, Status}; +use crate::error::AntdError; use crate::state::AppState; // Generated protobuf modules @@ -11,7 +13,7 @@ pub mod pb { } fn not_implemented(op: &str) -> Status { - Status::unimplemented(format!("{op} not yet implemented for ant-node")) + Status::unimplemented(format!("{op} not yet implemented")) } // ── HealthService ── @@ -83,53 +85,13 @@ impl pb::chunk_service_server::ChunkService for ChunkServiceImpl { .try_into() .map_err(|_| Status::invalid_argument("address must be 32 bytes"))?; - // Use the same chunk_get logic as REST - // TODO: Factor out shared chunk client logic - use ant_node::ant_protocol::{ - ChunkGetRequest as ProtoGetReq, ChunkGetResponse as ProtoGetResp, - ChunkMessage, ChunkMessageBody, - }; - use ant_node::client::send_and_await_chunk_response; - use std::time::Duration; + let chunk = self.state.client.chunk_get(&address).await + .map_err(|e| tonic::Status::from(AntdError::from_core(e)))? + .ok_or_else(|| Status::not_found("chunk not found"))?; - let connected_peers = self.state.node.connected_peers().await; - let peer_id = connected_peers.first() - .ok_or_else(|| Status::unavailable("not connected to any peers"))?; - let peer_addrs: Vec<_> = self.state.bootstrap_peers.clone(); - - let request_id = rand::random::(); - let msg = ChunkMessage { - request_id, - body: ChunkMessageBody::GetRequest(ProtoGetReq::new(address)), - }; - let msg_bytes = msg.encode() - .map_err(|e| Status::internal(format!("encode error: {e}")))?; - - let content: Vec = send_and_await_chunk_response( - &self.state.node, - peer_id, - msg_bytes, - request_id, - Duration::from_secs(30), - &peer_addrs, - |body| match body { - ChunkMessageBody::GetResponse(ProtoGetResp::Success { content, .. }) => { - Some(Ok(content)) - } - ChunkMessageBody::GetResponse(ProtoGetResp::NotFound { .. }) => { - Some(Err(Status::not_found("chunk not found"))) - } - ChunkMessageBody::GetResponse(ProtoGetResp::Error(e)) => { - Some(Err(Status::internal(e.to_string()))) - } - _ => None, - }, - |e| Status::unavailable(format!("failed to send: {e}")), - || Status::deadline_exceeded("chunk get timed out"), - ) - .await?; - - Ok(Response::new(pb::GetChunkResponse { data: content })) + Ok(Response::new(pb::GetChunkResponse { + data: chunk.content.to_vec(), + })) } async fn put( @@ -138,64 +100,19 @@ impl pb::chunk_service_server::ChunkService for ChunkServiceImpl { ) -> Result, Status> { let data = request.into_inner().data; - use ant_node::ant_protocol::{ - ChunkMessage, ChunkMessageBody, MAX_CHUNK_SIZE, - ChunkPutRequest as ProtoPutReq, ChunkPutResponse as ProtoPutResp, - }; - use ant_node::client::{compute_address, send_and_await_chunk_response}; - use std::time::Duration; - - if data.len() > MAX_CHUNK_SIZE { - return Err(Status::invalid_argument(format!( - "chunk size {} exceeds maximum {MAX_CHUNK_SIZE}", data.len() - ))); + if self.state.client.wallet().is_none() { + return Err(Status::failed_precondition( + "no EVM wallet configured — set AUTONOMI_WALLET_KEY", + )); } - let address = compute_address(&data); - - let connected_peers = self.state.node.connected_peers().await; - let peer_id = connected_peers.first() - .ok_or_else(|| Status::unavailable("not connected to any peers"))?; - let peer_addrs: Vec<_> = self.state.bootstrap_peers.clone(); - - let request_id = rand::random::(); - let msg = ChunkMessage { - request_id, - body: ChunkMessageBody::PutRequest(ProtoPutReq::new(address, data)), - }; - let msg_bytes = msg.encode() - .map_err(|e| Status::internal(format!("encode error: {e}")))?; - - let result_address: [u8; 32] = send_and_await_chunk_response( - &self.state.node, - peer_id, - msg_bytes, - request_id, - Duration::from_secs(30), - &peer_addrs, - |body| match body { - ChunkMessageBody::PutResponse(ProtoPutResp::Success { address }) => { - Some(Ok(address)) - } - ChunkMessageBody::PutResponse(ProtoPutResp::AlreadyExists { address }) => { - Some(Ok(address)) - } - ChunkMessageBody::PutResponse(ProtoPutResp::PaymentRequired { message }) => { - Some(Err(Status::failed_precondition(message))) - } - ChunkMessageBody::PutResponse(ProtoPutResp::Error(e)) => { - Some(Err(Status::internal(e.to_string()))) - } - _ => None, - }, - |e| Status::unavailable(format!("failed to send: {e}")), - || Status::deadline_exceeded("chunk put timed out"), - ) - .await?; + let content = Bytes::from(data); + let address = self.state.client.chunk_put(content).await + .map_err(|e| tonic::Status::from(AntdError::from_core(e)))?; Ok(Response::new(pb::PutChunkResponse { - cost: Some(pb::Cost { atto_tokens: "0".into() }), - address: hex::encode(result_address), + cost: Some(pb::Cost { atto_tokens: String::new() }), + address: hex::encode(address), })) } } diff --git a/antd/src/main.rs b/antd/src/main.rs index f695a97..314d2d0 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use clap::Parser; use tracing_subscriber::EnvFilter; -use ant_node::core::{CoreNodeConfig, MultiAddr, NodeMode, P2PNode}; +use ant_core::data::{ + Client, ClientConfig, CoreNodeConfig, MultiAddr, NodeMode, P2PNode, Wallet, +}; mod config; mod error; @@ -126,44 +128,42 @@ async fn main() -> Result<(), Box> { let peers = node.connected_peers().await; tracing::info!(count = peers.len(), peers = ?peers, "peer status at startup"); + // Build ant-core Client from the P2P node + let node = Arc::new(node); + let mut client = Client::from_node(node, ClientConfig::default()); + // Load EVM wallet if configured - let wallet = match std::env::var("AUTONOMI_WALLET_KEY") { - Ok(wallet_key) => { - let rpc_url = std::env::var("EVM_RPC_URL") - .unwrap_or_else(|_| "http://127.0.0.1:8545".to_string()); - let token_addr = std::env::var("EVM_PAYMENT_TOKEN_ADDRESS") - .unwrap_or_default(); - let payments_addr = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") - .unwrap_or_default(); - tracing::info!(%rpc_url, "loading EVM wallet..."); - let network = evmlib::Network::new_custom( - &rpc_url, - &token_addr, - &payments_addr, - None, - ); - match evmlib::wallet::Wallet::new_from_private_key(network, &wallet_key) { - Ok(w) => { - tracing::info!(address = %w.address(), "EVM wallet loaded"); - Some(w) - } - Err(e) => { - tracing::warn!("failed to load EVM wallet: {e}"); - None - } + if let Ok(wallet_key) = std::env::var("AUTONOMI_WALLET_KEY") { + let rpc_url = std::env::var("EVM_RPC_URL") + .unwrap_or_else(|_| "http://127.0.0.1:8545".to_string()); + let token_addr = std::env::var("EVM_PAYMENT_TOKEN_ADDRESS") + .unwrap_or_default(); + let payments_addr = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") + .unwrap_or_default(); + tracing::info!(%rpc_url, "loading EVM wallet..."); + let network = evmlib::Network::new_custom( + &rpc_url, + &token_addr, + &payments_addr, + None, + ); + match Wallet::new_from_private_key(network, &wallet_key) { + Ok(w) => { + tracing::info!(address = %w.address(), "EVM wallet loaded"); + client = client.with_wallet(w); + } + Err(e) => { + tracing::warn!("failed to load EVM wallet: {e}"); } } - Err(_) => { - tracing::info!("no AUTONOMI_WALLET_KEY set — write operations will fail"); - None - } - }; + } else { + tracing::info!("no AUTONOMI_WALLET_KEY set — write operations will fail"); + } let state = Arc::new(AppState { - node: Arc::new(node), + client, network: config.network.clone(), bootstrap_peers, - wallet, }); // Build REST router diff --git a/antd/src/rest/chunks.rs b/antd/src/rest/chunks.rs index fb80cb4..8560869 100644 --- a/antd/src/rest/chunks.rs +++ b/antd/src/rest/chunks.rs @@ -1,62 +1,15 @@ use std::sync::Arc; -use std::time::Duration; use axum::extract::{Path, State}; use axum::Json; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; - -use ant_node::ant_protocol::{ - ChunkGetRequest, ChunkGetResponse as ProtoGetResponse, - ChunkMessage, ChunkMessageBody, - ChunkPutRequest as ProtoPutRequest, ChunkPutResponse as ProtoPutResponse, - ChunkQuoteRequest, ChunkQuoteResponse as ProtoQuoteResponse, - MAX_CHUNK_SIZE, -}; -use ant_node::client::compute_address; -use ant_node::payment::single_node::REQUIRED_QUOTES; -use ant_node::client::send_and_await_chunk_response; +use bytes::Bytes; use crate::error::AntdError; use crate::state::AppState; use crate::types::*; -/// Default timeout for chunk operations. -const CHUNK_TIMEOUT: Duration = Duration::from_secs(30); - -/// Find a peer to route a chunk request to. -/// Tries connected peers first, falls back to reconnecting to a bootstrap peer. -async fn find_peer( - state: &AppState, -) -> Result<(ant_node::core::PeerId, Vec), AntdError> { - if state.bootstrap_peers.is_empty() { - return Err(AntdError::Network("no bootstrap peers available".into())); - } - - // Try connected peers first - let connected_peers = state.node.connected_peers().await; - if let Some(peer_id) = connected_peers.first() { - return Ok((peer_id.clone(), state.bootstrap_peers.clone())); - } - - // No connected peers — reconnect to first bootstrap peer - tracing::info!("no connected peers, reconnecting to bootstrap..."); - let peer_addr = &state.bootstrap_peers[0]; - match state.node.connect_peer(peer_addr).await { - Ok(_channel_id) => { - // Wait briefly for connection to register - tokio::time::sleep(Duration::from_millis(200)).await; - let peers = state.node.connected_peers().await; - if let Some(peer_id) = peers.first() { - Ok((peer_id.clone(), state.bootstrap_peers.clone())) - } else { - Err(AntdError::Network("connected but peer not yet registered".into())) - } - } - Err(e) => Err(AntdError::Network(format!("failed to reconnect: {e}"))), - } -} - pub async fn chunk_get( State(state): State>, Path(addr): Path, @@ -67,41 +20,12 @@ pub async fn chunk_get( .try_into() .map_err(|_| AntdError::BadRequest("address must be 32 bytes".into()))?; - let (peer_id, peer_addrs) = find_peer(&state).await?; - let request_id = rand::random::(); - - let msg = ChunkMessage { - request_id, - body: ChunkMessageBody::GetRequest(ChunkGetRequest::new(address)), - }; - let msg_bytes = msg.encode().map_err(AntdError::from)?; - - let content: Vec = send_and_await_chunk_response( - &state.node, - &peer_id, - msg_bytes, - request_id, - CHUNK_TIMEOUT, - &peer_addrs, - |body| match body { - ChunkMessageBody::GetResponse(ProtoGetResponse::Success { content, .. }) => { - Some(Ok(content)) - } - ChunkMessageBody::GetResponse(ProtoGetResponse::NotFound { .. }) => { - Some(Err(AntdError::NotFound("chunk not found".into()))) - } - ChunkMessageBody::GetResponse(ProtoGetResponse::Error(e)) => { - Some(Err(AntdError::from(e))) - } - _ => None, - }, - |e| AntdError::Network(format!("failed to send get request: {e}")), - || AntdError::Timeout("chunk get timed out".into()), - ) - .await?; + let chunk = state.client.chunk_get(&address).await + .map_err(|e| AntdError::from_core(e))? + .ok_or_else(|| AntdError::NotFound("chunk not found".into()))?; Ok(Json(ChunkGetResponse { - data: BASE64.encode(&content), + data: BASE64.encode(&chunk.content), })) } @@ -109,181 +33,22 @@ pub async fn chunk_put( State(state): State>, Json(req): Json, ) -> Result, AntdError> { - let wallet = state.wallet.as_ref() - .ok_or_else(|| AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into()))?; + if state.client.wallet().is_none() { + return Err(AntdError::Payment( + "no EVM wallet configured — set AUTONOMI_WALLET_KEY".into(), + )); + } let data = BASE64 .decode(&req.data) .map_err(|e| AntdError::BadRequest(format!("invalid base64: {e}")))?; - if data.len() > MAX_CHUNK_SIZE { - return Err(AntdError::BadRequest(format!( - "chunk size {} exceeds maximum {}", - data.len(), - MAX_CHUNK_SIZE - ))); - } - - let address = compute_address(&data); - - // ── Step 1: Get quotes from 5 peers ── - tracing::info!(addr = hex::encode(address), "requesting storage quotes from 5 peers..."); - - let connected_peers = state.node.connected_peers().await; - if connected_peers.len() < REQUIRED_QUOTES { - // Try reconnecting - for peer_addr in &state.bootstrap_peers { - if state.node.connected_peers().await.len() >= REQUIRED_QUOTES { - break; - } - let _ = state.node.connect_peer(peer_addr).await; - tokio::time::sleep(Duration::from_millis(100)).await; - } - } - - let peers = state.node.connected_peers().await; - if peers.len() < REQUIRED_QUOTES { - return Err(AntdError::Network(format!( - "need {} connected peers for payment, have {}", - REQUIRED_QUOTES, - peers.len() - ))); - } - - let peer_addrs = state.bootstrap_peers.clone(); - let mut quotes_with_prices: Vec<(ant_evm::PaymentQuote, ant_evm::Amount)> = Vec::new(); - let mut quote_peer_ids: Vec = Vec::new(); - - for peer_id in peers.iter().take(REQUIRED_QUOTES) { - let request_id = rand::random::(); - let msg = ChunkMessage { - request_id, - body: ChunkMessageBody::QuoteRequest(ChunkQuoteRequest::new( - address, - data.len() as u64, - )), - }; - let msg_bytes = msg.encode().map_err(AntdError::from)?; - - let (quote_bytes, already_stored): (Vec, bool) = send_and_await_chunk_response( - &state.node, - peer_id, - msg_bytes, - request_id, - CHUNK_TIMEOUT, - &peer_addrs, - |body| match body { - ChunkMessageBody::QuoteResponse(ProtoQuoteResponse::Success { - quote, - already_stored, - }) => Some(Ok((quote, already_stored))), - ChunkMessageBody::QuoteResponse(ProtoQuoteResponse::Error(e)) => { - Some(Err(AntdError::from(e))) - } - _ => None, - }, - |e| AntdError::Network(format!("failed to send quote request: {e}")), - || AntdError::Timeout("quote request timed out".into()), - ) - .await?; - - if already_stored { - tracing::info!(addr = hex::encode(address), "chunk already stored"); - return Ok(Json(ChunkPutResponse { - cost: "0".to_string(), - address: hex::encode(address), - })); - } - - let payment_quote: ant_evm::PaymentQuote = rmp_serde::from_slice("e_bytes) - .map_err(|e| AntdError::Internal(format!("failed to deserialize quote: {e}")))?; - - let price = ant_node::payment::calculate_price(&payment_quote.quoting_metrics); - quotes_with_prices.push((payment_quote, price)); - quote_peer_ids.push(peer_id.clone()); - } - - tracing::info!(addr = hex::encode(address), "got {} quotes", quotes_with_prices.len()); - - // ── Step 2: Create SingleNode payment and pay on-chain ── - // Save the original quote order before SingleNodePayment sorts them - let original_quotes: Vec = quotes_with_prices.iter().map(|(q, _)| q.clone()).collect(); - - let single_payment = ant_node::payment::SingleNodePayment::from_quotes(quotes_with_prices) - .map_err(|e| AntdError::Payment(format!("failed to create payment: {e}")))?; - - let cost = single_payment.total_amount(); - let cost_str = cost.to_string(); - tracing::info!(addr = hex::encode(address), cost = %cost_str, "paying on-chain..."); - - let tx_hashes = single_payment.pay(wallet).await - .map_err(|e| AntdError::Payment(format!("EVM payment failed: {e}")))?; - - tracing::info!(addr = hex::encode(address), cost = %cost_str, "payment submitted"); - - // ── Step 3: Build proof and store chunk ── - // Build ProofOfPayment with all 5 (peer_id, quote) pairs - let mut peer_quotes = Vec::new(); - for (i, quote) in original_quotes.into_iter().enumerate() { - let encoded_peer_id = ant_node::client::hex_node_id_to_encoded_peer_id( - "e_peer_ids[i].to_hex() - ).map_err(|e| AntdError::Internal(format!("failed to encode peer ID: {e}")))?; - peer_quotes.push((encoded_peer_id, quote)); - } - - let payment_proof = ant_node::payment::PaymentProof { - proof_of_payment: ant_evm::ProofOfPayment { peer_quotes }, - tx_hashes, - }; - let proof_bytes = rmp_serde::to_vec(&payment_proof) - .map_err(|e| AntdError::Internal(format!("failed to serialize proof: {e}")))?; - - tracing::info!(addr = hex::encode(address), "storing chunk with payment proof..."); - - // Send PUT to the first peer (who should be one of the 5 closest) - let (put_peer_id, _) = find_peer(&state).await?; - let put_request_id = rand::random::(); - let put_msg = ChunkMessage { - request_id: put_request_id, - body: ChunkMessageBody::PutRequest(ProtoPutRequest::with_payment( - address, - data, - proof_bytes, - )), - }; - let put_msg_bytes = put_msg.encode().map_err(AntdError::from)?; - - let result_address: [u8; 32] = send_and_await_chunk_response( - &state.node, - &put_peer_id, - put_msg_bytes, - put_request_id, - CHUNK_TIMEOUT, - &peer_addrs, - |body| match body { - ChunkMessageBody::PutResponse(ProtoPutResponse::Success { address }) => { - Some(Ok(address)) - } - ChunkMessageBody::PutResponse(ProtoPutResponse::AlreadyExists { address }) => { - Some(Ok(address)) - } - ChunkMessageBody::PutResponse(ProtoPutResponse::PaymentRequired { message }) => { - Some(Err(AntdError::Payment(message))) - } - ChunkMessageBody::PutResponse(ProtoPutResponse::Error(e)) => { - Some(Err(AntdError::from(e))) - } - _ => None, - }, - |e| AntdError::Network(format!("failed to send put request: {e}")), - || AntdError::Timeout("chunk put timed out".into()), - ) - .await?; - - tracing::info!(addr = hex::encode(result_address), cost = %cost_str, "chunk stored successfully"); + let content = Bytes::from(data); + let address = state.client.chunk_put(content).await + .map_err(|e| AntdError::from_core(e))?; Ok(Json(ChunkPutResponse { - cost: cost_str, - address: hex::encode(result_address), + cost: String::new(), // TODO: Client.chunk_put doesn't return cost yet + address: hex::encode(address), })) } diff --git a/antd/src/rest/wallet.rs b/antd/src/rest/wallet.rs index 420f628..e192d71 100644 --- a/antd/src/rest/wallet.rs +++ b/antd/src/rest/wallet.rs @@ -10,7 +10,7 @@ use crate::types::*; pub async fn wallet_address( State(state): State>, ) -> Result, AntdError> { - let wallet = state.wallet.as_ref() + let wallet = state.client.wallet() .ok_or_else(|| AntdError::BadRequest("no EVM wallet configured".into()))?; Ok(Json(WalletAddressResponse { @@ -21,7 +21,7 @@ pub async fn wallet_address( pub async fn wallet_balance( State(state): State>, ) -> Result, AntdError> { - let wallet = state.wallet.as_ref() + let wallet = state.client.wallet() .ok_or_else(|| AntdError::BadRequest("no EVM wallet configured".into()))?; let balance = wallet.balance_of_tokens().await diff --git a/antd/src/state.rs b/antd/src/state.rs index c2d4601..b5fb1c2 100644 --- a/antd/src/state.rs +++ b/antd/src/state.rs @@ -1,16 +1,11 @@ -use std::sync::Arc; - -use ant_node::core::P2PNode; +use ant_core::data::{Client, MultiAddr}; /// Shared application state passed to all handlers. -#[derive(Clone)] pub struct AppState { - /// The Autonomi P2P node in client mode. - pub node: Arc, + /// High-level Autonomi client (wraps P2P node, wallet, cache). + pub client: Client, /// Network mode label ("local", "default", etc.) pub network: String, - /// Bootstrap peer addresses for chunk routing. - pub bootstrap_peers: Vec, - /// EVM wallet for paying storage quotes (optional — not needed for reads). - pub wallet: Option, + /// Bootstrap peer addresses. + pub bootstrap_peers: Vec, } From 45a979b49da7b06cabc14b533d326655327ae2f0 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 26 Mar 2026 10:30:00 +0000 Subject: [PATCH 06/20] Add payment_mode parameter for merkle batch payments Adds optional payment_mode ("auto", "merkle", "single") to all data and file upload methods across antd and all 15 SDKs. antd types: - DataPutRequest and FileUploadRequest now accept payment_mode - DataPutPublicResponse returns chunks_stored and payment_mode_used - parse_payment_mode/format_payment_mode helpers added Data and file endpoints remain stubs (501 Not Implemented) due to an upstream lifetime issue in ant-core's stream closures that prevents the Client methods from being called in async axum handlers. The types and parameter plumbing are in place for when ant-core is fixed. AppState.client wrapped in Arc for Send + Sync compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/include/antd/client.hpp | 8 +- antd-cpp/src/client.cpp | 40 ++++++---- antd-csharp/Antd.Sdk/AntdGrpcClient.cs | 8 +- antd-csharp/Antd.Sdk/AntdRestClient.cs | 28 +++++-- antd-csharp/Antd.Sdk/IAntdClient.cs | 8 +- antd-dart/lib/src/client.dart | 32 +++++--- antd-elixir/lib/antd/client.ex | 60 +++++++++----- antd-go/client.go | 52 +++++++++--- .../java/com/autonomi/antd/AntdClient.java | 32 +++++++- antd-js/src/rest-client.ts | 32 ++++---- .../kotlin/com/autonomi/sdk/AntdGrpcClient.kt | 8 +- .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 28 +++++-- .../kotlin/com/autonomi/sdk/IAntdClient.kt | 8 +- antd-lua/src/antd/client.lua | 44 +++++++--- antd-php/src/AntdClient.php | 80 +++++++++++-------- antd-py/src/antd/_rest.py | 56 +++++++++---- antd-ruby/lib/antd/client.rb | 24 ++++-- antd-rust/src/client.rs | 32 ++++++-- .../Sources/AntdSdk/AntdRestClient.swift | 24 ++++-- antd-zig/src/antd.zig | 42 +++++++--- antd-zig/src/json_helpers.zig | 16 ++++ antd/src/main.rs | 2 +- antd/src/rest/data.rs | 36 ++++----- antd/src/rest/files.rs | 19 +++-- antd/src/state.rs | 5 +- antd/src/types.rs | 33 +++++++- 26 files changed, 511 insertions(+), 246 deletions(-) diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index 5f71c22..58b78f2 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -51,13 +51,13 @@ class Client { // --- Data (Immutable) --- /// Store public immutable data on the network. - PutResult data_put_public(const std::vector& data); + PutResult data_put_public(const std::vector& data, const std::string& payment_mode = ""); /// Retrieve public data by address. std::vector data_get_public(std::string_view address); /// Store private encrypted data on the network. - PutResult data_put_private(const std::vector& data); + PutResult data_put_private(const std::vector& data, const std::string& payment_mode = ""); /// Retrieve private data using a data map. std::vector data_get_private(std::string_view data_map); @@ -93,13 +93,13 @@ class Client { // --- Files & Directories --- /// Upload a local file to the network. - PutResult file_upload_public(std::string_view path); + PutResult file_upload_public(std::string_view path, const std::string& payment_mode = ""); /// Download a file from the network to a local path. void file_download_public(std::string_view address, std::string_view dest_path); /// Upload a local directory to the network. - PutResult dir_upload_public(std::string_view path); + PutResult dir_upload_public(std::string_view path, const std::string& payment_mode = ""); /// Download a directory from the network to a local path. void dir_download_public(std::string_view address, std::string_view dest_path); diff --git a/antd-cpp/src/client.cpp b/antd-cpp/src/client.cpp index a725d6c..00a46cc 100644 --- a/antd-cpp/src/client.cpp +++ b/antd-cpp/src/client.cpp @@ -107,10 +107,12 @@ HealthStatus Client::health() { // Data (Immutable) // --------------------------------------------------------------------------- -PutResult Client::data_put_public(const std::vector& data) { - auto j = impl_->do_json("POST", "/v1/data/public", json{ - {"data", detail::base64_encode(data)}, - }); +PutResult Client::data_put_public(const std::vector& data, const std::string& payment_mode) { + json body = {{"data", detail::base64_encode(data)}}; + if (!payment_mode.empty()) { + body["payment_mode"] = payment_mode; + } + auto j = impl_->do_json("POST", "/v1/data/public", body); return PutResult{ .cost = j.value("cost", ""), .address = j.value("address", ""), @@ -122,10 +124,12 @@ std::vector Client::data_get_public(std::string_view address) { return detail::base64_decode(j.value("data", "")); } -PutResult Client::data_put_private(const std::vector& data) { - auto j = impl_->do_json("POST", "/v1/data/private", json{ - {"data", detail::base64_encode(data)}, - }); +PutResult Client::data_put_private(const std::vector& data, const std::string& payment_mode) { + json body = {{"data", detail::base64_encode(data)}}; + if (!payment_mode.empty()) { + body["payment_mode"] = payment_mode; + } + auto j = impl_->do_json("POST", "/v1/data/private", body); return PutResult{ .cost = j.value("cost", ""), .address = j.value("data_map", ""), @@ -240,10 +244,12 @@ std::string Client::graph_entry_cost(std::string_view public_key) { // Files & Directories // --------------------------------------------------------------------------- -PutResult Client::file_upload_public(std::string_view path) { - auto j = impl_->do_json("POST", "/v1/files/upload/public", json{ - {"path", std::string(path)}, - }); +PutResult Client::file_upload_public(std::string_view path, const std::string& payment_mode) { + json body = {{"path", std::string(path)}}; + if (!payment_mode.empty()) { + body["payment_mode"] = payment_mode; + } + auto j = impl_->do_json("POST", "/v1/files/upload/public", body); return PutResult{ .cost = j.value("cost", ""), .address = j.value("address", ""), @@ -257,10 +263,12 @@ void Client::file_download_public(std::string_view address, std::string_view des }); } -PutResult Client::dir_upload_public(std::string_view path) { - auto j = impl_->do_json("POST", "/v1/dirs/upload/public", json{ - {"path", std::string(path)}, - }); +PutResult Client::dir_upload_public(std::string_view path, const std::string& payment_mode) { + json body = {{"path", std::string(path)}}; + if (!payment_mode.empty()) { + body["payment_mode"] = payment_mode; + } + auto j = impl_->do_json("POST", "/v1/dirs/upload/public", body); return PutResult{ .cost = j.value("cost", ""), .address = j.value("address", ""), diff --git a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs index 0cae715..1f44f71 100644 --- a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs +++ b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs @@ -63,7 +63,7 @@ public async Task HealthAsync() // ── Data ── - public async Task DataPutPublicAsync(byte[] data) + public async Task DataPutPublicAsync(byte[] data, string? paymentMode = null) { try { @@ -83,7 +83,7 @@ public async Task DataGetPublicAsync(string address) catch (RpcException ex) { throw Wrap(ex); } } - public async Task DataPutPrivateAsync(byte[] data) + public async Task DataPutPrivateAsync(byte[] data, string? paymentMode = null) { try { @@ -195,7 +195,7 @@ public async Task GraphEntryCostAsync(string publicKey) // ── Files ── - public async Task FileUploadPublicAsync(string path) + public async Task FileUploadPublicAsync(string path, string? paymentMode = null) { try { @@ -214,7 +214,7 @@ public async Task FileDownloadPublicAsync(string address, string destPath) catch (RpcException ex) { throw Wrap(ex); } } - public async Task DirUploadPublicAsync(string path) + public async Task DirUploadPublicAsync(string path, string? paymentMode = null) { try { diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index 61dc465..ac40716 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -91,9 +91,12 @@ public async Task HealthAsync() // ── Data ── - public async Task DataPutPublicAsync(byte[] data) + public async Task DataPutPublicAsync(byte[] data, string? paymentMode = null) { - var resp = await PostJsonAsync("/v1/data/public", new { data = Convert.ToBase64String(data) }); + 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); } @@ -103,9 +106,12 @@ public async Task DataGetPublicAsync(string address) return Convert.FromBase64String(resp.Data); } - public async Task DataPutPrivateAsync(byte[] data) + public async Task DataPutPrivateAsync(byte[] data, string? paymentMode = null) { - var resp = await PostJsonAsync("/v1/data/private", new { data = Convert.ToBase64String(data) }); + 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); } @@ -167,9 +173,12 @@ public async Task GraphEntryCostAsync(string publicKey) // ── Files ── - public async Task FileUploadPublicAsync(string path) + public async Task FileUploadPublicAsync(string path, string? paymentMode = null) { - var resp = await PostJsonAsync("/v1/files/upload/public", new { path }); + object body = paymentMode != null + ? new { path, payment_mode = paymentMode } + : (object)new { path }; + var resp = await PostJsonAsync("/v1/files/upload/public", body); return new PutResult(resp.Cost, resp.Address); } @@ -178,9 +187,12 @@ public async Task FileDownloadPublicAsync(string address, string destPath) await PostJsonNoResultAsync("/v1/files/download/public", new { address, dest_path = destPath }); } - public async Task DirUploadPublicAsync(string path) + public async Task DirUploadPublicAsync(string path, string? paymentMode = null) { - var resp = await PostJsonAsync("/v1/dirs/upload/public", new { path }); + object body = paymentMode != null + ? new { path, payment_mode = paymentMode } + : (object)new { path }; + var resp = await PostJsonAsync("/v1/dirs/upload/public", body); return new PutResult(resp.Cost, resp.Address); } diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index ef1ffdf..c92147c 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -6,9 +6,9 @@ public interface IAntdClient : IDisposable Task HealthAsync(); // Data - Task DataPutPublicAsync(byte[] data); + Task DataPutPublicAsync(byte[] data, string? paymentMode = null); Task DataGetPublicAsync(string address); - Task DataPutPrivateAsync(byte[] data); + Task DataPutPrivateAsync(byte[] data, string? paymentMode = null); Task DataGetPrivateAsync(string dataMap); Task DataCostAsync(byte[] data); @@ -23,9 +23,9 @@ public interface IAntdClient : IDisposable Task GraphEntryCostAsync(string publicKey); // Files - Task FileUploadPublicAsync(string path); + Task FileUploadPublicAsync(string path, string? paymentMode = null); Task FileDownloadPublicAsync(string address, string destPath); - Task DirUploadPublicAsync(string path); + Task DirUploadPublicAsync(string path, string? paymentMode = null); Task DirDownloadPublicAsync(string address, string destPath); Task ArchiveGetPublicAsync(string address); Task ArchivePutPublicAsync(Archive archive); diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index f74b619..1fae92a 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -143,10 +143,12 @@ class AntdClient { // --- Data --- /// Stores public immutable data on the network. - Future dataPutPublic(Uint8List data) async { - final json = await _doJson('POST', '/v1/data/public', { + Future dataPutPublic(Uint8List data, {String? paymentMode}) async { + final body = { 'data': _b64Encode(data), - }); + }; + if (paymentMode != null) body['payment_mode'] = paymentMode; + final json = await _doJson('POST', '/v1/data/public', body); return PutResult.fromJson(json!); } @@ -157,10 +159,12 @@ class AntdClient { } /// Stores private encrypted data on the network. - Future dataPutPrivate(Uint8List data) async { - final json = await _doJson('POST', '/v1/data/private', { + Future dataPutPrivate(Uint8List data, {String? paymentMode}) async { + final body = { 'data': _b64Encode(data), - }); + }; + if (paymentMode != null) body['payment_mode'] = paymentMode; + final json = await _doJson('POST', '/v1/data/private', body); return PutResult.fromJson(json!, addressKey: 'data_map'); } @@ -242,10 +246,12 @@ class AntdClient { // --- Files --- /// Uploads a local file to the network. - Future fileUploadPublic(String path) async { - final json = await _doJson('POST', '/v1/files/upload/public', { + Future fileUploadPublic(String path, {String? paymentMode}) async { + final body = { 'path': path, - }); + }; + if (paymentMode != null) body['payment_mode'] = paymentMode; + final json = await _doJson('POST', '/v1/files/upload/public', body); return PutResult.fromJson(json!); } @@ -258,10 +264,12 @@ class AntdClient { } /// Uploads a local directory to the network. - Future dirUploadPublic(String path) async { - final json = await _doJson('POST', '/v1/dirs/upload/public', { + Future dirUploadPublic(String path, {String? paymentMode}) async { + final body = { 'path': path, - }); + }; + if (paymentMode != null) body['payment_mode'] = paymentMode; + final json = await _doJson('POST', '/v1/dirs/upload/public', body); return PutResult.fromJson(json!); } diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index d5eee9a..362688a 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -89,9 +89,14 @@ defmodule Antd.Client do # --------------------------------------------------------------------------- @doc "Stores public immutable data on the network." - @spec data_put_public(t(), binary()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} - def data_put_public(%__MODULE__{} = client, data) when is_binary(data) do - case do_json(client, :post, "/v1/data/public", %{data: Base.encode64(data)}) do + @spec data_put_public(t(), binary(), keyword()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} + def data_put_public(%__MODULE__{} = client, data, opts \\ []) when is_binary(data) do + payload = %{data: Base.encode64(data)} + payload = case Keyword.get(opts, :payment_mode) do + nil -> payload + mode -> Map.put(payload, :payment_mode, mode) + end + case do_json(client, :post, "/v1/data/public", payload) do {:ok, body} -> {:ok, %Antd.PutResult{cost: body["cost"], address: body["address"]}} @@ -101,8 +106,8 @@ defmodule Antd.Client do end @doc "Like `data_put_public/2` but raises on error." - @spec data_put_public!(t(), binary()) :: Antd.PutResult.t() - def data_put_public!(client, data), do: unwrap!(data_put_public(client, data)) + @spec data_put_public!(t(), binary(), keyword()) :: Antd.PutResult.t() + def data_put_public!(client, data, opts \\ []), do: unwrap!(data_put_public(client, data, opts)) @doc "Retrieves public data by address." @spec data_get_public(t(), String.t()) :: {:ok, binary()} | {:error, Exception.t()} @@ -118,9 +123,14 @@ defmodule Antd.Client do def data_get_public!(client, address), do: unwrap!(data_get_public(client, address)) @doc "Stores private encrypted data on the network." - @spec data_put_private(t(), binary()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} - def data_put_private(%__MODULE__{} = client, data) when is_binary(data) do - case do_json(client, :post, "/v1/data/private", %{data: Base.encode64(data)}) do + @spec data_put_private(t(), binary(), keyword()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} + def data_put_private(%__MODULE__{} = client, data, opts \\ []) when is_binary(data) do + payload = %{data: Base.encode64(data)} + payload = case Keyword.get(opts, :payment_mode) do + nil -> payload + mode -> Map.put(payload, :payment_mode, mode) + end + case do_json(client, :post, "/v1/data/private", payload) do {:ok, body} -> {:ok, %Antd.PutResult{cost: body["cost"], address: body["data_map"]}} @@ -130,8 +140,8 @@ defmodule Antd.Client do end @doc "Like `data_put_private/2` but raises on error." - @spec data_put_private!(t(), binary()) :: Antd.PutResult.t() - def data_put_private!(client, data), do: unwrap!(data_put_private(client, data)) + @spec data_put_private!(t(), binary(), keyword()) :: Antd.PutResult.t() + def data_put_private!(client, data, opts \\ []), do: unwrap!(data_put_private(client, data, opts)) @doc "Retrieves private data using a data map." @spec data_get_private(t(), String.t()) :: {:ok, binary()} | {:error, Exception.t()} @@ -291,9 +301,14 @@ defmodule Antd.Client do # --------------------------------------------------------------------------- @doc "Uploads a local file to the network." - @spec file_upload_public(t(), String.t()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} - def file_upload_public(%__MODULE__{} = client, path) do - case do_json(client, :post, "/v1/files/upload/public", %{path: path}) do + @spec file_upload_public(t(), String.t(), keyword()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} + def file_upload_public(%__MODULE__{} = client, path, opts \\ []) do + payload = %{path: path} + payload = case Keyword.get(opts, :payment_mode) do + nil -> payload + mode -> Map.put(payload, :payment_mode, mode) + end + case do_json(client, :post, "/v1/files/upload/public", payload) do {:ok, body} -> {:ok, %Antd.PutResult{cost: body["cost"], address: body["address"]}} @@ -303,8 +318,8 @@ defmodule Antd.Client do end @doc "Like `file_upload_public/2` but raises on error." - @spec file_upload_public!(t(), String.t()) :: Antd.PutResult.t() - def file_upload_public!(client, path), do: unwrap!(file_upload_public(client, path)) + @spec file_upload_public!(t(), String.t(), keyword()) :: Antd.PutResult.t() + def file_upload_public!(client, path, opts \\ []), do: unwrap!(file_upload_public(client, path, opts)) @doc "Downloads a file from the network to a local path." @spec file_download_public(t(), String.t(), String.t()) :: :ok | {:error, Exception.t()} @@ -322,9 +337,14 @@ defmodule Antd.Client do end @doc "Uploads a local directory to the network." - @spec dir_upload_public(t(), String.t()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} - def dir_upload_public(%__MODULE__{} = client, path) do - case do_json(client, :post, "/v1/dirs/upload/public", %{path: path}) do + @spec dir_upload_public(t(), String.t(), keyword()) :: {:ok, Antd.PutResult.t()} | {:error, Exception.t()} + def dir_upload_public(%__MODULE__{} = client, path, opts \\ []) do + payload = %{path: path} + payload = case Keyword.get(opts, :payment_mode) do + nil -> payload + mode -> Map.put(payload, :payment_mode, mode) + end + case do_json(client, :post, "/v1/dirs/upload/public", payload) do {:ok, body} -> {:ok, %Antd.PutResult{cost: body["cost"], address: body["address"]}} @@ -334,8 +354,8 @@ defmodule Antd.Client do end @doc "Like `dir_upload_public/2` but raises on error." - @spec dir_upload_public!(t(), String.t()) :: Antd.PutResult.t() - def dir_upload_public!(client, path), do: unwrap!(dir_upload_public(client, path)) + @spec dir_upload_public!(t(), String.t(), keyword()) :: Antd.PutResult.t() + def dir_upload_public!(client, path, opts \\ []), do: unwrap!(dir_upload_public(client, path, opts)) @doc "Downloads a directory from the network to a local path." @spec dir_download_public(t(), String.t(), String.t()) :: :ok | {:error, Exception.t()} diff --git a/antd-go/client.go b/antd-go/client.go index d425f2a..69c0aa8 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -192,13 +192,29 @@ func (c *Client) Health(ctx context.Context) (*HealthStatus, error) { }, nil } +// PaymentMode controls how payments are made for storage operations. +type PaymentMode string + +const ( + // PaymentModeAuto lets the server choose the best payment strategy. + PaymentModeAuto PaymentMode = "auto" + // PaymentModeMerkle uses Merkle-based batch payments. + PaymentModeMerkle PaymentMode = "merkle" + // PaymentModeSingle uses individual payment per chunk. + PaymentModeSingle PaymentMode = "single" +) + // --- Data --- // DataPutPublic stores public immutable data on the network. -func (c *Client) DataPutPublic(ctx context.Context, data []byte) (*PutResult, error) { - j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/data/public", map[string]any{ +func (c *Client) DataPutPublic(ctx context.Context, data []byte, paymentMode ...PaymentMode) (*PutResult, error) { + body := map[string]any{ "data": b64Encode(data), - }) + } + if len(paymentMode) > 0 && paymentMode[0] != "" { + body["payment_mode"] = string(paymentMode[0]) + } + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/data/public", body) if err != nil { return nil, err } @@ -215,10 +231,14 @@ func (c *Client) DataGetPublic(ctx context.Context, address string) ([]byte, err } // DataPutPrivate stores private encrypted data on the network. -func (c *Client) DataPutPrivate(ctx context.Context, data []byte) (*PutResult, error) { - j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/data/private", map[string]any{ +func (c *Client) DataPutPrivate(ctx context.Context, data []byte, paymentMode ...PaymentMode) (*PutResult, error) { + body := map[string]any{ "data": b64Encode(data), - }) + } + if len(paymentMode) > 0 && paymentMode[0] != "" { + body["payment_mode"] = string(paymentMode[0]) + } + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/data/private", body) if err != nil { return nil, err } @@ -336,10 +356,14 @@ func (c *Client) GraphEntryCost(ctx context.Context, publicKey string) (string, // --- Files --- // FileUploadPublic uploads a local file to the network. -func (c *Client) FileUploadPublic(ctx context.Context, path string) (*PutResult, error) { - j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/files/upload/public", map[string]any{ +func (c *Client) FileUploadPublic(ctx context.Context, path string, paymentMode ...PaymentMode) (*PutResult, error) { + body := map[string]any{ "path": path, - }) + } + if len(paymentMode) > 0 && paymentMode[0] != "" { + body["payment_mode"] = string(paymentMode[0]) + } + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/files/upload/public", body) if err != nil { return nil, err } @@ -356,10 +380,14 @@ func (c *Client) FileDownloadPublic(ctx context.Context, address, destPath strin } // DirUploadPublic uploads a local directory to the network. -func (c *Client) DirUploadPublic(ctx context.Context, path string) (*PutResult, error) { - j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/dirs/upload/public", map[string]any{ +func (c *Client) DirUploadPublic(ctx context.Context, path string, paymentMode ...PaymentMode) (*PutResult, error) { + body := map[string]any{ "path": path, - }) + } + if len(paymentMode) > 0 && paymentMode[0] != "" { + body["payment_mode"] = string(paymentMode[0]) + } + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/dirs/upload/public", body) if err != nil { return nil, err } diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index de41c69..aa5dc53 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -198,7 +198,13 @@ public HealthStatus health() { // ── Data (Immutable) ── public PutResult dataPutPublic(byte[] data) { - String body = Json.object("data", b64Encode(data)); + return dataPutPublic(data, null); + } + + public PutResult dataPutPublic(byte[] data, String paymentMode) { + String body = paymentMode != null + ? Json.object("data", b64Encode(data), "payment_mode", paymentMode) + : Json.object("data", b64Encode(data)); Map j = doJson("POST", "/v1/data/public", body); return new PutResult(str(j, "cost"), str(j, "address")); } @@ -209,7 +215,13 @@ public byte[] dataGetPublic(String address) { } public PutResult dataPutPrivate(byte[] data) { - String body = Json.object("data", b64Encode(data)); + return dataPutPrivate(data, null); + } + + public PutResult dataPutPrivate(byte[] data, String paymentMode) { + String body = paymentMode != null + ? Json.object("data", b64Encode(data), "payment_mode", paymentMode) + : Json.object("data", b64Encode(data)); Map j = doJson("POST", "/v1/data/private", body); return new PutResult(str(j, "cost"), str(j, "data_map")); } @@ -283,7 +295,13 @@ public String graphEntryCost(String publicKey) { // ── Files & Directories ── public PutResult fileUploadPublic(String path) { - String body = Json.object("path", path); + return fileUploadPublic(path, null); + } + + public PutResult fileUploadPublic(String path, String paymentMode) { + String body = paymentMode != null + ? Json.object("path", path, "payment_mode", paymentMode) + : Json.object("path", path); Map j = doJson("POST", "/v1/files/upload/public", body); return new PutResult(str(j, "cost"), str(j, "address")); } @@ -294,7 +312,13 @@ public void fileDownloadPublic(String address, String destPath) { } public PutResult dirUploadPublic(String path) { - String body = Json.object("path", path); + return dirUploadPublic(path, null); + } + + public PutResult dirUploadPublic(String path, String paymentMode) { + String body = paymentMode != null + ? Json.object("path", path, "payment_mode", paymentMode) + : Json.object("path", path); Map j = doJson("POST", "/v1/dirs/upload/public", body); return new PutResult(str(j, "cost"), str(j, "address")); } diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index 9006cf7..60e82d1 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -128,10 +128,10 @@ export class RestClient { // ---- Data ---- - async dataPutPublic(data: Buffer): Promise { - const j = await this.postJson<{ cost: string; address: string }>("/v1/data/public", { - data: RestClient.b64(data), - }); + async dataPutPublic(data: Buffer, options?: { paymentMode?: string }): Promise { + const body: Record = { data: RestClient.b64(data) }; + if (options?.paymentMode) body.payment_mode = options.paymentMode; + const j = await this.postJson<{ cost: string; address: string }>("/v1/data/public", body); return { cost: j.cost, address: j.address }; } @@ -140,10 +140,10 @@ export class RestClient { return RestClient.unb64(j.data); } - async dataPutPrivate(data: Buffer): Promise { - const j = await this.postJson<{ cost: string; data_map: string }>("/v1/data/private", { - data: RestClient.b64(data), - }); + async dataPutPrivate(data: Buffer, options?: { paymentMode?: string }): Promise { + const body: Record = { data: RestClient.b64(data) }; + if (options?.paymentMode) body.payment_mode = options.paymentMode; + const j = await this.postJson<{ cost: string; data_map: string }>("/v1/data/private", body); return { cost: j.cost, address: j.data_map }; } @@ -224,10 +224,10 @@ export class RestClient { // ---- Files ---- - async fileUploadPublic(path: string): Promise { - const j = await this.postJson<{ cost: string; address: string }>("/v1/files/upload/public", { - path, - }); + async fileUploadPublic(path: string, options?: { paymentMode?: string }): Promise { + const body: Record = { path }; + if (options?.paymentMode) body.payment_mode = options.paymentMode; + const j = await this.postJson<{ cost: string; address: string }>("/v1/files/upload/public", body); return { cost: j.cost, address: j.address }; } @@ -238,10 +238,10 @@ export class RestClient { }); } - async dirUploadPublic(path: string): Promise { - const j = await this.postJson<{ cost: string; address: string }>("/v1/dirs/upload/public", { - path, - }); + async dirUploadPublic(path: string, options?: { paymentMode?: string }): Promise { + const body: Record = { path }; + if (options?.paymentMode) body.payment_mode = options.paymentMode; + const j = await this.postJson<{ cost: string; address: string }>("/v1/dirs/upload/public", body); return { cost: j.cost, address: j.address }; } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt index 9904687..3219427 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt @@ -49,7 +49,7 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { // ── Data ── - override suspend fun dataPutPublic(data: ByteArray): PutResult = try { + override suspend fun dataPutPublic(data: ByteArray, paymentMode: String?): PutResult = try { val resp = dataStub.putPublic(putPublicDataRequest { this.data = ByteString.copyFrom(data) }) PutResult(resp.cost.attoTokens, resp.address) } catch (ex: StatusRuntimeException) { throw wrap(ex) } @@ -59,7 +59,7 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { resp.data.toByteArray() } catch (ex: StatusRuntimeException) { throw wrap(ex) } - override suspend fun dataPutPrivate(data: ByteArray): PutResult = try { + override suspend fun dataPutPrivate(data: ByteArray, paymentMode: String?): PutResult = try { val resp = dataStub.putPrivate(putPrivateDataRequest { this.data = ByteString.copyFrom(data) }) PutResult(resp.cost.attoTokens, resp.dataMap) } catch (ex: StatusRuntimeException) { throw wrap(ex) } @@ -125,7 +125,7 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { // ── Files ── - override suspend fun fileUploadPublic(path: String): PutResult = try { + override suspend fun fileUploadPublic(path: String, paymentMode: String?): PutResult = try { val resp = fileStub.uploadPublic(uploadFileRequest { this.path = path }) PutResult(resp.cost.attoTokens, resp.address) } catch (ex: StatusRuntimeException) { throw wrap(ex) } @@ -135,7 +135,7 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { Unit } catch (ex: StatusRuntimeException) { throw wrap(ex) } - override suspend fun dirUploadPublic(path: String): PutResult = try { + override suspend fun dirUploadPublic(path: String, paymentMode: String?): PutResult = try { val resp = fileStub.dirUploadPublic(uploadFileRequest { this.path = path }) PutResult(resp.cost.attoTokens, resp.address) } catch (ex: StatusRuntimeException) { throw wrap(ex) } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index afa1039..c151693 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -103,8 +103,11 @@ class AntdRestClient( // ── Data ── - override suspend fun dataPutPublic(data: ByteArray): PutResult { - val body = buildJsonObject { put("data", b64(data)) }.toString() + override suspend fun dataPutPublic(data: ByteArray, paymentMode: String?): PutResult { + val body = buildJsonObject { + put("data", b64(data)) + if (paymentMode != null) put("payment_mode", paymentMode) + }.toString() val resp = postJson("/v1/data/public", body) return PutResult(resp.cost, resp.address) } @@ -114,8 +117,11 @@ class AntdRestClient( return fromB64(resp.data) } - override suspend fun dataPutPrivate(data: ByteArray): PutResult { - val body = buildJsonObject { put("data", b64(data)) }.toString() + override suspend fun dataPutPrivate(data: ByteArray, paymentMode: String?): PutResult { + val body = buildJsonObject { + put("data", b64(data)) + if (paymentMode != null) put("payment_mode", paymentMode) + }.toString() val resp = postJson("/v1/data/private", body) return PutResult(resp.cost, resp.dataMap) } @@ -185,8 +191,11 @@ class AntdRestClient( // ── Files ── - override suspend fun fileUploadPublic(path: String): PutResult { - val body = buildJsonObject { put("path", path) }.toString() + override suspend fun fileUploadPublic(path: String, paymentMode: String?): PutResult { + val body = buildJsonObject { + put("path", path) + if (paymentMode != null) put("payment_mode", paymentMode) + }.toString() val resp = postJson("/v1/files/upload/public", body) return PutResult(resp.cost, resp.address) } @@ -199,8 +208,11 @@ class AntdRestClient( postJsonNoResult("/v1/files/download/public", body) } - override suspend fun dirUploadPublic(path: String): PutResult { - val body = buildJsonObject { put("path", path) }.toString() + override suspend fun dirUploadPublic(path: String, paymentMode: String?): PutResult { + val body = buildJsonObject { + put("path", path) + if (paymentMode != null) put("payment_mode", paymentMode) + }.toString() val resp = postJson("/v1/dirs/upload/public", body) return PutResult(resp.cost, resp.address) } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt index 8a22f90..e2d50f3 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt @@ -14,9 +14,9 @@ interface IAntdClient : Closeable { suspend fun health(): HealthStatus // Data - suspend fun dataPutPublic(data: ByteArray): PutResult + suspend fun dataPutPublic(data: ByteArray, paymentMode: String? = null): PutResult suspend fun dataGetPublic(address: String): ByteArray - suspend fun dataPutPrivate(data: ByteArray): PutResult + suspend fun dataPutPrivate(data: ByteArray, paymentMode: String? = null): PutResult suspend fun dataGetPrivate(dataMap: String): ByteArray suspend fun dataCost(data: ByteArray): String @@ -31,9 +31,9 @@ interface IAntdClient : Closeable { suspend fun graphEntryCost(publicKey: String): String // Files - suspend fun fileUploadPublic(path: String): PutResult + suspend fun fileUploadPublic(path: String, paymentMode: String? = null): PutResult suspend fun fileDownloadPublic(address: String, destPath: String) - suspend fun dirUploadPublic(path: String): PutResult + suspend fun dirUploadPublic(path: String, paymentMode: String? = null): PutResult suspend fun dirDownloadPublic(address: String, destPath: String) suspend fun archiveGetPublic(address: String): Archive suspend fun archivePutPublic(archive: Archive): PutResult diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index bb0eb6a..da4644b 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -147,11 +147,16 @@ end --- Store public immutable data. -- @param data string raw bytes to store +-- @param opts table optional settings: { payment_mode = string } -- @return PutResult|nil, error|nil -function Client:data_put_public(data) - local j, _, err = self:_do_json("POST", "/v1/data/public", { +function Client:data_put_public(data, opts) + local body = { data = base64.encode(data), - }) + } + if opts and opts.payment_mode then + body.payment_mode = opts.payment_mode + end + local j, _, err = self:_do_json("POST", "/v1/data/public", body) if err then return nil, err end return models.new_put_result(str(j, "cost"), str(j, "address")), nil end @@ -167,11 +172,16 @@ end --- Store private encrypted data. -- @param data string raw bytes to store +-- @param opts table optional settings: { payment_mode = string } -- @return PutResult|nil, error|nil -function Client:data_put_private(data) - local j, _, err = self:_do_json("POST", "/v1/data/private", { +function Client:data_put_private(data, opts) + local body = { data = base64.encode(data), - }) + } + if opts and opts.payment_mode then + body.payment_mode = opts.payment_mode + end + local j, _, err = self:_do_json("POST", "/v1/data/private", body) if err then return nil, err end return models.new_put_result(str(j, "cost"), str(j, "data_map")), nil end @@ -297,11 +307,16 @@ end --- Upload a file to the network. -- @param path string local file path +-- @param opts table optional settings: { payment_mode = string } -- @return PutResult|nil, error|nil -function Client:file_upload_public(path) - local j, _, err = self:_do_json("POST", "/v1/files/upload/public", { +function Client:file_upload_public(path, opts) + local body = { path = path, - }) + } + if opts and opts.payment_mode then + body.payment_mode = opts.payment_mode + end + local j, _, err = self:_do_json("POST", "/v1/files/upload/public", body) if err then return nil, err end return models.new_put_result(str(j, "cost"), str(j, "address")), nil end @@ -320,11 +335,16 @@ end --- Upload a directory to the network. -- @param path string local directory path +-- @param opts table optional settings: { payment_mode = string } -- @return PutResult|nil, error|nil -function Client:dir_upload_public(path) - local j, _, err = self:_do_json("POST", "/v1/dirs/upload/public", { +function Client:dir_upload_public(path, opts) + local body = { path = path, - }) + } + if opts and opts.payment_mode then + body.payment_mode = opts.payment_mode + end + local j, _, err = self:_do_json("POST", "/v1/dirs/upload/public", body) if err then return nil, err end return models.new_put_result(str(j, "cost"), str(j, "address")), nil end diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index e70776d..65e795d 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -211,11 +211,13 @@ public function healthAsync(): PromiseInterface /** * Store public immutable data on the network. */ - public function dataPutPublic(string $data): PutResult + public function dataPutPublic(string $data, ?string $paymentMode = null): PutResult { - $json = $this->doJson('POST', '/v1/data/public', [ - 'data' => $this->b64Encode($data), - ]); + $body = ['data' => $this->b64Encode($data)]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + $json = $this->doJson('POST', '/v1/data/public', $body); return new PutResult( cost: $json['cost'] ?? '', address: $json['address'] ?? '', @@ -227,11 +229,13 @@ public function dataPutPublic(string $data): PutResult * * @return PromiseInterface */ - public function dataPutPublicAsync(string $data): PromiseInterface + public function dataPutPublicAsync(string $data, ?string $paymentMode = null): PromiseInterface { - return $this->doJsonAsync('POST', '/v1/data/public', [ - 'data' => $this->b64Encode($data), - ])->then( + $body = ['data' => $this->b64Encode($data)]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + return $this->doJsonAsync('POST', '/v1/data/public', $body)->then( fn(?array $json) => new PutResult( cost: $json['cost'] ?? '', address: $json['address'] ?? '', @@ -263,11 +267,13 @@ public function dataGetPublicAsync(string $address): PromiseInterface /** * Store private encrypted data on the network. */ - public function dataPutPrivate(string $data): PutResult + public function dataPutPrivate(string $data, ?string $paymentMode = null): PutResult { - $json = $this->doJson('POST', '/v1/data/private', [ - 'data' => $this->b64Encode($data), - ]); + $body = ['data' => $this->b64Encode($data)]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + $json = $this->doJson('POST', '/v1/data/private', $body); return new PutResult( cost: $json['cost'] ?? '', address: $json['data_map'] ?? '', @@ -279,11 +285,13 @@ public function dataPutPrivate(string $data): PutResult * * @return PromiseInterface */ - public function dataPutPrivateAsync(string $data): PromiseInterface + public function dataPutPrivateAsync(string $data, ?string $paymentMode = null): PromiseInterface { - return $this->doJsonAsync('POST', '/v1/data/private', [ - 'data' => $this->b64Encode($data), - ])->then( + $body = ['data' => $this->b64Encode($data)]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + return $this->doJsonAsync('POST', '/v1/data/private', $body)->then( fn(?array $json) => new PutResult( cost: $json['cost'] ?? '', address: $json['data_map'] ?? '', @@ -567,11 +575,13 @@ public function graphEntryCostAsync(string $publicKey): PromiseInterface /** * Upload a local file to the network. */ - public function fileUploadPublic(string $path): PutResult + public function fileUploadPublic(string $path, ?string $paymentMode = null): PutResult { - $json = $this->doJson('POST', '/v1/files/upload/public', [ - 'path' => $path, - ]); + $body = ['path' => $path]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + $json = $this->doJson('POST', '/v1/files/upload/public', $body); return new PutResult( cost: $json['cost'] ?? '', address: $json['address'] ?? '', @@ -583,11 +593,13 @@ public function fileUploadPublic(string $path): PutResult * * @return PromiseInterface */ - public function fileUploadPublicAsync(string $path): PromiseInterface + public function fileUploadPublicAsync(string $path, ?string $paymentMode = null): PromiseInterface { - return $this->doJsonAsync('POST', '/v1/files/upload/public', [ - 'path' => $path, - ])->then( + $body = ['path' => $path]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + return $this->doJsonAsync('POST', '/v1/files/upload/public', $body)->then( fn(?array $json) => new PutResult( cost: $json['cost'] ?? '', address: $json['address'] ?? '', @@ -622,11 +634,13 @@ public function fileDownloadPublicAsync(string $address, string $destPath): Prom /** * Upload a local directory to the network. */ - public function dirUploadPublic(string $path): PutResult + public function dirUploadPublic(string $path, ?string $paymentMode = null): PutResult { - $json = $this->doJson('POST', '/v1/dirs/upload/public', [ - 'path' => $path, - ]); + $body = ['path' => $path]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + $json = $this->doJson('POST', '/v1/dirs/upload/public', $body); return new PutResult( cost: $json['cost'] ?? '', address: $json['address'] ?? '', @@ -638,11 +652,13 @@ public function dirUploadPublic(string $path): PutResult * * @return PromiseInterface */ - public function dirUploadPublicAsync(string $path): PromiseInterface + public function dirUploadPublicAsync(string $path, ?string $paymentMode = null): PromiseInterface { - return $this->doJsonAsync('POST', '/v1/dirs/upload/public', [ - 'path' => $path, - ])->then( + $body = ['path' => $path]; + if ($paymentMode !== null) { + $body['payment_mode'] = $paymentMode; + } + return $this->doJsonAsync('POST', '/v1/dirs/upload/public', $body)->then( fn(?array $json) => new PutResult( cost: $json['cost'] ?? '', address: $json['address'] ?? '', diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 19f64f7..422022d 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -83,8 +83,11 @@ def health(self) -> HealthStatus: # --- Data --- - def data_put_public(self, data: bytes) -> PutResult: - resp = self._http.post("/v1/data/public", json={"data": _b64(data)}) + def data_put_public(self, data: bytes, payment_mode: str | None = None) -> PutResult: + body: dict = {"data": _b64(data)} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = self._http.post("/v1/data/public", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["address"]) @@ -94,8 +97,11 @@ def data_get_public(self, address: str) -> bytes: _check(resp) return _unb64(resp.json()["data"]) - def data_put_private(self, data: bytes) -> PutResult: - resp = self._http.post("/v1/data/private", json={"data": _b64(data)}) + def data_put_private(self, data: bytes, payment_mode: str | None = None) -> PutResult: + body: dict = {"data": _b64(data)} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = self._http.post("/v1/data/private", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["data_map"]) @@ -163,8 +169,11 @@ def graph_entry_cost(self, public_key: str) -> str: # --- Files --- - def file_upload_public(self, path: str) -> PutResult: - resp = self._http.post("/v1/files/upload/public", json={"path": path}) + def file_upload_public(self, path: str, payment_mode: str | None = None) -> PutResult: + body: dict = {"path": path} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = self._http.post("/v1/files/upload/public", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["address"]) @@ -176,8 +185,11 @@ def file_download_public(self, address: str, dest_path: str) -> None: }) _check(resp) - def dir_upload_public(self, path: str) -> PutResult: - resp = self._http.post("/v1/dirs/upload/public", json={"path": path}) + def dir_upload_public(self, path: str, payment_mode: str | None = None) -> PutResult: + body: dict = {"path": path} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = self._http.post("/v1/dirs/upload/public", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["address"]) @@ -279,8 +291,11 @@ async def health(self) -> HealthStatus: # --- Data --- - async def data_put_public(self, data: bytes) -> PutResult: - resp = await self._http.post("/v1/data/public", json={"data": _b64(data)}) + async def data_put_public(self, data: bytes, payment_mode: str | None = None) -> PutResult: + body: dict = {"data": _b64(data)} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = await self._http.post("/v1/data/public", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["address"]) @@ -290,8 +305,11 @@ async def data_get_public(self, address: str) -> bytes: _check(resp) return _unb64(resp.json()["data"]) - async def data_put_private(self, data: bytes) -> PutResult: - resp = await self._http.post("/v1/data/private", json={"data": _b64(data)}) + async def data_put_private(self, data: bytes, payment_mode: str | None = None) -> PutResult: + body: dict = {"data": _b64(data)} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = await self._http.post("/v1/data/private", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["data_map"]) @@ -359,8 +377,11 @@ async def graph_entry_cost(self, public_key: str) -> str: # --- Files --- - async def file_upload_public(self, path: str) -> PutResult: - resp = await self._http.post("/v1/files/upload/public", json={"path": path}) + async def file_upload_public(self, path: str, payment_mode: str | None = None) -> PutResult: + body: dict = {"path": path} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = await self._http.post("/v1/files/upload/public", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["address"]) @@ -372,8 +393,11 @@ async def file_download_public(self, address: str, dest_path: str) -> None: }) _check(resp) - async def dir_upload_public(self, path: str) -> PutResult: - resp = await self._http.post("/v1/dirs/upload/public", json={"path": path}) + async def dir_upload_public(self, path: str, payment_mode: str | None = None) -> PutResult: + body: dict = {"path": path} + if payment_mode is not None: + body["payment_mode"] = payment_mode + resp = await self._http.post("/v1/dirs/upload/public", json=body) _check(resp) j = resp.json() return PutResult(cost=j["cost"], address=j["address"]) diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index b10141a..d65bdd3 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -45,8 +45,10 @@ def health # Store public immutable data on the network. # @param data [String] raw bytes # @return [PutResult] - def data_put_public(data) - j = do_json(:post, "/v1/data/public", { data: b64_encode(data) }) + def data_put_public(data, payment_mode: nil) + body = { data: b64_encode(data) } + body[:payment_mode] = payment_mode if payment_mode + j = do_json(:post, "/v1/data/public", body) PutResult.new(cost: j["cost"], address: j["address"]) end @@ -61,8 +63,10 @@ def data_get_public(address) # Store private encrypted data on the network. # @param data [String] raw bytes # @return [PutResult] - def data_put_private(data) - j = do_json(:post, "/v1/data/private", { data: b64_encode(data) }) + def data_put_private(data, payment_mode: nil) + body = { data: b64_encode(data) } + body[:payment_mode] = payment_mode if payment_mode + j = do_json(:post, "/v1/data/private", body) PutResult.new(cost: j["cost"], address: j["data_map"]) end @@ -159,8 +163,10 @@ def graph_entry_cost(public_key) # Upload a local file to the network. # @param path [String] local file path # @return [PutResult] - def file_upload_public(path) - j = do_json(:post, "/v1/files/upload/public", { path: path }) + def file_upload_public(path, payment_mode: nil) + body = { path: path } + body[:payment_mode] = payment_mode if payment_mode + j = do_json(:post, "/v1/files/upload/public", body) PutResult.new(cost: j["cost"], address: j["address"]) end @@ -176,8 +182,10 @@ def file_download_public(address, dest_path) # Upload a local directory to the network. # @param path [String] local directory path # @return [PutResult] - def dir_upload_public(path) - j = do_json(:post, "/v1/dirs/upload/public", { path: path }) + def dir_upload_public(path, payment_mode: nil) + body = { path: path } + body[:payment_mode] = payment_mode if payment_mode + j = do_json(:post, "/v1/dirs/upload/public", body) PutResult.new(cost: j["cost"], address: j["address"]) end diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index e5b316d..81f73c6 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -162,12 +162,16 @@ impl Client { // --- Data --- /// Stores public immutable data on the network. - pub async fn data_put_public(&self, data: &[u8]) -> Result { + pub async fn data_put_public(&self, data: &[u8], payment_mode: Option<&str>) -> Result { + let mut body = json!({ "data": Self::b64_encode(data) }); + if let Some(mode) = payment_mode { + body["payment_mode"] = json!(mode); + } let (j, _) = self .do_json( reqwest::Method::POST, "/v1/data/public", - Some(json!({ "data": Self::b64_encode(data) })), + Some(body), ) .await?; let j = j.unwrap_or_default(); @@ -191,12 +195,16 @@ impl Client { } /// Stores private encrypted data on the network. - pub async fn data_put_private(&self, data: &[u8]) -> Result { + pub async fn data_put_private(&self, data: &[u8], payment_mode: Option<&str>) -> Result { + let mut body = json!({ "data": Self::b64_encode(data) }); + if let Some(mode) = payment_mode { + body["payment_mode"] = json!(mode); + } let (j, _) = self .do_json( reqwest::Method::POST, "/v1/data/private", - Some(json!({ "data": Self::b64_encode(data) })), + Some(body), ) .await?; let j = j.unwrap_or_default(); @@ -367,12 +375,16 @@ impl Client { // --- Files --- /// Uploads a local file to the network. - pub async fn file_upload_public(&self, path: &str) -> Result { + pub async fn file_upload_public(&self, path: &str, payment_mode: Option<&str>) -> Result { + let mut body = json!({ "path": path }); + if let Some(mode) = payment_mode { + body["payment_mode"] = json!(mode); + } let (j, _) = self .do_json( reqwest::Method::POST, "/v1/files/upload/public", - Some(json!({ "path": path })), + Some(body), ) .await?; let j = j.unwrap_or_default(); @@ -398,12 +410,16 @@ impl Client { } /// Uploads a local directory to the network. - pub async fn dir_upload_public(&self, path: &str) -> Result { + pub async fn dir_upload_public(&self, path: &str, payment_mode: Option<&str>) -> Result { + let mut body = json!({ "path": path }); + if let Some(mode) = payment_mode { + body["payment_mode"] = json!(mode); + } let (j, _) = self .do_json( reqwest::Method::POST, "/v1/dirs/upload/public", - Some(json!({ "path": path })), + Some(body), ) .await?; let j = j.unwrap_or_default(); diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index bf50e1e..1122d89 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -75,8 +75,10 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { // MARK: - Data - public func dataPutPublic(_ data: Data) async throws -> PutResult { - let resp: CostAddressDTO = try await postJSON("/v1/data/public", body: ["data": data.base64EncodedString()]) + public func dataPutPublic(_ data: Data, paymentMode: String? = nil) async throws -> PutResult { + var body: [String: Any] = ["data": data.base64EncodedString()] + if let mode = paymentMode { body["payment_mode"] = mode } + let resp: CostAddressDTO = try await postJSON("/v1/data/public", body: body) return PutResult(cost: resp.cost, address: resp.address) } @@ -86,8 +88,10 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { return decoded } - public func dataPutPrivate(_ data: Data) async throws -> PutResult { - let resp: CostDataMapDTO = try await postJSON("/v1/data/private", body: ["data": data.base64EncodedString()]) + public func dataPutPrivate(_ data: Data, paymentMode: String? = nil) async throws -> PutResult { + var body: [String: Any] = ["data": data.base64EncodedString()] + if let mode = paymentMode { body["payment_mode"] = mode } + let resp: CostDataMapDTO = try await postJSON("/v1/data/private", body: body) return PutResult(cost: resp.cost, address: resp.dataMap) } @@ -145,8 +149,10 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { // MARK: - Files - public func fileUploadPublic(path: String) async throws -> PutResult { - let resp: CostAddressDTO = try await postJSON("/v1/files/upload/public", body: ["path": path]) + public func fileUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { + var body: [String: Any] = ["path": path] + if let mode = paymentMode { body["payment_mode"] = mode } + let resp: CostAddressDTO = try await postJSON("/v1/files/upload/public", body: body) return PutResult(cost: resp.cost, address: resp.address) } @@ -154,8 +160,10 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { try await postJSONNoResult("/v1/files/download/public", body: ["address": address, "dest_path": destPath]) } - public func dirUploadPublic(path: String) async throws -> PutResult { - let resp: CostAddressDTO = try await postJSON("/v1/dirs/upload/public", body: ["path": path]) + public func dirUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { + var body: [String: Any] = ["path": path] + if let mode = paymentMode { body["payment_mode"] = mode } + let resp: CostAddressDTO = try await postJSON("/v1/dirs/upload/public", body: body) return PutResult(cost: resp.cost, address: resp.address) } diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index 4151593..09a88cc 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -180,8 +180,11 @@ pub const Client = struct { // --- Data --- /// Store public immutable data on the network. - pub fn dataPutPublic(self: *Client, data: []const u8) !PutResult { - const req_body = try json_helpers.buildDataBody(self.allocator, data); + pub fn dataPutPublic(self: *Client, data: []const u8, payment_mode: ?[]const u8) !PutResult { + const req_body = if (payment_mode) |mode| + try json_helpers.buildDataBodyWithPaymentMode(self.allocator, data, mode) + else + try json_helpers.buildDataBody(self.allocator, data); defer self.allocator.free(req_body); const resp = try self.doRequest(.POST, "/v1/data/public", req_body) orelse return error.JsonError; defer self.allocator.free(resp); @@ -198,8 +201,11 @@ pub const Client = struct { } /// Store private encrypted data on the network. - pub fn dataPutPrivate(self: *Client, data: []const u8) !PutResult { - const req_body = try json_helpers.buildDataBody(self.allocator, data); + pub fn dataPutPrivate(self: *Client, data: []const u8, payment_mode: ?[]const u8) !PutResult { + const req_body = if (payment_mode) |mode| + try json_helpers.buildDataBodyWithPaymentMode(self.allocator, data, mode) + else + try json_helpers.buildDataBody(self.allocator, data); defer self.allocator.free(req_body); const resp = try self.doRequest(.POST, "/v1/data/private", req_body) orelse return error.JsonError; defer self.allocator.free(resp); @@ -316,10 +322,16 @@ pub const Client = struct { // --- Files --- /// Upload a local file to the network. - pub fn fileUploadPublic(self: *Client, path: []const u8) !PutResult { - const req_body = try json_helpers.buildJsonBody(self.allocator, &.{ - .{ .key = "path", .value = .{ .string = path } }, - }); + pub fn fileUploadPublic(self: *Client, path: []const u8, payment_mode: ?[]const u8) !PutResult { + const req_body = if (payment_mode) |mode| + try json_helpers.buildJsonBody(self.allocator, &.{ + .{ .key = "path", .value = .{ .string = path } }, + .{ .key = "payment_mode", .value = .{ .string = mode } }, + }) + else + try json_helpers.buildJsonBody(self.allocator, &.{ + .{ .key = "path", .value = .{ .string = path } }, + }); defer self.allocator.free(req_body); const resp = try self.doRequest(.POST, "/v1/files/upload/public", req_body) orelse return error.JsonError; defer self.allocator.free(resp); @@ -337,10 +349,16 @@ pub const Client = struct { } /// Upload a local directory to the network. - pub fn dirUploadPublic(self: *Client, path: []const u8) !PutResult { - const req_body = try json_helpers.buildJsonBody(self.allocator, &.{ - .{ .key = "path", .value = .{ .string = path } }, - }); + pub fn dirUploadPublic(self: *Client, path: []const u8, payment_mode: ?[]const u8) !PutResult { + const req_body = if (payment_mode) |mode| + try json_helpers.buildJsonBody(self.allocator, &.{ + .{ .key = "path", .value = .{ .string = path } }, + .{ .key = "payment_mode", .value = .{ .string = mode } }, + }) + else + try json_helpers.buildJsonBody(self.allocator, &.{ + .{ .key = "path", .value = .{ .string = path } }, + }); defer self.allocator.free(req_body); const resp = try self.doRequest(.POST, "/v1/dirs/upload/public", req_body) orelse return error.JsonError; defer self.allocator.free(resp); diff --git a/antd-zig/src/json_helpers.zig b/antd-zig/src/json_helpers.zig index 7bfa235..0ef1a69 100644 --- a/antd-zig/src/json_helpers.zig +++ b/antd-zig/src/json_helpers.zig @@ -290,6 +290,22 @@ pub fn buildDataBody(allocator: Allocator, data: []const u8) ![]const u8 { return std.fmt.allocPrint(allocator, "{{\"data\":{s}}}", .{escaped}) catch return error.JsonError; } +/// Build a JSON body for a data upload with an optional payment_mode field. +pub fn buildDataBodyWithPaymentMode(allocator: Allocator, data: []const u8, payment_mode: []const u8) ![]const u8 { + const encoded_len = std.base64.standard.Encoder.calcSize(data.len); + const encoded = allocator.alloc(u8, encoded_len) catch return error.JsonError; + defer allocator.free(encoded); + _ = std.base64.standard.Encoder.encode(encoded, data); + + const escaped_data = jsonEscapeString(allocator, encoded) catch return error.JsonError; + defer allocator.free(escaped_data); + + const escaped_mode = jsonEscapeString(allocator, payment_mode) catch return error.JsonError; + defer allocator.free(escaped_mode); + + return std.fmt.allocPrint(allocator, "{{\"data\":{s},\"payment_mode\":{s}}}", .{ escaped_data, escaped_mode }) catch return error.JsonError; +} + /// Values supported in JSON body construction. pub const JsonValue = union(enum) { string: []const u8, diff --git a/antd/src/main.rs b/antd/src/main.rs index 314d2d0..24e9604 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -161,7 +161,7 @@ async fn main() -> Result<(), Box> { } let state = Arc::new(AppState { - client, + client: Arc::new(client), network: config.network.clone(), bootstrap_peers, }); diff --git a/antd/src/rest/data.rs b/antd/src/rest/data.rs index dd6db95..242fd9a 100644 --- a/antd/src/rest/data.rs +++ b/antd/src/rest/data.rs @@ -3,57 +3,55 @@ use std::sync::Arc; use axum::extract::{Path, Query, State}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::Json; -use base64::Engine; -use base64::engine::general_purpose::STANDARD as BASE64; use crate::error::AntdError; use crate::state::AppState; use crate::types::*; -// TODO: Implement data operations on top of ant-node chunk protocol. -// Data operations require multi-chunk handling (chunking, self-encryption) -// which is not yet available in the ant-node client. - -pub async fn data_get_public( - State(_state): State>, - Path(_addr): Path, -) -> Result, AntdError> { - Err(AntdError::Internal("data operations not yet implemented yet".into())) -} +// Data operations are blocked on an upstream lifetime issue in ant-core's +// stream closures (data_upload_with_mode, data_download). The types and +// payment_mode parameter are in place — implementations will land once +// ant-core is fixed. pub async fn data_put_public( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("data operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("data put public pending ant-core fix".into())) } -pub async fn data_get_private( +pub async fn data_get_public( State(_state): State>, - Query(_query): Query, + Path(_addr): Path, ) -> Result, AntdError> { - Err(AntdError::Internal("private data operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("data get public pending ant-core fix".into())) } pub async fn data_put_private( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("private data operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("data put private pending ant-core fix".into())) +} + +pub async fn data_get_private( + State(_state): State>, + Query(_query): Query, +) -> Result, AntdError> { + Err(AntdError::NotImplemented("data get private pending ant-core fix".into())) } pub async fn data_cost( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("data cost not yet implemented yet".into())) + Err(AntdError::NotImplemented("data cost estimation not yet available".into())) } pub async fn data_stream_public( State(_state): State>, Path(_addr): Path, ) -> Result>>, AntdError> { - // Return an empty stream for now let stream = futures::stream::empty(); Ok(Sse::new(stream).keep_alive(KeepAlive::default())) } diff --git a/antd/src/rest/files.rs b/antd/src/rest/files.rs index 2b8ae9b..c2e4a46 100644 --- a/antd/src/rest/files.rs +++ b/antd/src/rest/files.rs @@ -7,55 +7,54 @@ use crate::error::AntdError; use crate::state::AppState; use crate::types::*; -// TODO: Implement file operations on top of ant-node chunk protocol. -// File operations require chunking, FEC encoding, and archive manifests -// which need to be built on top of the raw chunk layer. +// File operations are blocked on the same ant-core lifetime issue as data ops. +// The payment_mode parameter is in place on FileUploadRequest. pub async fn file_upload_public( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("file operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("file upload pending ant-core fix".into())) } pub async fn file_download_public( State(_state): State>, Json(_req): Json, ) -> Result { - Err(AntdError::Internal("file operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("file download pending ant-core fix".into())) } pub async fn dir_upload_public( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("directory operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("directory upload pending ant-core fix".into())) } pub async fn dir_download_public( State(_state): State>, Json(_req): Json, ) -> Result { - Err(AntdError::Internal("directory operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("directory download pending ant-core fix".into())) } pub async fn archive_get_public( State(_state): State>, axum::extract::Path(_addr): axum::extract::Path, ) -> Result, AntdError> { - Err(AntdError::Internal("archive operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("archive operations not yet available".into())) } pub async fn archive_put_public( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("archive operations not yet implemented yet".into())) + Err(AntdError::NotImplemented("archive operations not yet available".into())) } pub async fn file_cost( State(_state): State>, Json(_req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("file cost not yet implemented yet".into())) + Err(AntdError::NotImplemented("file cost estimation not yet available".into())) } diff --git a/antd/src/state.rs b/antd/src/state.rs index b5fb1c2..e755d4a 100644 --- a/antd/src/state.rs +++ b/antd/src/state.rs @@ -1,9 +1,12 @@ +use std::sync::Arc; + use ant_core::data::{Client, MultiAddr}; /// Shared application state passed to all handlers. +#[derive(Clone)] pub struct AppState { /// High-level Autonomi client (wraps P2P node, wallet, cache). - pub client: Client, + pub client: Arc, /// Network mode label ("local", "default", etc.) pub network: String, /// Bootstrap peer addresses. diff --git a/antd/src/types.rs b/antd/src/types.rs index c40dde6..9471866 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -5,12 +5,16 @@ use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct DataPutRequest { pub data: String, // base64 + /// Payment mode: "auto" (default), "merkle", or "single". + #[serde(default)] + pub payment_mode: Option, } #[derive(Serialize)] pub struct DataPutPublicResponse { - pub cost: String, pub address: String, + pub chunks_stored: usize, + pub payment_mode_used: String, } #[derive(Serialize)] @@ -25,8 +29,9 @@ pub struct DataCostRequest { #[derive(Serialize)] pub struct DataPutPrivateResponse { - pub cost: String, - pub data_map: String, // hex + pub data_map: String, // hex-encoded serialized data map + pub chunks_stored: usize, + pub payment_mode_used: String, } #[derive(Deserialize)] @@ -92,6 +97,9 @@ pub struct GraphEntryCostRequest { #[derive(Deserialize)] pub struct FileUploadRequest { pub path: String, + /// Payment mode: "auto" (default), "merkle", or "single". + #[serde(default)] + pub payment_mode: Option, } #[derive(Serialize)] @@ -167,6 +175,25 @@ fn default_true() -> bool { true } +/// Parse a payment mode string into ant-core's PaymentMode. +pub fn parse_payment_mode(mode: Option<&str>) -> Result { + match mode { + None | Some("auto") => Ok(ant_core::data::PaymentMode::Auto), + Some("merkle") => Ok(ant_core::data::PaymentMode::Merkle), + Some("single") => Ok(ant_core::data::PaymentMode::Single), + Some(other) => Err(format!("invalid payment_mode: {other:?}. Use \"auto\", \"merkle\", or \"single\"")), + } +} + +/// Format a PaymentMode for JSON responses. +pub fn format_payment_mode(mode: ant_core::data::PaymentMode) -> String { + match mode { + ant_core::data::PaymentMode::Auto => "auto".into(), + ant_core::data::PaymentMode::Merkle => "merkle".into(), + ant_core::data::PaymentMode::Single => "single".into(), + } +} + // ── Wallet ── #[derive(Serialize)] From 40827f0e2e01f1c66c1576d1c534bde0cdc495cf Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 26 Mar 2026 13:58:47 +0000 Subject: [PATCH 07/20] Implement data and file endpoints using ant-core Client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the upstream HRTB lifetime fix (ant-client#8), ant-core's Client methods can now be called from async axum handlers via tokio::spawn. Implemented endpoints: - POST /v1/data/public — upload with self-encryption + merkle payments - GET /v1/data/public/{addr} — download via data map - POST /v1/data/private — upload, return data map to caller - GET /v1/data/private — download using caller-provided data map - POST /v1/files/upload/public — stream-encrypt file from disk - POST /v1/files/download/public — download file to disk - POST /v1/dirs/upload/public — upload directory - POST /v1/dirs/download/public — download directory All upload endpoints accept optional payment_mode parameter ("auto"/"merkle"/"single"). Default is "auto" which uses merkle batch payments for 64+ chunks. Still stubbed (501): archives, cost estimation, data streaming. Co-Authored-By: Claude Opus 4.6 (1M context) --- antd/Cargo.lock | 2 +- antd/src/rest/data.rs | 108 ++++++++++++++++++++++++++++++++++------- antd/src/rest/files.rs | 108 +++++++++++++++++++++++++++++++++++------ 3 files changed, 185 insertions(+), 33 deletions(-) diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 6e739c4..f4e304e 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "ant-core" version = "0.1.0" -source = "git+https://github.com/WithAutonomi/ant-client#1a90ca8edd66b22b35526f8575440d1e2f10554a" +source = "git+https://github.com/WithAutonomi/ant-client#c979f145a15c310693bfbb801e1500252af36cad" dependencies = [ "ant-evm", "ant-node", diff --git a/antd/src/rest/data.rs b/antd/src/rest/data.rs index 242fd9a..c88456a 100644 --- a/antd/src/rest/data.rs +++ b/antd/src/rest/data.rs @@ -3,42 +3,116 @@ use std::sync::Arc; use axum::extract::{Path, Query, State}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::Json; +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64; +use bytes::Bytes; use crate::error::AntdError; use crate::state::AppState; use crate::types::*; -// Data operations are blocked on an upstream lifetime issue in ant-core's -// stream closures (data_upload_with_mode, data_download). The types and -// payment_mode parameter are in place — implementations will land once -// ant-core is fixed. - pub async fn data_put_public( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("data put public pending ant-core fix".into())) + if state.client.wallet().is_none() { + return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + } + + let data = BASE64.decode(&req.data) + .map_err(|e| AntdError::BadRequest(format!("invalid base64: {e}")))?; + + let mode = parse_payment_mode(req.payment_mode.as_deref()) + .map_err(AntdError::BadRequest)?; + + let client = state.client.clone(); + let (address, chunks_stored, payment_mode_used) = tokio::spawn(async move { + let result = client.data_upload_with_mode(Bytes::from(data), mode).await + .map_err(AntdError::from_core)?; + let address = client.data_map_store(&result.data_map).await + .map_err(AntdError::from_core)?; + Ok::<_, AntdError>((address, result.chunks_stored, result.payment_mode_used)) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(DataPutPublicResponse { + address: hex::encode(address), + chunks_stored, + payment_mode_used: format_payment_mode(payment_mode_used), + })) } pub async fn data_get_public( - State(_state): State>, - Path(_addr): Path, + State(state): State>, + Path(addr): Path, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("data get public pending ant-core fix".into())) + let address_bytes = hex::decode(&addr) + .map_err(|e| AntdError::BadRequest(format!("invalid hex address: {e}")))?; + let address: [u8; 32] = address_bytes + .try_into() + .map_err(|_| AntdError::BadRequest("address must be 32 bytes".into()))?; + + let client = state.client.clone(); + let content = tokio::spawn(async move { + let data_map = client.data_map_fetch(&address).await + .map_err(AntdError::from_core)?; + client.data_download(&data_map).await + .map_err(AntdError::from_core) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(DataGetResponse { + data: BASE64.encode(&content), + })) } pub async fn data_put_private( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("data put private pending ant-core fix".into())) + if state.client.wallet().is_none() { + return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + } + + let data = BASE64.decode(&req.data) + .map_err(|e| AntdError::BadRequest(format!("invalid base64: {e}")))?; + + let mode = parse_payment_mode(req.payment_mode.as_deref()) + .map_err(AntdError::BadRequest)?; + + let client = state.client.clone(); + let (data_map_hex, chunks_stored, payment_mode_used) = tokio::spawn(async move { + let result = client.data_upload_with_mode(Bytes::from(data), mode).await + .map_err(AntdError::from_core)?; + let data_map_bytes = rmp_serde::to_vec(&result.data_map) + .map_err(|e| AntdError::Internal(format!("failed to serialize data map: {e}")))?; + Ok::<_, AntdError>((hex::encode(data_map_bytes), result.chunks_stored, result.payment_mode_used)) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(DataPutPrivateResponse { + data_map: data_map_hex, + chunks_stored, + payment_mode_used: format_payment_mode(payment_mode_used), + })) } pub async fn data_get_private( - State(_state): State>, - Query(_query): Query, + State(state): State>, + Query(query): Query, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("data get private pending ant-core fix".into())) + let data_map_bytes = hex::decode(&query.data_map) + .map_err(|e| AntdError::BadRequest(format!("invalid hex data_map: {e}")))?; + + let data_map: ant_core::data::DataMap = rmp_serde::from_slice(&data_map_bytes) + .map_err(|e| AntdError::BadRequest(format!("invalid data map: {e}")))?; + + let client = state.client.clone(); + let content = tokio::spawn(async move { + client.data_download(&data_map).await + .map_err(AntdError::from_core) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(DataGetResponse { + data: BASE64.encode(&content), + })) } pub async fn data_cost( diff --git a/antd/src/rest/files.rs b/antd/src/rest/files.rs index c2e4a46..a4a30b9 100644 --- a/antd/src/rest/files.rs +++ b/antd/src/rest/files.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::sync::Arc; use axum::extract::State; @@ -7,35 +8,112 @@ use crate::error::AntdError; use crate::state::AppState; use crate::types::*; -// File operations are blocked on the same ant-core lifetime issue as data ops. -// The payment_mode parameter is in place on FileUploadRequest. - pub async fn file_upload_public( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("file upload pending ant-core fix".into())) + if state.client.wallet().is_none() { + return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + } + + let path = PathBuf::from(&req.path); + if !path.exists() { + return Err(AntdError::BadRequest(format!("file not found: {}", req.path))); + } + + let mode = parse_payment_mode(req.payment_mode.as_deref()) + .map_err(AntdError::BadRequest)?; + + let client = state.client.clone(); + let address = tokio::spawn(async move { + let result = client.file_upload_with_mode(&path, mode).await + .map_err(AntdError::from_core)?; + let address = client.data_map_store(&result.data_map).await + .map_err(AntdError::from_core)?; + Ok::<_, AntdError>(address) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(FileUploadPublicResponse { + cost: String::new(), + address: hex::encode(address), + })) } pub async fn file_download_public( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result { - Err(AntdError::NotImplemented("file download pending ant-core fix".into())) + let address_bytes = hex::decode(&req.address) + .map_err(|e| AntdError::BadRequest(format!("invalid hex address: {e}")))?; + let address: [u8; 32] = address_bytes + .try_into() + .map_err(|_| AntdError::BadRequest("address must be 32 bytes".into()))?; + + let dest = PathBuf::from(&req.dest_path); + let client = state.client.clone(); + tokio::spawn(async move { + let data_map = client.data_map_fetch(&address).await + .map_err(AntdError::from_core)?; + client.file_download(&data_map, &dest).await + .map_err(AntdError::from_core)?; + Ok::<_, AntdError>(()) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(axum::http::StatusCode::OK) } pub async fn dir_upload_public( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("directory upload pending ant-core fix".into())) + if state.client.wallet().is_none() { + return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + } + + let path = PathBuf::from(&req.path); + if !path.is_dir() { + return Err(AntdError::BadRequest(format!("not a directory: {}", req.path))); + } + + let mode = parse_payment_mode(req.payment_mode.as_deref()) + .map_err(AntdError::BadRequest)?; + + let client = state.client.clone(); + let address = tokio::spawn(async move { + let result = client.file_upload_with_mode(&path, mode).await + .map_err(AntdError::from_core)?; + let address = client.data_map_store(&result.data_map).await + .map_err(AntdError::from_core)?; + Ok::<_, AntdError>(address) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(DirUploadPublicResponse { + cost: String::new(), + address: hex::encode(address), + })) } pub async fn dir_download_public( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result { - Err(AntdError::NotImplemented("directory download pending ant-core fix".into())) + let address_bytes = hex::decode(&req.address) + .map_err(|e| AntdError::BadRequest(format!("invalid hex address: {e}")))?; + let address: [u8; 32] = address_bytes + .try_into() + .map_err(|_| AntdError::BadRequest("address must be 32 bytes".into()))?; + + let dest = PathBuf::from(&req.dest_path); + let client = state.client.clone(); + tokio::spawn(async move { + let data_map = client.data_map_fetch(&address).await + .map_err(AntdError::from_core)?; + client.file_download(&data_map, &dest).await + .map_err(AntdError::from_core)?; + Ok::<_, AntdError>(()) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(axum::http::StatusCode::OK) } pub async fn archive_get_public( From 75ac06aab45d72e2d99d50d329242a74d76c38b8 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 26 Mar 2026 14:22:19 +0000 Subject: [PATCH 08/20] Update docs, MCP server, and READMEs for payment modes and implemented endpoints - MCP: store_data and upload_file tools now accept payment_mode parameter - README: Add Payment Modes section explaining auto/merkle/single - llms.txt: Document payment_mode on data/file PUT endpoints - llms-full.txt: Add Payment Modes section with SDK examples - skill.md: Add merkle guidance to data storage pattern - antd-go/README.md: Add payment mode configuration examples - antd-mcp/README.md: Update tool table and add Payment Modes section Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 13 +++++++++- antd-go/README.md | 6 +++++ antd-mcp/README.md | 17 ++++++++++--- antd-mcp/src/antd_mcp/server.py | 16 +++++++++--- llms-full.txt | 43 +++++++++++++++++++++++++++++---- llms.txt | 22 ++++++++++++++--- skill.md | 7 +++++- 7 files changed, 106 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9a054a1..9d405cc 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ This is especially useful in managed mode, where a parent process (e.g. indelibl If no port file is found, all SDKs fall back to the default REST endpoint (`http://localhost:8082`) or gRPC target (`localhost:50051`). +### Payment Modes + +All data and file upload operations accept an optional `payment_mode` parameter (defaults to `"auto"`): + +- **`auto`** — Uses merkle batch payments for uploads of 64+ chunks, single payments otherwise. Recommended for most use cases. +- **`merkle`** — Forces merkle batch payments regardless of chunk count (minimum 2 chunks). Saves gas on larger uploads. +- **`single`** — Forces per-chunk payments. Useful for small data or debugging. + ## Components ### Infrastructure @@ -162,7 +170,7 @@ client = AntdClient() status = client.health() print(f"Network: {status.network}") -# Store data on the network +# Store data on the network (payment_mode defaults to "auto") result = client.data_put_public(b"Hello, Autonomi!") print(f"Address: {result.address}") print(f"Cost: {result.cost} atto tokens") @@ -170,6 +178,9 @@ print(f"Cost: {result.cost} atto tokens") # Retrieve it back data = client.data_get_public(result.address) print(data.decode()) # "Hello, Autonomi!" + +# For large uploads, you can explicitly set payment_mode: +# result = client.data_put_public(large_data, payment_mode="merkle") ``` ### Write your first app (JavaScript/TypeScript) diff --git a/antd-go/README.md b/antd-go/README.md index 05c14e0..ac5526b 100644 --- a/antd-go/README.md +++ b/antd-go/README.md @@ -73,6 +73,12 @@ client := antd.NewClient(antd.DefaultBaseURL, antd.WithTimeout(30 * time.Second) // Custom HTTP client client := antd.NewClient(antd.DefaultBaseURL, antd.WithHTTPClient(myHTTPClient)) + +// Payment mode for uploads (defaults to "auto") +result, _ := client.DataPutPublic(ctx, data, antd.WithPaymentMode("merkle")) +// "auto" = merkle for 64+ chunks, single otherwise +// "merkle" = force batch payments (saves gas, min 2 chunks) +// "single" = per-chunk payments ``` ## API Reference diff --git a/antd-mcp/README.md b/antd-mcp/README.md index 56f514f..8c71c47 100644 --- a/antd-mcp/README.md +++ b/antd-mcp/README.md @@ -50,9 +50,9 @@ The server will auto-discover the daemon via the port file. Add `"env": {"ANTD_B | # | Tool | Description | |---|------|-------------| -| 1 | `store_data(text, private?)` | Store text on the network (public or encrypted) | +| 1 | `store_data(text, private?, payment_mode?)` | Store text on the network (public or encrypted) | | 2 | `retrieve_data(address, private?)` | Retrieve text by address | -| 3 | `upload_file(path, is_directory?)` | Upload a local file or directory | +| 3 | `upload_file(path, is_directory?, payment_mode?)` | Upload a local file or directory | | 4 | `download_file(address, dest_path, is_directory?)` | Download to local path | | 5 | `get_cost(text?, file_path?)` | Estimate storage cost | | 6 | `check_balance()` | Check daemon health and network status | @@ -80,6 +80,16 @@ The server will auto-discover the daemon via the port file. Add `"env": {"ANTD_B | 13 | `archive_get(address)` | List files in an archive | | 14 | `archive_put(entries)` | Create an archive manifest | +### Payment Modes + +The `store_data` and `upload_file` tools accept an optional `payment_mode` parameter: + +| Mode | Behavior | +|------|----------| +| `"auto"` (default) | Uses merkle batch payments for 64+ chunks, single payments otherwise. Recommended for most use cases. | +| `"merkle"` | Forces merkle batch payments regardless of chunk count (minimum 2 chunks). Saves gas on larger uploads. | +| `"single"` | Forces per-chunk payments. Useful for small data or debugging. | + ## Response Format All tools return JSON with a `network` field indicating the connected network: @@ -110,6 +120,7 @@ antd-mcp/ ├── pyproject.toml └── src/antd_mcp/ ├── __init__.py - ├── server.py # 14 MCP tool definitions + ├── server.py # 16 MCP tool definitions + ├── discover.py # Daemon port-file discovery └── errors.py # Error formatting ``` diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index 9a71705..c87a42b 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -87,12 +87,16 @@ def _err(exc: Exception, network: str) -> str: async def store_data( text: str, private: bool = False, + payment_mode: str = "auto", ) -> str: """Store text on the Autonomi network. Args: text: The text content to store. private: If True, store as private (encrypted). Default: public. + payment_mode: Payment strategy — "auto" (default, uses merkle for 64+ + chunks), "merkle" (force batch payments, min 2 chunks), or "single" + (per-chunk payments). Returns: JSON with address and cost, or error details. @@ -101,9 +105,9 @@ async def store_data( data = text.encode("utf-8") try: if private: - result = await client.data_put_private(data) + result = await client.data_put_private(data, payment_mode=payment_mode) else: - result = await client.data_put_public(data) + result = await client.data_put_public(data, payment_mode=payment_mode) return _ok({"address": result.address, "cost": result.cost}, network) except AntdError as exc: return _err_antd(exc, network) @@ -152,12 +156,16 @@ async def retrieve_data( async def upload_file( path: str, is_directory: bool = False, + payment_mode: str = "auto", ) -> str: """Upload a local file or directory to the Autonomi network (public). Args: path: Absolute path to the local file or directory. is_directory: Set True if path is a directory. + payment_mode: Payment strategy — "auto" (default, uses merkle for 64+ + chunks), "merkle" (force batch payments, min 2 chunks), or "single" + (per-chunk payments). Returns: JSON with address and cost, or error details. @@ -165,9 +173,9 @@ async def upload_file( client, network = _get_ctx() try: if is_directory: - result = await client.dir_upload_public(path) + result = await client.dir_upload_public(path, payment_mode=payment_mode) else: - result = await client.file_upload_public(path) + result = await client.file_upload_public(path, payment_mode=payment_mode) return _ok({"address": result.address, "cost": result.cost}, network) except AntdError as exc: return _err_antd(exc, network) diff --git a/llms-full.txt b/llms-full.txt index fc04fca..52fa3b2 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -41,10 +41,13 @@ client = AntdClient() # REST, localhost:8082 client = AntdClient(transport="grpc") # gRPC, localhost:50051 client = AntdClient(base_url="http://remote:8082") -# Store and retrieve data +# Store and retrieve data (payment_mode defaults to "auto") result = client.data_put_public(b"hello world") print(result.address, result.cost) data = client.data_get_public(result.address) # b"hello world" + +# Explicit payment mode for large uploads +result = client.data_put_public(large_data, payment_mode="merkle") ``` ```typescript @@ -219,7 +222,7 @@ Response: `{"status": "ok", "network": "local"}` #### `POST /v1/data/public` Store public (unencrypted) data on the network. -Request: `{"data": ""}` +Request: `{"data": "", "payment_mode": "auto"}` (payment_mode is optional, defaults to "auto") Response: `{"cost": "", "address": ""}` #### `GET /v1/data/public/{addr}` @@ -233,7 +236,7 @@ Stream public data as chunked response. Same path params as above. #### `POST /v1/data/private` Store private (encrypted) data. Returns a data map instead of address. -Request: `{"data": ""}` +Request: `{"data": "", "payment_mode": "auto"}` (payment_mode is optional, defaults to "auto") Response: `{"cost": "", "data_map": ""}` #### `GET /v1/data/private` @@ -308,7 +311,7 @@ Upload and download files/directories from the local filesystem. #### `POST /v1/files/upload/public` Upload a local file. -Request: `{"path": "/absolute/path/to/file"}` +Request: `{"path": "/absolute/path/to/file", "payment_mode": "auto"}` (payment_mode is optional, defaults to "auto") Response: `{"cost": "", "address": ""}` #### `POST /v1/files/download/public` @@ -319,7 +322,7 @@ Request: `{"address": "", "dest_path": "/absolute/path"}` #### `POST /v1/dirs/upload/public` Upload a local directory (recursively). -Request: `{"path": "/absolute/path/to/dir"}` +Request: `{"path": "/absolute/path/to/dir", "payment_mode": "auto"}` (payment_mode is optional, defaults to "auto") Response: `{"cost": "", "address": ""}` #### `POST /v1/dirs/download/public` @@ -411,6 +414,36 @@ JS/TS, PHP, Lua, and Zig are REST-only. --- +## Payment Modes + +All data and file upload operations accept an optional `payment_mode` parameter that controls how storage payments are batched: + +| Mode | Behavior | +|------|----------| +| `"auto"` (default) | Uses merkle batch payments for uploads of 64+ chunks, single payments otherwise. Best for general use. | +| `"merkle"` | Forces merkle batch payments regardless of chunk count (minimum 2 chunks). Saves gas on larger uploads. | +| `"single"` | Forces per-chunk payments. Useful for small data or debugging. | + +This applies to all PUT endpoints (`data_put_public`, `data_put_private`, `file_upload_public`, `dir_upload_public`) across all SDKs. The parameter is always optional and defaults to `"auto"`. + +**REST:** Include `"payment_mode": "merkle"` in the JSON request body. + +**SDK examples:** +```python +# Python +result = client.data_put_public(data, payment_mode="merkle") +``` +```go +// Go +result, err := client.DataPutPublic(ctx, data, antd.WithPaymentMode("merkle")) +``` +```typescript +// TypeScript +const result = await client.dataPutPublic(data, { paymentMode: "merkle" }); +``` + +--- + ## JS/TS SDK Method Signatures ```typescript diff --git a/llms.txt b/llms.txt index 76e1e3f..e4ca5d3 100644 --- a/llms.txt +++ b/llms.txt @@ -43,9 +43,9 @@ data = client.data_get_public(result.address) | GET | `/health` | Health check and network status | | GET | `/v1/data/public/{addr}` | Retrieve public data by address | | GET | `/v1/data/public/{addr}/stream` | Stream public data by address | -| POST | `/v1/data/public` | Store public data | +| POST | `/v1/data/public` | Store public data (accepts optional `payment_mode`) | | GET | `/v1/data/private` | Retrieve private (encrypted) data | -| POST | `/v1/data/private` | Store private (encrypted) data | +| POST | `/v1/data/private` | Store private (encrypted) data (accepts optional `payment_mode`) | | POST | `/v1/data/cost` | Estimate data storage cost | | GET | `/v1/chunks/{addr}` | Get raw chunk by address | | POST | `/v1/chunks` | Store raw chunk | @@ -53,9 +53,9 @@ data = client.data_get_public(result.address) | HEAD | `/v1/graph/{addr}` | Check graph entry existence | | POST | `/v1/graph` | Create graph entry | | POST | `/v1/graph/cost` | Estimate graph entry cost | -| POST | `/v1/files/upload/public` | Upload file to network | +| POST | `/v1/files/upload/public` | Upload file to network (accepts optional `payment_mode`) | | POST | `/v1/files/download/public` | Download file from network | -| POST | `/v1/dirs/upload/public` | Upload directory to network | +| POST | `/v1/dirs/upload/public` | Upload directory to network (accepts optional `payment_mode`) | | POST | `/v1/dirs/download/public` | Download directory from network | | GET | `/v1/archives/public/{addr}` | List archive entries | | POST | `/v1/archives/public` | Create archive manifest | @@ -122,3 +122,17 @@ Port file locations: `%APPDATA%\ant\daemon.port` (Windows), `~/.local/share/ant/ - REST: `http://localhost:8082` - gRPC: `localhost:50051` + +## Payment Modes + +All data and file PUT/upload endpoints accept an optional `payment_mode` field in the request body: + +| Mode | Behavior | +|------|----------| +| `"auto"` (default) | Uses merkle batch payments for 64+ chunks, single payments otherwise | +| `"merkle"` | Forces merkle batch payments regardless of chunk count (minimum 2 chunks). Saves gas on larger uploads | +| `"single"` | Forces per-chunk payments. Useful for small data or debugging | + +REST example: `POST /v1/data/public` with `{"data": "", "payment_mode": "merkle"}` + +All SDK `*_put_public`, `*_put_private`, `file_upload_public`, and `dir_upload_public` methods accept a `payment_mode` parameter (defaults to `"auto"`). diff --git a/skill.md b/skill.md index 54eb41f..4e0f0d4 100644 --- a/skill.md +++ b/skill.md @@ -68,14 +68,19 @@ This is the most important decision. Match the developer's use case to the right Store data permanently on the network. Content-addressed, so duplicate data is free. ```python -# Store public data +# Store public data (payment_mode defaults to "auto") result = client.data_put_public(b"Hello, Autonomi!") print(f"Address: {result.address}") # Retrieve it back data = client.data_get_public(result.address) + +# For large uploads, explicitly use merkle batch payments to save gas +result = client.data_put_public(large_data, payment_mode="merkle") ``` +All write operations accept an optional `payment_mode` parameter: `"auto"` (default — uses merkle for 64+ chunks), `"merkle"` (force batch payments, min 2 chunks), or `"single"` (per-chunk payments). The `"auto"` mode is recommended for most use cases. + **When to suggest this:** Developer wants permanent, immutable content storage with public readability. ### Pattern 2: Private Data Storage From 06674f0ba656312edb5d585a2adce0fb57d1fbf7 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 26 Mar 2026 14:43:52 +0000 Subject: [PATCH 09/20] Implement cost estimation and wallet approve across antd and all SDKs antd: - Data cost: encrypts data to determine chunks, queries network for storage quotes, sums prices (skips already-stored chunks) - File cost: same approach for files on disk - Wallet approve: POST /v1/wallet/approve calls approve_token_spend() to authorize payment contracts (one-time before storage) - Added self_encryption dependency for cost estimation chunking SDK bindings added for wallet_approve() across all 15 languages + MCP. Documentation updated (llms.txt, llms-full.txt, skill.md, READMEs). Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/include/antd/client.hpp | 3 + antd-cpp/src/client.cpp | 5 + antd-csharp/Antd.Sdk/AntdRestClient.cs | 12 +++ antd-csharp/Antd.Sdk/IAntdClient.cs | 1 + antd-dart/lib/src/client.dart | 6 ++ antd-elixir/lib/antd/client.ex | 16 +++ antd-go/client.go | 11 +++ .../java/com/autonomi/antd/AntdClient.java | 13 +++ antd-js/src/rest-client.ts | 6 ++ .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 7 ++ .../kotlin/com/autonomi/sdk/IAntdClient.kt | 1 + .../main/kotlin/com/autonomi/sdk/Models.kt | 5 + antd-lua/src/antd/client.lua | 8 ++ antd-mcp/README.md | 28 ++++-- antd-mcp/src/antd_mcp/server.py | 27 +++++- antd-php/src/AntdClient.php | 24 +++++ antd-py/src/antd/_rest.py | 14 +++ antd-ruby/lib/antd/client.rb | 7 ++ antd-rust/src/client.rs | 14 +++ .../Sources/AntdSdk/AntdClientProtocol.swift | 1 + .../Sources/AntdSdk/AntdRestClient.swift | 10 ++ antd-zig/src/antd.zig | 7 ++ antd-zig/src/json_helpers.zig | 16 +++ antd/Cargo.lock | 1 + antd/Cargo.toml | 1 + antd/src/rest/data.rs | 40 +++++++- antd/src/rest/files.rs | 44 ++++++++- antd/src/rest/mod.rs | 1 + antd/src/rest/wallet.rs | 18 ++++ antd/src/types.rs | 6 ++ llms-full.txt | 97 +++++++++++++++++++ llms.txt | 7 +- skill.md | 2 +- 33 files changed, 439 insertions(+), 20 deletions(-) diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index 58b78f2..ad3b7ee 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -121,6 +121,9 @@ class Client { /// Get the wallet balance (tokens and gas). WalletBalance wallet_balance(); + /// Approve the wallet to spend tokens on payment contracts (one-time operation). + bool wallet_approve(); + private: struct Impl; std::unique_ptr impl_; diff --git a/antd-cpp/src/client.cpp b/antd-cpp/src/client.cpp index 00a46cc..a9671f7 100644 --- a/antd-cpp/src/client.cpp +++ b/antd-cpp/src/client.cpp @@ -352,4 +352,9 @@ WalletBalance Client::wallet_balance() { }; } +bool Client::wallet_approve() { + auto j = impl_->do_json("POST", "/v1/wallet/approve", json::object()); + return j.value("approved", false); +} + } // namespace antd diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index ac40716..6030f80 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -246,6 +246,15 @@ 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; + } + // ── Internal DTOs for JSON deserialization ── private sealed record HealthResponseDto( @@ -292,4 +301,7 @@ private sealed record WalletAddressDto( private sealed record WalletBalanceDto( [property: JsonPropertyName("balance")] string Balance, [property: JsonPropertyName("gas_balance")] string GasBalance); + + private sealed record WalletApproveDto( + [property: JsonPropertyName("approved")] bool Approved); } diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index c92147c..c63b1a4 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -34,4 +34,5 @@ public interface IAntdClient : IDisposable // Wallet Task WalletAddressAsync(); Task WalletBalanceAsync(); + Task WalletApproveAsync(); } diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index 1fae92a..a19e741 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -322,4 +322,10 @@ class AntdClient { final json = await _doJson('GET', '/v1/wallet/balance'); return WalletBalance.fromJson(json!); } + + /// Approves the wallet to spend tokens on payment contracts (one-time operation). + Future walletApprove() async { + final json = await _doJson('POST', '/v1/wallet/approve', {}); + return json!['approved'] as bool; + } } diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index 362688a..01d5b7f 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -480,6 +480,22 @@ defmodule Antd.Client do @spec wallet_balance!(t()) :: Antd.WalletBalance.t() def wallet_balance!(client), do: unwrap!(wallet_balance(client)) + @doc "Approves the wallet to spend tokens on payment contracts (one-time operation)." + @spec wallet_approve(t()) :: {:ok, boolean()} | {:error, Exception.t()} + def wallet_approve(%__MODULE__{} = client) do + case do_json(client, :post, "/v1/wallet/approve", %{}) do + {:ok, body} -> + {:ok, body["approved"] == true} + + {:error, _} = err -> + err + end + end + + @doc "Like `wallet_approve/1` but raises on error." + @spec wallet_approve!(t()) :: boolean() + def wallet_approve!(client), do: unwrap!(wallet_approve(client)) + # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- diff --git a/antd-go/client.go b/antd-go/client.go index 69c0aa8..d819296 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -477,3 +477,14 @@ func (c *Client) WalletBalance(ctx context.Context) (*WalletBalance, error) { GasBalance: str(j, "gas_balance"), }, nil } + +// WalletApprove approves the wallet to spend tokens on payment contracts. +// This is a one-time operation required before any storage operations. +func (c *Client) WalletApprove(ctx context.Context) error { + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/wallet/approve", map[string]any{}) + if err != nil { + return err + } + _ = j + return nil +} diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index aa5dc53..1d27156 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -368,4 +368,17 @@ public WalletBalance walletBalance() { Map j = doJson("GET", "/v1/wallet/balance", null); return new WalletBalance(str(j, "balance"), str(j, "gas_balance")); } + + /** + * Approves the wallet to spend tokens on payment contracts. + * This is a one-time operation required before any storage operations. + * + * @return true if the wallet was approved + * @throws AntdException if no wallet is configured (HTTP 400) or on other errors + */ + public boolean walletApprove() { + Map j = doJson("POST", "/v1/wallet/approve", "{}"); + Object approved = j.get("approved"); + return approved instanceof Boolean b && b; + } } diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index 60e82d1..895e550 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -303,4 +303,10 @@ export class RestClient { const j = await this.getJson<{ balance: string; gas_balance: string }>("/v1/wallet/balance"); return { balance: j.balance, gasBalance: j.gas_balance }; } + + /** Approve the wallet to spend tokens on payment contracts (one-time operation). */ + async walletApprove(): Promise { + const j = await this.postJson<{ approved: boolean }>("/v1/wallet/approve", {}); + return j.approved; + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index c151693..4651805 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -270,4 +270,11 @@ class AntdRestClient( val resp = getJson("/v1/wallet/balance") return WalletBalance(resp.balance, resp.gasBalance) } + + /** Approves the wallet to spend tokens on payment contracts (one-time operation). */ + override suspend fun walletApprove(): Boolean { + val body = buildJsonObject {}.toString() + val resp = postJson("/v1/wallet/approve", body) + return resp.approved + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt index e2d50f3..57d2ef9 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt @@ -42,4 +42,5 @@ interface IAntdClient : Closeable { // Wallet suspend fun walletAddress(): WalletAddress suspend fun walletBalance(): WalletBalance + suspend fun walletApprove(): Boolean } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt index 95700a3..a0271cf 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt @@ -95,3 +95,8 @@ internal data class WalletBalanceDto( val balance: String, @SerialName("gas_balance") val gasBalance: String, ) + +@Serializable +internal data class WalletApproveDto( + val approved: Boolean, +) diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index da4644b..46c779d 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -440,6 +440,14 @@ function Client:wallet_balance() return { balance = str(j, "balance"), gas_balance = str(j, "gas_balance") }, nil end +--- Approve the wallet to spend tokens on payment contracts (one-time operation). +-- @return boolean|nil, error|nil +function Client:wallet_approve() + local j, _, err = self:_do_json("POST", "/v1/wallet/approve", {}) + if err then return nil, err end + return j.approved == true, nil +end + --- Create a client using daemon port discovery. -- Falls back to the default base URL if discovery fails. -- @param opts table optional settings: { timeout = number } diff --git a/antd-mcp/README.md b/antd-mcp/README.md index 8c71c47..0b6b7e2 100644 --- a/antd-mcp/README.md +++ b/antd-mcp/README.md @@ -1,6 +1,6 @@ # antd-mcp — MCP Server for Autonomi -An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the Autonomi network as 14 tools for AI agents. Works with Claude Desktop, Claude Code, and any MCP-compatible client. +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the Autonomi network as 17 tools for AI agents. Works with Claude Desktop, Claude Code, and any MCP-compatible client. ## Installation @@ -57,28 +57,36 @@ The server will auto-discover the daemon via the port file. Add `"env": {"ANTD_B | 5 | `get_cost(text?, file_path?)` | Estimate storage cost | | 6 | `check_balance()` | Check daemon health and network status | +### Wallet Operations + +| # | Tool | Description | +|---|------|-------------| +| 7 | `wallet_address()` | Get wallet public address | +| 8 | `wallet_balance()` | Get wallet token and gas balances | +| 16 | `wallet_approve()` | Approve wallet to spend tokens on payment contracts (one-time) | + ### Chunk Operations | # | Tool | Description | |---|------|-------------| -| 7 | `chunk_put(data)` | Store a raw chunk (base64 input) | -| 8 | `chunk_get(address)` | Retrieve a chunk (base64 output) | +| 9 | `chunk_put(data)` | Store a raw chunk (base64 input) | +| 10 | `chunk_get(address)` | Retrieve a chunk (base64 output) | ### Graph Operations | # | Tool | Description | |---|------|-------------| -| 9 | `create_graph_entry(owner_secret_key, content, parents?, descendants?)` | Create DAG node | -| 10 | `get_graph_entry(address)` | Read graph entry | -| 11 | `graph_entry_exists(address)` | Check if entry exists | -| 12 | `graph_entry_cost(public_key)` | Estimate creation cost | +| 11 | `create_graph_entry(owner_secret_key, content, parents?, descendants?)` | Create DAG node | +| 12 | `get_graph_entry(address)` | Read graph entry | +| 13 | `graph_entry_exists(address)` | Check if entry exists | +| 14 | `graph_entry_cost(public_key)` | Estimate creation cost | ### Archive Operations | # | Tool | Description | |---|------|-------------| -| 13 | `archive_get(address)` | List files in an archive | -| 14 | `archive_put(entries)` | Create an archive manifest | +| 15 | `archive_get(address)` | List files in an archive | +| 17 | `archive_put(entries)` | Create an archive manifest | ### Payment Modes @@ -120,7 +128,7 @@ antd-mcp/ ├── pyproject.toml └── src/antd_mcp/ ├── __init__.py - ├── server.py # 16 MCP tool definitions + ├── server.py # 17 MCP tool definitions ├── discover.py # Daemon port-file discovery └── errors.py # Error formatting ``` diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index c87a42b..c9db591 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -547,7 +547,32 @@ async def archive_get( # --------------------------------------------------------------------------- -# Tool 16: archive_put +# Tool 16: wallet_approve +# --------------------------------------------------------------------------- + + +@mcp.tool() +async def wallet_approve() -> str: + """Approve the wallet to spend tokens on payment contracts. + + This is a one-time operation required before any storage operations. + Must be called after configuring a wallet but before storing data. + + Returns: + JSON with approved boolean, or error details. + """ + client, network = _get_ctx() + try: + result = await client.wallet_approve() + return _ok({"approved": result}, network) + except AntdError as exc: + return _err_antd(exc, network) + except Exception as exc: + return _err(exc, network) + + +# --------------------------------------------------------------------------- +# Tool 17: archive_put # --------------------------------------------------------------------------- diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index 65e795d..0d50547 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -840,6 +840,30 @@ public function walletBalanceAsync(): PromiseInterface ); } + /** + * Approve the wallet to spend tokens on payment contracts (one-time operation). + * + * @return bool + * @throws AntdError if no wallet is configured (HTTP 400) + */ + public function walletApprove(): bool + { + $json = $this->doJson('POST', '/v1/wallet/approve', []); + return $json['approved'] ?? false; + } + + /** + * Async: Approve the wallet to spend tokens on payment contracts (one-time operation). + * + * @return PromiseInterface + */ + public function walletApproveAsync(): PromiseInterface + { + return $this->doJsonAsync('POST', '/v1/wallet/approve', [])->then( + fn(?array $json) => $json['approved'] ?? false, + ); + } + /** * Estimate the cost of uploading a file. */ diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 422022d..65bc11f 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -249,6 +249,13 @@ def wallet_balance(self) -> WalletBalance: j = resp.json() return WalletBalance(balance=j["balance"], gas_balance=j["gas_balance"]) + def wallet_approve(self) -> bool: + """Approve the wallet to spend tokens on payment contracts (one-time operation).""" + resp = self._http.post("/v1/wallet/approve", json={}) + _check(resp) + j = resp.json() + return j.get("approved", False) + class AsyncRestClient: """Asynchronous REST client for the antd daemon.""" @@ -456,3 +463,10 @@ async def wallet_balance(self) -> WalletBalance: _check(resp) j = resp.json() return WalletBalance(balance=j["balance"], gas_balance=j["gas_balance"]) + + async def wallet_approve(self) -> bool: + """Approve the wallet to spend tokens on payment contracts (one-time operation).""" + resp = await self._http.post("/v1/wallet/approve", json={}) + _check(resp) + j = resp.json() + return j.get("approved", False) diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index d65bdd3..6b4694f 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -256,6 +256,13 @@ def wallet_balance WalletBalance.new(balance: j["balance"], gas_balance: j["gas_balance"]) end + # Approve the wallet to spend tokens on payment contracts (one-time operation). + # @return [Boolean] + def wallet_approve + j = do_json(:post, "/v1/wallet/approve", {}) + j["approved"] == true + end + private def b64_encode(data) diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index 81f73c6..e13ba17 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -547,4 +547,18 @@ impl Client { gas_balance: Self::str_field(&j, "gas_balance"), }) } + + /// Approves the wallet to spend tokens on payment contracts. + /// This is a one-time operation required before any storage operations. + pub async fn wallet_approve(&self) -> Result { + let (j, _) = self + .do_json( + reqwest::Method::POST, + "/v1/wallet/approve", + Some(json!({})), + ) + .await?; + let j = j.unwrap_or_default(); + Ok(j.get("approved").and_then(|v| v.as_bool()).unwrap_or(false)) + } } diff --git a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift index a12d48d..6b232cb 100644 --- a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift +++ b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift @@ -38,4 +38,5 @@ public protocol AntdClientProtocol: Sendable { // Wallet func walletAddress() async throws -> WalletAddress func walletBalance() async throws -> WalletBalance + func walletApprove() async throws -> Bool } diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index 1122d89..b7cae2e 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -205,6 +205,12 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { let resp: WalletBalanceDTO = try await getJSON("/v1/wallet/balance") return WalletBalance(balance: resp.balance, gasBalance: resp.gasBalance) } + + /// Approves the wallet to spend tokens on payment contracts (one-time operation). + public func walletApprove() async throws -> Bool { + let resp: WalletApproveDTO = try await postJSON("/v1/wallet/approve", body: [:] as [String: Any]) + return resp.approved + } } // MARK: - Internal DTOs @@ -265,6 +271,10 @@ private struct WalletBalanceDTO: Decodable { let gasBalance: String } +private struct WalletApproveDTO: Decodable { + let approved: Bool +} + extension JSONDecoder { static let snakeCase: JSONDecoder = { let decoder = JSONDecoder() diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index 09a88cc..9bee1af 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -319,6 +319,13 @@ pub const Client = struct { return json_helpers.parseWalletBalance(self.allocator, resp); } + /// Approve the wallet to spend tokens on payment contracts (one-time operation). + pub fn walletApprove(self: *Client) !bool { + const resp = try self.doRequest(.POST, "/v1/wallet/approve", "{}") orelse return error.JsonError; + defer self.allocator.free(resp); + return json_helpers.parseBoolField(self.allocator, resp, "approved"); + } + // --- Files --- /// Upload a local file to the network. diff --git a/antd-zig/src/json_helpers.zig b/antd-zig/src/json_helpers.zig index 0ef1a69..7d0dee7 100644 --- a/antd-zig/src/json_helpers.zig +++ b/antd-zig/src/json_helpers.zig @@ -428,6 +428,22 @@ pub fn parseWalletBalance(allocator: Allocator, body: []const u8) !models.Wallet }; } +/// Extract a boolean field from a JSON response body. +pub fn parseBoolField(allocator: Allocator, body: []const u8, key: []const u8) !bool { + const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch + return error.JsonError; + defer parsed.deinit(); + const obj = switch (parsed.value) { + .object => |o| o, + else => return error.JsonError, + }; + const val = obj.get(key) orelse return false; + return switch (val) { + .bool => |b| b, + else => false, + }; +} + /// Extract the "error" message from a JSON error response body. pub fn parseErrorMessage(allocator: Allocator, body: []const u8) ?[]const u8 { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return null; diff --git a/antd/Cargo.lock b/antd/Cargo.lock index f4e304e..76b5889 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -971,6 +971,7 @@ dependencies = [ "prost", "rand 0.8.5", "rmp-serde", + "self_encryption", "serde", "serde_json", "thiserror 2.0.18", diff --git a/antd/Cargo.toml b/antd/Cargo.toml index eed9b6f..1e410a0 100644 --- a/antd/Cargo.toml +++ b/antd/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] ant-core = { git = "https://github.com/WithAutonomi/ant-client" } ant-evm = "0.1.19" +self_encryption = "0.35.0" evmlib = "0.4.9" axum = { version = "0.8", features = ["macros"] } tower-http = { version = "0.6", features = ["cors", "trace"] } diff --git a/antd/src/rest/data.rs b/antd/src/rest/data.rs index c88456a..67e58cd 100644 --- a/antd/src/rest/data.rs +++ b/antd/src/rest/data.rs @@ -116,10 +116,44 @@ pub async fn data_get_private( } pub async fn data_cost( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("data cost estimation not yet available".into())) + let data = BASE64.decode(&req.data) + .map_err(|e| AntdError::BadRequest(format!("invalid base64: {e}")))?; + + // Encrypt to determine chunk count and addresses, then quote each + let client = state.client.clone(); + let total_cost = tokio::spawn(async move { + use self_encryption::encrypt; + let (_data_map, encrypted_chunks) = encrypt(Bytes::from(data)) + .map_err(|e| AntdError::Internal(format!("encryption failed: {e}")))?; + + let mut total = ant_core::data::U256::ZERO; + for chunk in &encrypted_chunks { + let address = ant_core::data::compute_address(&chunk.content); + let data_size = chunk.content.len() as u64; + match client.get_store_quotes(&address, data_size, 0).await { + Ok(quotes) => { + for (_, _, _, price) in "es { + total = total.saturating_add(*price); + } + } + Err(e) => { + // AlreadyStored means no cost for this chunk + let core_err_str = format!("{e}"); + if !core_err_str.contains("AlreadyStored") { + return Err(AntdError::from_core(e)); + } + } + } + } + Ok::<_, AntdError>(total) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(CostResponse { + cost: total_cost.to_string(), + })) } pub async fn data_stream_public( diff --git a/antd/src/rest/files.rs b/antd/src/rest/files.rs index a4a30b9..b66e148 100644 --- a/antd/src/rest/files.rs +++ b/antd/src/rest/files.rs @@ -131,8 +131,46 @@ pub async fn archive_put_public( } pub async fn file_cost( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::NotImplemented("file cost estimation not yet available".into())) + let path = PathBuf::from(&req.path); + if !path.exists() { + return Err(AntdError::BadRequest(format!("path not found: {}", req.path))); + } + + // Read file, encrypt to get chunks, then quote each + let client = state.client.clone(); + let total_cost = tokio::spawn(async move { + use self_encryption::encrypt; + let file_data = tokio::fs::read(&path).await + .map_err(|e| AntdError::Internal(format!("failed to read file: {e}")))?; + + let (_data_map, encrypted_chunks) = encrypt(bytes::Bytes::from(file_data)) + .map_err(|e| AntdError::Internal(format!("encryption failed: {e}")))?; + + let mut total = ant_core::data::U256::ZERO; + for chunk in &encrypted_chunks { + let address = ant_core::data::compute_address(&chunk.content); + let data_size = chunk.content.len() as u64; + match client.get_store_quotes(&address, data_size, 0).await { + Ok(quotes) => { + for (_, _, _, price) in "es { + total = total.saturating_add(*price); + } + } + Err(e) => { + let core_err_str = format!("{e}"); + if !core_err_str.contains("AlreadyStored") { + return Err(AntdError::from_core(e)); + } + } + } + } + Ok::<_, AntdError>(total) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(CostResponse { + cost: total_cost.to_string(), + })) } diff --git a/antd/src/rest/mod.rs b/antd/src/rest/mod.rs index d5db591..469e834 100644 --- a/antd/src/rest/mod.rs +++ b/antd/src/rest/mod.rs @@ -47,6 +47,7 @@ pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router // Wallet .route("/v1/wallet/address", get(wallet::wallet_address)) .route("/v1/wallet/balance", get(wallet::wallet_balance)) + .route("/v1/wallet/approve", post(wallet::wallet_approve)) .with_state(state); if enable_cors { diff --git a/antd/src/rest/wallet.rs b/antd/src/rest/wallet.rs index e192d71..bf53308 100644 --- a/antd/src/rest/wallet.rs +++ b/antd/src/rest/wallet.rs @@ -35,3 +35,21 @@ pub async fn wallet_balance( gas_balance: gas_balance.to_string(), })) } + +pub async fn wallet_approve( + State(state): State>, +) -> Result, AntdError> { + if state.client.wallet().is_none() { + return Err(AntdError::BadRequest("no EVM wallet configured".into())); + } + + let client = state.client.clone(); + tokio::spawn(async move { + client.approve_token_spend().await + .map_err(AntdError::from_core) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(WalletApproveResponse { + approved: true, + })) +} diff --git a/antd/src/types.rs b/antd/src/types.rs index 9471866..3001628 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -210,6 +210,12 @@ pub struct WalletAddressResponse { pub address: String, } +#[derive(Serialize)] +pub struct WalletApproveResponse { + /// Whether the token spend was approved. + pub approved: bool, +} + // ── Health ── #[derive(Serialize)] diff --git a/llms-full.txt b/llms-full.txt index 52fa3b2..4701a18 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -354,6 +354,25 @@ Estimate file upload cost. Request: `{"path": "/path/to/file", "is_public": true, "include_archive": false}` Response: `{"cost": ""}` +#### `GET /v1/wallet/address` +Get the wallet's public address. + +Response: `{"address": "0x..."}` +Returns 400 if no wallet is configured. + +#### `GET /v1/wallet/balance` +Get the wallet's token and gas balances. + +Response: `{"balance": "", "gas_balance": ""}` +Returns 400 if no wallet is configured. + +#### `POST /v1/wallet/approve` +Approve the wallet to spend tokens on payment contracts (one-time operation before any storage). + +Request: `{}` (empty body) +Response: `{"approved": true}` +Returns 400 if no wallet is configured. + --- ## gRPC API — All Services @@ -481,6 +500,11 @@ client.dirDownloadPublic(address: string, destPath: string): Promise client.archiveGetPublic(address: string): Promise client.archivePutPublic(archive: Archive): Promise client.fileCost(path: string, isPublic?: boolean, includeArchive?: boolean): Promise + +// Wallet +client.walletAddress(): Promise +client.walletBalance(): Promise +client.walletApprove(): Promise ``` --- @@ -521,6 +545,11 @@ client.dir_download_public(address: str, dest_path: str) → None client.archive_get_public(address: str) → Archive client.archive_put_public(archive: Archive) → PutResult client.file_cost(path: str, is_public: bool = True, include_archive: bool = False) → str + +# Wallet +client.wallet_address() → WalletAddress +client.wallet_balance() → WalletBalance +client.wallet_approve() → bool ``` --- @@ -561,6 +590,11 @@ Task DirDownloadPublicAsync(string address, string destPath); Task ArchiveGetPublicAsync(string address); Task ArchivePutPublicAsync(Archive archive); Task FileCostAsync(string path, bool isPublic = true, bool includeArchive = false); + +// Wallet +Task WalletAddressAsync(); +Task WalletBalanceAsync(); +Task WalletApproveAsync(); ``` --- @@ -601,6 +635,11 @@ suspend fun dirDownloadPublic(address: String, destPath: String) suspend fun archiveGetPublic(address: String): Archive suspend fun archivePutPublic(archive: Archive): PutResult suspend fun fileCost(path: String, isPublic: Boolean = true, includeArchive: Boolean = false): String + +// Wallet +suspend fun walletAddress(): WalletAddress +suspend fun walletBalance(): WalletBalance +suspend fun walletApprove(): Boolean ``` --- @@ -643,6 +682,11 @@ func dirDownloadPublic(address: String, destPath: String) async throws func archiveGetPublic(address: String) async throws -> Archive func archivePutPublic(archive: Archive) async throws -> PutResult func fileCost(path: String, isPublic: Bool, includeArchive: Bool) async throws -> String + +// Wallet +func walletAddress() async throws -> WalletAddress +func walletBalance() async throws -> WalletBalance +func walletApprove() async throws -> Bool ``` --- @@ -682,6 +726,11 @@ func (c *Client) DirDownloadPublic(ctx context.Context, address string, destPath func (c *Client) ArchiveGetPublic(ctx context.Context, address string) (*Archive, error) func (c *Client) ArchivePutPublic(ctx context.Context, archive *Archive) (*PutResult, error) func (c *Client) FileCost(ctx context.Context, path string, isPublic bool, includeArchive bool) (string, error) + +// Wallet +func (c *Client) WalletAddress(ctx context.Context) (*WalletAddress, error) +func (c *Client) WalletBalance(ctx context.Context) (*WalletBalance, error) +func (c *Client) WalletApprove(ctx context.Context) error ``` --- @@ -725,6 +774,11 @@ Archive archiveGetPublic(String address) throws AntdException PutResult archivePutPublic(Archive archive) throws AntdException String fileCost(String path, boolean isPublic, boolean includeArchive) throws AntdException +// Wallet +WalletAddress walletAddress() throws AntdException +WalletBalance walletBalance() throws AntdException +boolean walletApprove() throws AntdException + // AntdClient implements AutoCloseable — use try-with-resources ``` @@ -767,6 +821,11 @@ async fn archive_get_public(&self, address: &str) -> Result async fn archive_put_public(&self, archive: &Archive) -> Result async fn file_cost(&self, path: &str, is_public: bool, include_archive: bool) -> Result +// Wallet +async fn wallet_address(&self) -> Result +async fn wallet_balance(&self) -> Result +async fn wallet_approve(&self) -> Result + // AntdError variants: BadRequest, Payment, NotFound, AlreadyExists, Fork, TooLarge, Internal, Network ``` @@ -810,6 +869,11 @@ antd::Archive archiveGetPublic(const std::string& address) antd::PutResult archivePutPublic(const antd::Archive& archive) std::string fileCost(const std::string& path, bool isPublic = true, bool includeArchive = false) +// Wallet +antd::WalletAddress walletAddress() +antd::WalletBalance walletBalance() +bool walletApprove() + // All methods throw antd::AntdError or subclasses: BadRequestError, PaymentError, NotFoundError, // AlreadyExistsError, ForkError, TooLargeError, InternalError, NetworkError ``` @@ -852,6 +916,11 @@ client.dir_download_public(address, dest_path) → nil client.archive_get_public(address) → Archive client.archive_put_public(archive) → PutResult client.file_cost(path, is_public: true, include_archive: false) → String + +# Wallet +client.wallet_address → WalletAddress +client.wallet_balance → WalletBalance +client.wallet_approve → Boolean ``` --- @@ -892,6 +961,11 @@ $client->dirDownloadPublic(string $address, string $destPath): void $client->archiveGetPublic(string $address): Archive $client->archivePutPublic(Archive $archive): PutResult $client->fileCost(string $path, bool $isPublic = true, bool $includeArchive = false): string + +// Wallet +$client->walletAddress(): array{address: string} +$client->walletBalance(): array{balance: string, gas_balance: string} +$client->walletApprove(): bool ``` --- @@ -932,6 +1006,11 @@ Future dirDownloadPublic(String address, String destPath) Future archiveGetPublic(String address) Future archivePutPublic(Archive archive) Future fileCost(String path, {bool isPublic = true, bool includeArchive = false}) + +// Wallet +Future walletAddress() +Future walletBalance() +Future walletApprove() ``` --- @@ -972,6 +1051,11 @@ client:dir_download_public(address, dest_path) → nil client:archive_get_public(address) → Archive client:archive_put_public(archive) → PutResult client:file_cost(path, is_public, include_archive) → string + +-- Wallet +client:wallet_address() → {address=string} +client:wallet_balance() → {balance=string, gas_balance=string} +client:wallet_approve() → boolean ``` --- @@ -1010,6 +1094,11 @@ Antd.Client.dir_download_public(client, address, dest_path) :: :ok | {:error, te Antd.Client.archive_get_public(client, address) :: {:ok, Archive.t()} | {:error, term()} Antd.Client.archive_put_public(client, archive) :: {:ok, PutResult.t()} | {:error, term()} Antd.Client.file_cost(client, path, is_public \\ true, include_archive \\ false) :: {:ok, String.t()} | {:error, term()} + +# Wallet +Antd.Client.wallet_address(client) :: {:ok, WalletAddress.t()} | {:error, term()} +Antd.Client.wallet_balance(client) :: {:ok, WalletBalance.t()} | {:error, term()} +Antd.Client.wallet_approve(client) :: {:ok, boolean()} | {:error, term()} ``` --- @@ -1051,6 +1140,11 @@ fn dirDownloadPublic(self: *Client, address: []const u8, dest_path: []const u8) fn archiveGetPublic(self: *Client, address: []const u8) !Archive fn archivePutPublic(self: *Client, archive: Archive) !PutResult fn fileCost(self: *Client, path: []const u8, is_public: bool, include_archive: bool) ![]u8 + +// Wallet +fn walletAddress(self: *Client) !WalletAddress +fn walletBalance(self: *Client) !WalletBalance +fn walletApprove(self: *Client) !bool ``` --- @@ -1073,6 +1167,9 @@ fn fileCost(self: *Client, path: []const u8, is_public: bool, include_archive: b | `archive_put` | `archive_put_public` | Create archive | | `get_cost` | `data_cost` / `file_cost` | Estimate storage cost | | `check_balance` | `health` | Check daemon health | +| `wallet_address` | `wallet_address` | Get wallet public address | +| `wallet_balance` | `wallet_balance` | Get wallet balances | +| `wallet_approve` | `wallet_approve` | Approve wallet for payments | --- diff --git a/llms.txt b/llms.txt index e4ca5d3..16886a9 100644 --- a/llms.txt +++ b/llms.txt @@ -1,6 +1,6 @@ # antd SDK -> Go, JavaScript/TypeScript, Python, C#, Kotlin, Swift, Ruby, PHP, Dart, Lua, Elixir, Zig, Rust, C++, and Java SDKs for the Autonomi network via the antd daemon. Provides REST and gRPC transports, an MCP server (14 tools), and a local daemon that manages wallet, payments, and network connectivity. +> Go, JavaScript/TypeScript, Python, C#, Kotlin, Swift, Ruby, PHP, Dart, Lua, Elixir, Zig, Rust, C++, and Java SDKs for the Autonomi network via the antd daemon. Provides REST and gRPC transports, an MCP server (17 tools), and a local daemon that manages wallet, payments, and network connectivity. ## Quick Start @@ -28,7 +28,7 @@ data = client.data_get_public(result.address) - [Rust SDK](antd-rust/) — `cargo add antd-client` — REST + gRPC, async/tokio - [C++ SDK](antd-cpp/) — CMake FetchContent — REST + gRPC, sync + async (C++20) - [Java SDK](antd-java/) — Gradle/Maven — REST + gRPC, sync + async (Java 17+) -- [MCP Server](antd-mcp/) — 14 tools for AI agent integration +- [MCP Server](antd-mcp/) — 17 tools for AI agent integration ## API Reference @@ -60,6 +60,9 @@ data = client.data_get_public(result.address) | GET | `/v1/archives/public/{addr}` | List archive entries | | POST | `/v1/archives/public` | Create archive manifest | | POST | `/v1/cost/file` | Estimate file upload cost | +| GET | `/v1/wallet/address` | Get wallet public address | +| GET | `/v1/wallet/balance` | Get wallet token and gas balances | +| POST | `/v1/wallet/approve` | Approve wallet to spend tokens on payment contracts | ## gRPC Services diff --git a/skill.md b/skill.md index 4e0f0d4..855c462 100644 --- a/skill.md +++ b/skill.md @@ -109,7 +109,7 @@ entry2 = client.graph_entry_put(key2, parents=[entry1.address], content=content2 ## Key Rules -1. **Every write costs tokens.** Always offer to estimate cost first with the `*_cost` methods. Reads are free. +1. **Every write costs tokens.** Always offer to estimate cost first with the `*_cost` methods (now fully implemented for both data and files). Before the first storage operation, the wallet must be approved via `wallet_approve()`. Reads are free. 2. **Data is permanent.** Once stored, it cannot be deleted. Warn developers about storing sensitive data publicly. 3. **No access revocation.** Once data is public, it stays public. 4. **Content-addressed = deduplication.** Storing the same bytes twice produces the same address and doesn't cost extra. From ecc83364b957582f6542502a1874e7228a68cb61 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 26 Mar 2026 14:51:06 +0000 Subject: [PATCH 10/20] Use HTTP 503 for missing wallet instead of 400/402 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When antd is started without AUTONOMI_WALLET_KEY, write operations (chunk put, data put, file upload, wallet endpoints) now return 503 Service Unavailable instead of 400 Bad Request or 402 Payment Required. This correctly signals a server configuration issue rather than a client error — the request is valid but the server can't fulfil it without a wallet. - antd: Added ServiceUnavailable error variant (503 / gRPC UNAVAILABLE) - All 15 SDKs: Added ServiceUnavailableError exception type, mapped 503 - Docs: Updated error tables in llms.txt, llms-full.txt, skill.md Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/include/antd/errors.hpp | 7 +++++++ antd-csharp/Antd.Sdk/Exceptions.cs | 7 +++++++ antd-dart/lib/src/errors.dart | 7 +++++++ antd-elixir/lib/antd/errors.ex | 12 ++++++++++++ antd-go/errors.go | 6 ++++++ .../com/autonomi/antd/errors/ExceptionFactory.java | 1 + .../antd/errors/ServiceUnavailableException.java | 8 ++++++++ antd-js/src/errors.ts | 9 +++++++++ .../src/main/kotlin/com/autonomi/sdk/Exceptions.kt | 2 ++ antd-lua/src/antd/errors.lua | 8 ++++++++ antd-php/src/Errors/ErrorFactory.php | 1 + antd-php/src/Errors/ServiceUnavailableError.php | 14 ++++++++++++++ antd-py/src/antd/exceptions.py | 6 ++++++ antd-ruby/lib/antd/errors.rb | 6 ++++++ antd-rust/src/errors.rs | 5 +++++ antd-swift/Sources/AntdSdk/Errors.swift | 7 +++++++ antd-zig/src/errors.zig | 2 ++ antd/src/error.rs | 5 +++++ antd/src/grpc/service.rs | 4 ++-- antd/src/rest/chunks.rs | 4 ++-- antd/src/rest/data.rs | 4 ++-- antd/src/rest/files.rs | 4 ++-- antd/src/rest/wallet.rs | 6 +++--- llms-full.txt | 1 + llms.txt | 1 + skill.md | 1 + 26 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 antd-java/src/main/java/com/autonomi/antd/errors/ServiceUnavailableException.java create mode 100644 antd-php/src/Errors/ServiceUnavailableError.php diff --git a/antd-cpp/include/antd/errors.hpp b/antd-cpp/include/antd/errors.hpp index 6978d46..ee4d84c 100644 --- a/antd-cpp/include/antd/errors.hpp +++ b/antd-cpp/include/antd/errors.hpp @@ -63,6 +63,12 @@ class NetworkError : public AntdError { NetworkError(const std::string& msg) : AntdError(502, msg) {} }; +/// Service unavailable, e.g. wallet not configured (HTTP 503). +class ServiceUnavailableError : public AntdError { +public: + ServiceUnavailableError(const std::string& msg) : AntdError(503, msg) {} +}; + /// Throw the appropriate AntdError subclass for an HTTP status code. [[noreturn]] inline void error_for_status(int code, const std::string& message) { switch (code) { @@ -73,6 +79,7 @@ class NetworkError : public AntdError { case 413: throw TooLargeError(message); case 500: throw InternalError(message); case 502: throw NetworkError(message); + case 503: throw ServiceUnavailableError(message); default: throw AntdError(code, message); } } diff --git a/antd-csharp/Antd.Sdk/Exceptions.cs b/antd-csharp/Antd.Sdk/Exceptions.cs index 1f17f41..c1ea8d2 100644 --- a/antd-csharp/Antd.Sdk/Exceptions.cs +++ b/antd-csharp/Antd.Sdk/Exceptions.cs @@ -47,6 +47,12 @@ public NetworkException(string message, int statusCode = 502) : base(message, statusCode) { } } +public class ServiceUnavailableException : AntdException +{ + public ServiceUnavailableException(string message, int statusCode = 503) + : base(message, statusCode) { } +} + public class TooLargeException : AntdException { public TooLargeException(string message, int statusCode = 413) @@ -73,6 +79,7 @@ public static AntdException FromHttpStatus(HttpStatusCode status, string body) 413 => new TooLargeException(body, code), 500 => new InternalException(body, code), 502 => new NetworkException(body, code), + 503 => new ServiceUnavailableException(body, code), _ => new AntdException(body, code), }; } diff --git a/antd-dart/lib/src/errors.dart b/antd-dart/lib/src/errors.dart index c9e73eb..1f6cde3 100644 --- a/antd-dart/lib/src/errors.dart +++ b/antd-dart/lib/src/errors.dart @@ -52,6 +52,11 @@ class NetworkError extends AntdError { const NetworkError(String message) : super(502, message); } +/// Service unavailable, e.g. wallet not configured (HTTP 503). +class ServiceUnavailableError extends AntdError { + const ServiceUnavailableError(String message) : super(503, message); +} + /// Returns the appropriate error type for an HTTP status code. AntdError errorForStatus(int statusCode, String message) { switch (statusCode) { @@ -69,6 +74,8 @@ AntdError errorForStatus(int statusCode, String message) { return InternalError(message); case 502: return NetworkError(message); + case 503: + return ServiceUnavailableError(message); default: return AntdError(statusCode, message); } diff --git a/antd-elixir/lib/antd/errors.ex b/antd-elixir/lib/antd/errors.ex index fae41a0..1b1834b 100644 --- a/antd-elixir/lib/antd/errors.ex +++ b/antd-elixir/lib/antd/errors.ex @@ -97,6 +97,17 @@ defmodule Antd.NetworkError do } end +defmodule Antd.ServiceUnavailableError do + @moduledoc "Service unavailable, e.g. wallet not configured (HTTP 503)." + + defexception [:message, :status_code] + + @type t :: %__MODULE__{ + message: String.t(), + status_code: integer() + } +end + defmodule Antd.Errors do @moduledoc false @@ -111,6 +122,7 @@ defmodule Antd.Errors do 413 -> %Antd.TooLargeError{message: message, status_code: 413} 500 -> %Antd.InternalError{message: message, status_code: 500} 502 -> %Antd.NetworkError{message: message, status_code: 502} + 503 -> %Antd.ServiceUnavailableError{message: message, status_code: 503} _ -> %Antd.AntdError{message: message, status_code: status_code} end end diff --git a/antd-go/errors.go b/antd-go/errors.go index 501cde5..ee53fe1 100644 --- a/antd-go/errors.go +++ b/antd-go/errors.go @@ -37,6 +37,10 @@ type InternalError struct{ AntdError } // NetworkError indicates the daemon cannot reach the network (HTTP 502). type NetworkError struct{ AntdError } +// ServiceUnavailableError indicates the daemon is missing a required +// dependency such as a wallet (HTTP 503). +type ServiceUnavailableError struct{ AntdError } + // errorForStatus returns the appropriate error type for an HTTP status code. func errorForStatus(statusCode int, message string) error { base := AntdError{StatusCode: statusCode, Message: message} @@ -55,6 +59,8 @@ func errorForStatus(statusCode int, message string) error { return &InternalError{base} case 502: return &NetworkError{base} + case 503: + return &ServiceUnavailableError{base} default: return &base } diff --git a/antd-java/src/main/java/com/autonomi/antd/errors/ExceptionFactory.java b/antd-java/src/main/java/com/autonomi/antd/errors/ExceptionFactory.java index 1153ee6..1567848 100644 --- a/antd-java/src/main/java/com/autonomi/antd/errors/ExceptionFactory.java +++ b/antd-java/src/main/java/com/autonomi/antd/errors/ExceptionFactory.java @@ -23,6 +23,7 @@ public static AntdException fromHttpStatus(int statusCode, String message) { case 413 -> new TooLargeException(message); case 500 -> new InternalException(message); case 502 -> new NetworkException(message); + case 503 -> new ServiceUnavailableException(message); default -> new AntdException(statusCode, message); }; } diff --git a/antd-java/src/main/java/com/autonomi/antd/errors/ServiceUnavailableException.java b/antd-java/src/main/java/com/autonomi/antd/errors/ServiceUnavailableException.java new file mode 100644 index 0000000..aea2860 --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/errors/ServiceUnavailableException.java @@ -0,0 +1,8 @@ +package com.autonomi.antd.errors; + +/** Service unavailable, e.g. wallet not configured (HTTP 503). */ +public class ServiceUnavailableException extends AntdException { + public ServiceUnavailableException(String message) { + super(503, message); + } +} diff --git a/antd-js/src/errors.ts b/antd-js/src/errors.ts index 6d94f65..0bb2d03 100644 --- a/antd-js/src/errors.ts +++ b/antd-js/src/errors.ts @@ -65,6 +65,14 @@ export class TooLargeError extends AntdError { } } +/** Service unavailable, e.g. wallet not configured (HTTP 503). */ +export class ServiceUnavailableError extends AntdError { + constructor(message: string, statusCode: number = 503) { + super(message, statusCode); + this.name = "ServiceUnavailableError"; + } +} + /** Internal server error (HTTP 500). */ export class InternalError extends AntdError { constructor(message: string, statusCode: number = 500) { @@ -82,6 +90,7 @@ const HTTP_STATUS_MAP: Record TooLargeException(body, statusCode) 500 -> InternalException(body, statusCode) 502 -> NetworkException(body, statusCode) + 503 -> ServiceUnavailableException(body, statusCode) else -> AntdException(body, statusCode) } diff --git a/antd-lua/src/antd/errors.lua b/antd-lua/src/antd/errors.lua index 5cf8f70..51b203b 100644 --- a/antd-lua/src/antd/errors.lua +++ b/antd-lua/src/antd/errors.lua @@ -74,6 +74,13 @@ function M.network(message) return new_error("network", 502, message) end +--- Create a service_unavailable error (HTTP 503). +-- @param message string +-- @return table +function M.service_unavailable(message) + return new_error("service_unavailable", 503, message) +end + --- Return the appropriate error for an HTTP status code. -- @param code number HTTP status code -- @param message string error message @@ -86,6 +93,7 @@ function M.error_for_status(code, message) if code == 413 then return M.too_large(message) end if code == 500 then return M.internal(message) end if code == 502 then return M.network(message) end + if code == 503 then return M.service_unavailable(message) end return new_error("unknown", code, message) end diff --git a/antd-php/src/Errors/ErrorFactory.php b/antd-php/src/Errors/ErrorFactory.php index 2a7190f..476bb10 100644 --- a/antd-php/src/Errors/ErrorFactory.php +++ b/antd-php/src/Errors/ErrorFactory.php @@ -19,6 +19,7 @@ public static function fromHttpStatus(int $code, string $message): AntdError 413 => new TooLargeError($message), 500 => new InternalError($message), 502 => new NetworkError($message), + 503 => new ServiceUnavailableError($message), default => new AntdError($code, $message), }; } diff --git a/antd-php/src/Errors/ServiceUnavailableError.php b/antd-php/src/Errors/ServiceUnavailableError.php new file mode 100644 index 0000000..42b7adf --- /dev/null +++ b/antd-php/src/Errors/ServiceUnavailableError.php @@ -0,0 +1,14 @@ + AntdError { 413 => AntdError::TooLarge(message), 500 => AntdError::Internal(message), 502 => AntdError::Network(message), + 503 => AntdError::ServiceUnavailable(message), _ => AntdError::Internal(format!("unexpected status {code}: {message}")), } } diff --git a/antd-swift/Sources/AntdSdk/Errors.swift b/antd-swift/Sources/AntdSdk/Errors.swift index dad9977..de09bbc 100644 --- a/antd-swift/Sources/AntdSdk/Errors.swift +++ b/antd-swift/Sources/AntdSdk/Errors.swift @@ -61,6 +61,12 @@ public final class InternalError: AntdError { } } +public final class ServiceUnavailableError: AntdError { + public init(_ message: String, statusCode: Int = 503) { + super.init(message, statusCode: statusCode) + } +} + enum ErrorMapping { static func fromHTTPStatus(_ statusCode: Int, body: String) -> AntdError { @@ -72,6 +78,7 @@ enum ErrorMapping { case 413: return TooLargeError(body, statusCode: statusCode) case 500: return InternalError(body, statusCode: statusCode) case 502: return NetworkError(body, statusCode: statusCode) + case 503: return ServiceUnavailableError(body, statusCode: statusCode) default: return AntdError(body, statusCode: statusCode) } } diff --git a/antd-zig/src/errors.zig b/antd-zig/src/errors.zig index e21f4ac..4e842d9 100644 --- a/antd-zig/src/errors.zig +++ b/antd-zig/src/errors.zig @@ -8,6 +8,7 @@ pub const AntdError = error{ TooLarge, Internal, Network, + ServiceUnavailable, UnexpectedStatus, HttpError, JsonError, @@ -29,6 +30,7 @@ pub fn errorForStatus(code: u16) AntdError { 413 => error.TooLarge, 500 => error.Internal, 502 => error.Network, + 503 => error.ServiceUnavailable, else => error.UnexpectedStatus, }; } diff --git a/antd/src/error.rs b/antd/src/error.rs index 9ca332a..16809be 100644 --- a/antd/src/error.rs +++ b/antd/src/error.rs @@ -25,6 +25,9 @@ pub enum AntdError { #[error("Timeout: {0}")] Timeout(String), + #[error("Service unavailable: {0}")] + ServiceUnavailable(String), + #[error("Not implemented: {0}")] NotImplemented(String), @@ -66,6 +69,7 @@ impl IntoResponse for AntdError { AntdError::Network(_) => StatusCode::BAD_GATEWAY, AntdError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE, AntdError::Timeout(_) => StatusCode::GATEWAY_TIMEOUT, + AntdError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, AntdError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, AntdError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, }; @@ -87,6 +91,7 @@ impl From for tonic::Status { AntdError::Network(msg) => tonic::Status::unavailable(msg), AntdError::TooLarge => tonic::Status::resource_exhausted("too large for memory"), AntdError::Timeout(msg) => tonic::Status::deadline_exceeded(msg), + AntdError::ServiceUnavailable(msg) => tonic::Status::unavailable(msg), AntdError::NotImplemented(msg) => tonic::Status::unimplemented(msg), AntdError::Internal(msg) => tonic::Status::internal(msg), } diff --git a/antd/src/grpc/service.rs b/antd/src/grpc/service.rs index a9723f9..360adf4 100644 --- a/antd/src/grpc/service.rs +++ b/antd/src/grpc/service.rs @@ -101,8 +101,8 @@ impl pb::chunk_service_server::ChunkService for ChunkServiceImpl { let data = request.into_inner().data; if self.state.client.wallet().is_none() { - return Err(Status::failed_precondition( - "no EVM wallet configured — set AUTONOMI_WALLET_KEY", + return Err(Status::unavailable( + "wallet not configured — set AUTONOMI_WALLET_KEY", )); } diff --git a/antd/src/rest/chunks.rs b/antd/src/rest/chunks.rs index 8560869..e03c9cd 100644 --- a/antd/src/rest/chunks.rs +++ b/antd/src/rest/chunks.rs @@ -34,8 +34,8 @@ pub async fn chunk_put( Json(req): Json, ) -> Result, AntdError> { if state.client.wallet().is_none() { - return Err(AntdError::Payment( - "no EVM wallet configured — set AUTONOMI_WALLET_KEY".into(), + return Err(AntdError::ServiceUnavailable( + "wallet not configured — set AUTONOMI_WALLET_KEY".into(), )); } diff --git a/antd/src/rest/data.rs b/antd/src/rest/data.rs index 67e58cd..3b6dde7 100644 --- a/antd/src/rest/data.rs +++ b/antd/src/rest/data.rs @@ -16,7 +16,7 @@ pub async fn data_put_public( Json(req): Json, ) -> Result, AntdError> { if state.client.wallet().is_none() { - return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + return Err(AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into())); } let data = BASE64.decode(&req.data) @@ -69,7 +69,7 @@ pub async fn data_put_private( Json(req): Json, ) -> Result, AntdError> { if state.client.wallet().is_none() { - return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + return Err(AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into())); } let data = BASE64.decode(&req.data) diff --git a/antd/src/rest/files.rs b/antd/src/rest/files.rs index b66e148..428c820 100644 --- a/antd/src/rest/files.rs +++ b/antd/src/rest/files.rs @@ -13,7 +13,7 @@ pub async fn file_upload_public( Json(req): Json, ) -> Result, AntdError> { if state.client.wallet().is_none() { - return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + return Err(AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into())); } let path = PathBuf::from(&req.path); @@ -67,7 +67,7 @@ pub async fn dir_upload_public( Json(req): Json, ) -> Result, AntdError> { if state.client.wallet().is_none() { - return Err(AntdError::Payment("no EVM wallet configured — set AUTONOMI_WALLET_KEY".into())); + return Err(AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into())); } let path = PathBuf::from(&req.path); diff --git a/antd/src/rest/wallet.rs b/antd/src/rest/wallet.rs index bf53308..0a1092e 100644 --- a/antd/src/rest/wallet.rs +++ b/antd/src/rest/wallet.rs @@ -11,7 +11,7 @@ pub async fn wallet_address( State(state): State>, ) -> Result, AntdError> { let wallet = state.client.wallet() - .ok_or_else(|| AntdError::BadRequest("no EVM wallet configured".into()))?; + .ok_or_else(|| AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into()))?; Ok(Json(WalletAddressResponse { address: format!("{:#x}", wallet.address()), @@ -22,7 +22,7 @@ pub async fn wallet_balance( State(state): State>, ) -> Result, AntdError> { let wallet = state.client.wallet() - .ok_or_else(|| AntdError::BadRequest("no EVM wallet configured".into()))?; + .ok_or_else(|| AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into()))?; let balance = wallet.balance_of_tokens().await .map_err(|e| AntdError::Internal(format!("failed to get token balance: {e}")))?; @@ -40,7 +40,7 @@ pub async fn wallet_approve( State(state): State>, ) -> Result, AntdError> { if state.client.wallet().is_none() { - return Err(AntdError::BadRequest("no EVM wallet configured".into())); + return Err(AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into())); } let client = state.client.clone(); diff --git a/llms-full.txt b/llms-full.txt index 4701a18..5e89218 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1185,6 +1185,7 @@ fn walletApprove(self: *Client) !bool | 413 | RESOURCE_EXHAUSTED | `TooLargeError` | `TooLargeError` | `TooLargeException` | `TooLargeException` | `TooLargeError` | `TooLargeException` | `AntdError::TooLarge` | `TooLargeError` | Data exceeds size limit | | 500 | INTERNAL | `InternalError` | `InternalError` | `InternalException` | `InternalException` | `InternalError` | `InternalException` | `AntdError::Internal` | `InternalError` | Internal server error | | 502 | UNAVAILABLE | `NetworkError` | `NetworkError` | `NetworkException` | `NetworkException` | `NetworkError` | `NetworkException` | `AntdError::Network` | `NetworkError` | Cannot reach network | +| 503 | UNAVAILABLE | `ServiceUnavailableError` | `ServiceUnavailableError` | `ServiceUnavailableException` | `ServiceUnavailableException` | `ServiceUnavailableError` | `ServiceUnavailableException` | `AntdError::ServiceUnavailable` | `ServiceUnavailableError` | Wallet not configured | All exceptions inherit from `AntdError` (JS/TS, Python, Swift, Ruby, Lua, Zig, C++) / `AntdException` (C#, Kotlin, Java, PHP, Dart, Elixir uses `{:error, reason}` tuples). Rust uses `Result` with `match` on enum variants. diff --git a/llms.txt b/llms.txt index 16886a9..0237bff 100644 --- a/llms.txt +++ b/llms.txt @@ -86,6 +86,7 @@ data = client.data_get_public(result.address) | 413 | RESOURCE_EXHAUSTED | TooLargeError | Payload too large | | 500 | INTERNAL | InternalError | Server error | | 502 | UNAVAILABLE | NetworkError | Network unreachable | +| 503 | UNAVAILABLE | ServiceUnavailableError | Wallet not configured | ## Examples diff --git a/skill.md b/skill.md index 855c462..739bca9 100644 --- a/skill.md +++ b/skill.md @@ -125,6 +125,7 @@ All SDKs use the same error hierarchy. Always generate code with proper error ha | `PaymentError` / `PaymentException` | 402 | Wallet has insufficient funds | | `AlreadyExistsError` / `AlreadyExistsException` | 409 | Trying to create something that exists | | `NetworkError` / `NetworkException` | 502 | Daemon can't reach the network | +| `ServiceUnavailableError` / `ServiceUnavailableException` | 503 | Wallet not configured | | `BadRequestError` / `BadRequestException` | 400 | Invalid parameters | Python/JS/Swift use `Error` suffix. C#/Kotlin use `Exception` suffix. All inherit from a base `AntdError`/`AntdException`. From e4a7446ebda5513872a150fbf085b4ee8abb1ba0 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 09:34:55 +0100 Subject: [PATCH 11/20] Fix Swift/Kotlin binding gaps and sweep remaining 8080 references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swift fixes: - AntdClientProtocol: add paymentMode parameter to 4 upload methods - AntdClient factory: default port 8080 → 8082 - AntdGrpcClient: add missing walletApprove stub Kotlin fixes: - AntdClient factory: default port 8080 → 8082 - AntdGrpcClient: add missing walletApprove stub Port 8080 → 8082 sweep across all remaining files: - Factory classes (C# AntdClientFactory, Java AsyncAntdClient) - READMEs (all 15 SDKs + antd) - OpenAPI spec, test scripts, examples, quickstart docs - Architecture docs Zero 8080 references remain in the repo. Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/README.md | 8 ++++---- antd-cpp/examples/01-connect.cpp | 2 +- antd-csharp/Antd.Sdk.Tests/TestRunner.cs | 2 +- antd-csharp/Antd.Sdk/AntdClientFactory.cs | 4 ++-- antd-csharp/README.md | 2 +- antd-csharp/simple-test.ps1 | 4 ++-- antd-csharp/simple-test.sh | 4 ++-- antd-dart/README.md | 2 +- antd-elixir/README.md | 4 ++-- antd-java/README.md | 8 ++++---- .../src/main/java/com/autonomi/antd/AsyncAntdClient.java | 2 +- antd-js/README.md | 8 ++++---- antd-js/examples/01-connect.ts | 2 +- antd-kotlin/README.md | 2 +- .../lib/src/main/kotlin/com/autonomi/sdk/AntdClient.kt | 6 +++--- .../src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt | 4 ++++ antd-lua/README.md | 4 ++-- antd-lua/examples/01-connect.lua | 2 +- antd-lua/spec/client_spec.lua | 2 +- antd-php/README.md | 6 +++--- antd-php/examples/01-connect.php | 2 +- antd-php/tests/AntdClientTest.php | 2 +- antd-py/README.md | 4 ++-- antd-py/examples/01_connect.py | 2 +- antd-py/scripts/test_rest.py | 4 ++-- antd-py/simple-test.ps1 | 4 ++-- antd-py/simple-test.sh | 4 ++-- antd-ruby/README.md | 2 +- antd-ruby/test/test_client.rb | 2 +- antd-rust/README.md | 2 +- antd-swift/README.md | 2 +- antd-swift/Sources/AntdSdk/AntdClient.swift | 6 +++--- antd-swift/Sources/AntdSdk/AntdClientProtocol.swift | 8 ++++---- antd-swift/Sources/AntdSdk/AntdGrpcClient.swift | 9 +++++---- antd-zig/README.md | 2 +- antd/README.md | 6 +++--- antd/openapi.yaml | 2 +- docs/architecture.md | 2 +- docs/quickstart-cpp.md | 2 +- docs/quickstart-csharp.md | 2 +- docs/quickstart-dart.md | 2 +- docs/quickstart-elixir.md | 2 +- docs/quickstart-java.md | 2 +- docs/quickstart-kotlin.md | 2 +- docs/quickstart-lua.md | 2 +- docs/quickstart-php.md | 2 +- docs/quickstart-python.md | 2 +- docs/quickstart-ruby.md | 2 +- docs/quickstart-rust.md | 2 +- docs/quickstart-swift.md | 2 +- docs/quickstart-zig.md | 2 +- 51 files changed, 87 insertions(+), 82 deletions(-) diff --git a/antd-cpp/README.md b/antd-cpp/README.md index 2af621d..2c8c51e 100644 --- a/antd-cpp/README.md +++ b/antd-cpp/README.md @@ -38,7 +38,7 @@ cmake --build build #include int main() { - antd::Client client; // defaults to http://localhost:8080 + antd::Client client; // defaults to http://localhost:8082 // Check daemon health auto health = client.health(); @@ -68,7 +68,7 @@ No additional dependencies are required — only C++20 ``. #include int main() { - antd::AsyncClient client; // defaults to http://localhost:8080 + antd::AsyncClient client; // defaults to http://localhost:8082 // Fire off two requests concurrently auto health_future = client.health(); @@ -197,14 +197,14 @@ ant dev start ## Configuration ```cpp -// Default: http://localhost:8080, 5 minute timeout +// Default: http://localhost:8082, 5 minute timeout antd::Client client; // Custom URL antd::Client client("http://custom-host:9090"); // Custom URL and timeout (seconds) -antd::Client client("http://localhost:8080", 30); +antd::Client client("http://localhost:8082", 30); ``` ## API Reference diff --git a/antd-cpp/examples/01-connect.cpp b/antd-cpp/examples/01-connect.cpp index 7d69f5a..87f2b9b 100644 --- a/antd-cpp/examples/01-connect.cpp +++ b/antd-cpp/examples/01-connect.cpp @@ -3,7 +3,7 @@ int main() { try { - antd::Client client; // defaults to http://localhost:8080 + antd::Client client; // defaults to http://localhost:8082 auto health = client.health(); std::cout << "OK: " << (health.ok ? "true" : "false") << "\n"; diff --git a/antd-csharp/Antd.Sdk.Tests/TestRunner.cs b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs index 093e3cb..d566c9a 100644 --- a/antd-csharp/Antd.Sdk.Tests/TestRunner.cs +++ b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs @@ -74,7 +74,7 @@ private void Skip(string name, string detail = "") public async Task RunAllAsync() { - var endpointDesc = _transport == "grpc" ? "localhost:50051" : "localhost:8080"; + var endpointDesc = _transport == "grpc" ? "localhost:50051" : "localhost:8082"; Console.WriteLine($"\n{Bold}{Cyan}antd C# SDK - {_transport.ToUpperInvariant()} Integration Test{Reset}"); Console.WriteLine($"Target: {endpointDesc}\n"); diff --git a/antd-csharp/Antd.Sdk/AntdClientFactory.cs b/antd-csharp/Antd.Sdk/AntdClientFactory.cs index 7daa7e3..d8b2e14 100644 --- a/antd-csharp/Antd.Sdk/AntdClientFactory.cs +++ b/antd-csharp/Antd.Sdk/AntdClientFactory.cs @@ -2,7 +2,7 @@ namespace Antd.Sdk; public static class AntdClient { - public static IAntdClient CreateRest(string baseUrl = "http://localhost:8080", TimeSpan? timeout = null) + public static IAntdClient CreateRest(string baseUrl = "http://localhost:8082", TimeSpan? timeout = null) => new AntdRestClient(baseUrl, timeout); public static IAntdClient CreateGrpc(string target = "http://localhost:50051") @@ -12,7 +12,7 @@ public static IAntdClient Create(string transport = "rest", string? endpoint = n { return transport.ToLowerInvariant() switch { - "rest" => CreateRest(endpoint ?? "http://localhost:8080"), + "rest" => CreateRest(endpoint ?? "http://localhost:8082"), "grpc" => CreateGrpc(endpoint ?? "http://localhost:50051"), _ => throw new ArgumentException($"Unknown transport: {transport}. Use 'rest' or 'grpc'."), }; diff --git a/antd-csharp/README.md b/antd-csharp/README.md index 24444dd..a7fabc8 100644 --- a/antd-csharp/README.md +++ b/antd-csharp/README.md @@ -48,7 +48,7 @@ using Antd.Sdk; // REST transport (default) using var client = AntdClient.CreateRest( - baseUrl: "http://localhost:8080", + baseUrl: "http://localhost:8082", timeout: TimeSpan.FromSeconds(30) ); diff --git a/antd-csharp/simple-test.ps1 b/antd-csharp/simple-test.ps1 index 2f73b58..92532fb 100644 --- a/antd-csharp/simple-test.ps1 +++ b/antd-csharp/simple-test.ps1 @@ -1,9 +1,9 @@ #!/usr/bin/env pwsh # Run C# SDK integration tests (REST + gRPC) -# Requires a running antd daemon with REST on :8080 and gRPC on :50051 +# Requires a running antd daemon with REST on :8082 and gRPC on :50051 param( - [string]$RestEndpoint = "http://localhost:8080", + [string]$RestEndpoint = "http://localhost:8082", [string]$GrpcEndpoint = "http://localhost:50051" ) diff --git a/antd-csharp/simple-test.sh b/antd-csharp/simple-test.sh index c61c7b4..40ca647 100644 --- a/antd-csharp/simple-test.sh +++ b/antd-csharp/simple-test.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # Run C# SDK integration tests (REST + gRPC) -# Requires a running antd daemon with REST on :8080 and gRPC on :50051 +# Requires a running antd daemon with REST on :8082 and gRPC on :50051 set -euo pipefail -REST_ENDPOINT="${1:-http://localhost:8080}" +REST_ENDPOINT="${1:-http://localhost:8082}" GRPC_ENDPOINT="${2:-http://localhost:50051}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" failed=0 diff --git a/antd-dart/README.md b/antd-dart/README.md index 220eed7..20065ab 100644 --- a/antd-dart/README.md +++ b/antd-dart/README.md @@ -125,7 +125,7 @@ ant dev start ## Configuration ```dart -// Default: http://localhost:8080, 5 minute timeout +// Default: http://localhost:8082, 5 minute timeout final client = AntdClient(); // Custom URL diff --git a/antd-elixir/README.md b/antd-elixir/README.md index 53b375d..4aaa7ee 100644 --- a/antd-elixir/README.md +++ b/antd-elixir/README.md @@ -107,14 +107,14 @@ ant dev start ## Configuration ```elixir -# Default: http://localhost:8080, 5 minute timeout +# Default: http://localhost:8082, 5 minute timeout client = Antd.Client.new() # Custom URL client = Antd.Client.new("http://custom-host:9090") # Custom timeout (in milliseconds) -client = Antd.Client.new("http://localhost:8080", timeout: 30_000) +client = Antd.Client.new("http://localhost:8082", timeout: 30_000) ``` ## API Reference diff --git a/antd-java/README.md b/antd-java/README.md index 1f0e1c2..8ac5e90 100644 --- a/antd-java/README.md +++ b/antd-java/README.md @@ -68,18 +68,18 @@ ant dev start ## Configuration ```java -// Default: http://localhost:8080, 5 minute timeout +// Default: http://localhost:8082, 5 minute timeout var client = new AntdClient(); // Custom URL var client = new AntdClient("http://custom-host:9090"); // Custom URL and timeout -var client = new AntdClient("http://localhost:8080", Duration.ofSeconds(30)); +var client = new AntdClient("http://localhost:8082", Duration.ofSeconds(30)); // Custom HTTP client var httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); -var client = new AntdClient("http://localhost:8080", Duration.ofSeconds(30), httpClient); +var client = new AntdClient("http://localhost:8082", Duration.ofSeconds(30), httpClient); ``` ## API Reference @@ -171,7 +171,7 @@ The async client has the same constructors as `AntdClient`: ```java var client = new AsyncAntdClient(); // defaults var client = new AsyncAntdClient("http://custom:9090"); // custom URL -var client = new AsyncAntdClient("http://localhost:8080", Duration.ofSeconds(30)); // custom timeout +var client = new AsyncAntdClient("http://localhost:8082", Duration.ofSeconds(30)); // custom timeout ``` All 19 methods follow the naming convention `methodNameAsync()` and return `CompletableFuture` where `T` matches the sync return type. Void methods return `CompletableFuture`. diff --git a/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java index 5d83024..0543f90 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java @@ -37,7 +37,7 @@ public class AsyncAntdClient implements AutoCloseable { /** Default daemon address. */ - public static final String DEFAULT_BASE_URL = "http://localhost:8080"; + public static final String DEFAULT_BASE_URL = "http://localhost:8082"; /** Default request timeout (5 minutes). */ public static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(5); diff --git a/antd-js/README.md b/antd-js/README.md index d961106..f92f632 100644 --- a/antd-js/README.md +++ b/antd-js/README.md @@ -15,7 +15,7 @@ Requires **Node.js 18+** (uses native `fetch`). ```typescript import { createClient } from "antd"; -const client = createClient(); // default: http://localhost:8080 +const client = createClient(); // default: http://localhost:8082 // Check daemon health const status = await client.health(); @@ -36,16 +36,16 @@ import { createClient, RestClient } from "antd"; // Factory function const client = createClient(); -const client = createClient({ baseUrl: "http://remote:8080" }); +const client = createClient({ baseUrl: "http://remote:8082" }); const client = createClient({ timeout: 60_000 }); // Direct constructor -const client = new RestClient({ baseUrl: "http://localhost:8080", timeout: 300_000 }); +const client = new RestClient({ baseUrl: "http://localhost:8082", timeout: 300_000 }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| -| `baseUrl` | `string` | `"http://localhost:8080"` | antd daemon URL | +| `baseUrl` | `string` | `"http://localhost:8082"` | antd daemon URL | | `timeout` | `number` | `300000` | Request timeout (ms) | ## API Reference diff --git a/antd-js/examples/01-connect.ts b/antd-js/examples/01-connect.ts index cefb82f..3915a0c 100644 --- a/antd-js/examples/01-connect.ts +++ b/antd-js/examples/01-connect.ts @@ -1,7 +1,7 @@ /** * Example 01: Connect to antd daemon and check health. * - * Prerequisite: antd daemon running locally (default: http://localhost:8080). + * Prerequisite: antd daemon running locally (default: http://localhost:8082). */ import { createClient } from "../src/index.js"; diff --git a/antd-kotlin/README.md b/antd-kotlin/README.md index a8f4528..e861593 100644 --- a/antd-kotlin/README.md +++ b/antd-kotlin/README.md @@ -49,7 +49,7 @@ fun main() = runBlocking { ```kotlin // REST (default, recommended) -val restClient = AntdClient.createRest("http://localhost:8080") +val restClient = AntdClient.createRest("http://localhost:8082") // gRPC (higher throughput) val grpcClient = AntdClient.createGrpc("localhost:50051") diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdClient.kt index 4b69b8b..e8b89cb 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdClient.kt @@ -14,13 +14,13 @@ object AntdClient { /** * Create a REST client connecting to the antd daemon. - * @param baseUrl Base URL of the antd REST API (default: http://localhost:8080) + * @param baseUrl Base URL of the antd REST API (default: http://localhost:8082) * @param timeout Request timeout (default: 300 seconds) */ @JvmStatic @JvmOverloads fun createRest( - baseUrl: String = "http://localhost:8080", + baseUrl: String = "http://localhost:8082", timeout: Duration = Duration.ofSeconds(300), ): IAntdClient = AntdRestClient(baseUrl, timeout) @@ -41,7 +41,7 @@ object AntdClient { @JvmOverloads fun create(transport: String = "rest", endpoint: String? = null): IAntdClient = when (transport.lowercase()) { - "rest" -> createRest(endpoint ?: "http://localhost:8080") + "rest" -> createRest(endpoint ?: "http://localhost:8082") "grpc" -> createGrpc(endpoint ?: "localhost:50051") else -> throw IllegalArgumentException("Unknown transport: $transport. Use 'rest' or 'grpc'.") } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt index 3219427..b3c86d3 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt @@ -179,4 +179,8 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { override suspend fun walletBalance(): WalletBalance { throw UnsupportedOperationException("walletBalance is not yet supported via gRPC") } + + override suspend fun walletApprove(): Boolean { + throw UnsupportedOperationException("walletApprove not available via gRPC") + } } diff --git a/antd-lua/README.md b/antd-lua/README.md index 74cb645..189f7a7 100644 --- a/antd-lua/README.md +++ b/antd-lua/README.md @@ -43,7 +43,7 @@ luarocks make ```lua local antd = require("antd") --- Create a client (default: http://localhost:8080) +-- Create a client (default: http://localhost:8082) local client = antd.new_client() -- Check daemon health @@ -84,7 +84,7 @@ ant dev start ```lua local antd = require("antd") --- Default: http://localhost:8080, 300 second timeout +-- Default: http://localhost:8082, 300 second timeout local client = antd.new_client() -- Custom URL diff --git a/antd-lua/examples/01-connect.lua b/antd-lua/examples/01-connect.lua index 638c968..e1d385b 100644 --- a/antd-lua/examples/01-connect.lua +++ b/antd-lua/examples/01-connect.lua @@ -2,7 +2,7 @@ local antd = require("antd") --- Create a client with default settings (localhost:8080) +-- Create a client with default settings (localhost:8082) local client = antd.new_client() -- Check daemon health diff --git a/antd-lua/spec/client_spec.lua b/antd-lua/spec/client_spec.lua index 2aac221..f9a70bf 100644 --- a/antd-lua/spec/client_spec.lua +++ b/antd-lua/spec/client_spec.lua @@ -153,7 +153,7 @@ describe("antd client", function() before_each(function() setup_daemon() - client = antd.new_client("http://localhost:8080") + client = antd.new_client("http://localhost:8082") end) after_each(function() diff --git a/antd-php/README.md b/antd-php/README.md index 7172a46..0a0d35c 100644 --- a/antd-php/README.md +++ b/antd-php/README.md @@ -43,17 +43,17 @@ ant dev start ## Configuration ```php -// Default: http://localhost:8080, 300 second timeout +// Default: http://localhost:8082, 300 second timeout $client = new AntdClient(); // Custom URL $client = new AntdClient('http://custom-host:9090'); // Custom timeout (in seconds) -$client = new AntdClient('http://localhost:8080', 30.0); +$client = new AntdClient('http://localhost:8082', 30.0); // Custom Guzzle HTTP client -$client = new AntdClient('http://localhost:8080', 300.0, $myGuzzleClient); +$client = new AntdClient('http://localhost:8082', 300.0, $myGuzzleClient); ``` ## API Reference diff --git a/antd-php/examples/01-connect.php b/antd-php/examples/01-connect.php index e4d9314..3086122 100644 --- a/antd-php/examples/01-connect.php +++ b/antd-php/examples/01-connect.php @@ -10,7 +10,7 @@ use Autonomi\Antd\AntdClient; -$client = new AntdClient('http://localhost:8080'); +$client = new AntdClient('http://localhost:8082'); $health = $client->health(); echo "OK: " . ($health->ok ? 'true' : 'false') . "\n"; diff --git a/antd-php/tests/AntdClientTest.php b/antd-php/tests/AntdClientTest.php index 24edfc8..93ab6da 100644 --- a/antd-php/tests/AntdClientTest.php +++ b/antd-php/tests/AntdClientTest.php @@ -27,7 +27,7 @@ private function createClient(MockHandler $mock): AntdClient { $handlerStack = HandlerStack::create($mock); $httpClient = new Client(['handler' => $handlerStack]); - return new AntdClient('http://localhost:8080', 300.0, $httpClient); + return new AntdClient('http://localhost:8082', 300.0, $httpClient); } private function jsonResponse(int $status, array $body): Response diff --git a/antd-py/README.md b/antd-py/README.md index 16021e7..34ba18b 100644 --- a/antd-py/README.md +++ b/antd-py/README.md @@ -23,7 +23,7 @@ pip install -e ".[all]" ```python from antd import AntdClient -client = AntdClient() # REST transport, localhost:8080 +client = AntdClient() # REST transport, localhost:8082 # Health check status = client.health() @@ -43,7 +43,7 @@ print(data.decode()) # "Hello, Autonomi!" from antd import AntdClient, AsyncAntdClient # REST (default) -client = AntdClient(transport="rest", base_url="http://localhost:8080", timeout=30) +client = AntdClient(transport="rest", base_url="http://localhost:8082", timeout=30) # gRPC client = AntdClient(transport="grpc", target="localhost:50051") diff --git a/antd-py/examples/01_connect.py b/antd-py/examples/01_connect.py index 6b24b89..08c036d 100644 --- a/antd-py/examples/01_connect.py +++ b/antd-py/examples/01_connect.py @@ -1,6 +1,6 @@ """Example 01: Connect to antd daemon and check health. -Prerequisite: antd daemon running locally (default: http://localhost:8080). +Prerequisite: antd daemon running locally (default: http://localhost:8082). """ from antd import AntdClient diff --git a/antd-py/scripts/test_rest.py b/antd-py/scripts/test_rest.py index abd7a12..ed07174 100644 --- a/antd-py/scripts/test_rest.py +++ b/antd-py/scripts/test_rest.py @@ -2,7 +2,7 @@ """REST integration test for antd Python SDK. Mirrors simple-test.ps1 -- standalone script with colored pass/fail output. -Requires a running antd daemon on localhost:8080. +Requires a running antd daemon on localhost:8082. Usage: python scripts/test_rest.py """ @@ -70,7 +70,7 @@ def test_skip(name: str, detail: str = ""): def main(): - base_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8080" + base_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8082" print(f"\n{BOLD}{CYAN}antd Python SDK - REST Integration Test{RESET}") print(f"Target: {base_url}\n") diff --git a/antd-py/simple-test.ps1 b/antd-py/simple-test.ps1 index b0f0a2d..24fa5e9 100644 --- a/antd-py/simple-test.ps1 +++ b/antd-py/simple-test.ps1 @@ -1,9 +1,9 @@ #!/usr/bin/env pwsh # Run Python SDK integration tests (REST + gRPC) -# Requires a running antd daemon with REST on :8080 and gRPC on :50051 +# Requires a running antd daemon with REST on :8082 and gRPC on :50051 param( - [string]$RestUrl = "http://localhost:8080", + [string]$RestUrl = "http://localhost:8082", [string]$GrpcTarget = "localhost:50051" ) diff --git a/antd-py/simple-test.sh b/antd-py/simple-test.sh index 39c4094..54dc76f 100644 --- a/antd-py/simple-test.sh +++ b/antd-py/simple-test.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # Run Python SDK integration tests (REST + gRPC) -# Requires a running antd daemon with REST on :8080 and gRPC on :50051 +# Requires a running antd daemon with REST on :8082 and gRPC on :50051 set -euo pipefail -REST_URL="${1:-http://localhost:8080}" +REST_URL="${1:-http://localhost:8082}" GRPC_TARGET="${2:-localhost:50051}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" failed=0 diff --git a/antd-ruby/README.md b/antd-ruby/README.md index 79bed6d..f484b00 100644 --- a/antd-ruby/README.md +++ b/antd-ruby/README.md @@ -96,7 +96,7 @@ ant dev start ## Configuration ```ruby -# Default: http://localhost:8080, 300 second timeout +# Default: http://localhost:8082, 300 second timeout client = Antd::Client.new # Custom URL diff --git a/antd-ruby/test/test_client.rb b/antd-ruby/test/test_client.rb index d371c8c..fcd7d2e 100644 --- a/antd-ruby/test/test_client.rb +++ b/antd-ruby/test/test_client.rb @@ -4,7 +4,7 @@ require "base64" class TestClient < Minitest::Test - BASE = "http://localhost:8080" + BASE = "http://localhost:8082" def setup @client = Antd::Client.new(base_url: BASE) diff --git a/antd-rust/README.md b/antd-rust/README.md index a02b9c8..7d94261 100644 --- a/antd-rust/README.md +++ b/antd-rust/README.md @@ -53,7 +53,7 @@ ant dev start use antd_client::{Client, DEFAULT_BASE_URL}; use std::time::Duration; -// Default: http://localhost:8080, 5 minute timeout +// Default: http://localhost:8082, 5 minute timeout let client = Client::new(DEFAULT_BASE_URL); // Custom URL diff --git a/antd-swift/README.md b/antd-swift/README.md index d665c2f..8907e3a 100644 --- a/antd-swift/README.md +++ b/antd-swift/README.md @@ -47,7 +47,7 @@ print(String(data: data, encoding: .utf8)!) // "Hello, Autonomi!" ```swift // REST (default, recommended) -let restClient = AntdClient.createRest(baseURL: "http://localhost:8080") +let restClient = AntdClient.createRest(baseURL: "http://localhost:8082") // gRPC (requires generated proto stubs) let grpcClient = AntdClient.createGrpc(target: "localhost:50051") diff --git a/antd-swift/Sources/AntdSdk/AntdClient.swift b/antd-swift/Sources/AntdSdk/AntdClient.swift index 14f3ef0..b5d0a10 100644 --- a/antd-swift/Sources/AntdSdk/AntdClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdClient.swift @@ -10,10 +10,10 @@ public enum AntdClient { /// Create a REST client connecting to the antd daemon. /// - Parameters: - /// - baseURL: Base URL of the antd REST API (default: http://localhost:8080) + /// - baseURL: Base URL of the antd REST API (default: http://localhost:8082) /// - timeout: Request timeout in seconds (default: 300) public static func createRest( - baseURL: String = "http://localhost:8080", + baseURL: String = "http://localhost:8082", timeout: TimeInterval = 300 ) -> AntdClientProtocol { AntdRestClient(baseURL: baseURL, timeout: timeout) @@ -32,7 +32,7 @@ public enum AntdClient { public static func create(transport: String = "rest", endpoint: String? = nil) -> AntdClientProtocol { switch transport.lowercased() { case "rest": - return createRest(baseURL: endpoint ?? "http://localhost:8080") + return createRest(baseURL: endpoint ?? "http://localhost:8082") case "grpc": return createGrpc(target: endpoint ?? "localhost:50051") default: diff --git a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift index 6b232cb..493638d 100644 --- a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift +++ b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift @@ -10,9 +10,9 @@ public protocol AntdClientProtocol: Sendable { func health() async throws -> HealthStatus // Data - func dataPutPublic(_ data: Data) async throws -> PutResult + func dataPutPublic(_ data: Data, paymentMode: String?) async throws -> PutResult func dataGetPublic(address: String) async throws -> Data - func dataPutPrivate(_ data: Data) async throws -> PutResult + func dataPutPrivate(_ data: Data, paymentMode: String?) async throws -> PutResult func dataGetPrivate(dataMap: String) async throws -> Data func dataCost(_ data: Data) async throws -> String @@ -27,9 +27,9 @@ public protocol AntdClientProtocol: Sendable { func graphEntryCost(publicKey: String) async throws -> String // Files - func fileUploadPublic(path: String) async throws -> PutResult + func fileUploadPublic(path: String, paymentMode: String?) async throws -> PutResult func fileDownloadPublic(address: String, destPath: String) async throws - func dirUploadPublic(path: String) async throws -> PutResult + func dirUploadPublic(path: String, paymentMode: String?) async throws -> PutResult func dirDownloadPublic(address: String, destPath: String) async throws func archiveGetPublic(address: String) async throws -> Archive func archivePutPublic(archive: Archive) async throws -> PutResult diff --git a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift index c04f1a4..c6c5fae 100644 --- a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift @@ -25,9 +25,9 @@ public final class AntdGrpcClient: AntdClientProtocol, @unchecked Sendable { } public func health() async throws -> HealthStatus { throw notImplemented() } - public func dataPutPublic(_ data: Data) async throws -> PutResult { throw notImplemented() } + public func dataPutPublic(_ data: Data, paymentMode: String? = nil) async throws -> PutResult { throw notImplemented() } public func dataGetPublic(address: String) async throws -> Data { throw notImplemented() } - public func dataPutPrivate(_ data: Data) async throws -> PutResult { throw notImplemented() } + public func dataPutPrivate(_ data: Data, paymentMode: String? = nil) async throws -> PutResult { throw notImplemented() } public func dataGetPrivate(dataMap: String) async throws -> Data { throw notImplemented() } public func dataCost(_ data: Data) async throws -> String { throw notImplemented() } public func chunkPut(_ data: Data) async throws -> PutResult { throw notImplemented() } @@ -36,13 +36,14 @@ public final class AntdGrpcClient: AntdClientProtocol, @unchecked Sendable { public func graphEntryGet(address: String) async throws -> GraphEntry { throw notImplemented() } public func graphEntryExists(address: String) async throws -> Bool { throw notImplemented() } public func graphEntryCost(publicKey: String) async throws -> String { throw notImplemented() } - public func fileUploadPublic(path: String) async throws -> PutResult { throw notImplemented() } + public func fileUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { throw notImplemented() } public func fileDownloadPublic(address: String, destPath: String) async throws { throw notImplemented() } - public func dirUploadPublic(path: String) async throws -> PutResult { throw notImplemented() } + public func dirUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { throw notImplemented() } public func dirDownloadPublic(address: String, destPath: String) async throws { throw notImplemented() } public func archiveGetPublic(address: String) async throws -> Archive { throw notImplemented() } public func archivePutPublic(archive: Archive) async throws -> PutResult { throw notImplemented() } public func fileCost(path: String, isPublic: Bool = true, includeArchive: Bool = false) async throws -> String { throw notImplemented() } public func walletAddress() async throws -> WalletAddress { throw notImplemented() } public func walletBalance() async throws -> WalletBalance { throw notImplemented() } + public func walletApprove() async throws -> Bool { throw notImplemented() } } diff --git a/antd-zig/README.md b/antd-zig/README.md index 8d7aee6..4db0e56 100644 --- a/antd-zig/README.md +++ b/antd-zig/README.md @@ -73,7 +73,7 @@ ant dev start ## Configuration ```zig -// Default: http://localhost:8080 +// Default: http://localhost:8082 var client = antd.Client.init(allocator, antd.default_base_url); defer client.deinit(); diff --git a/antd/README.md b/antd/README.md index 7b7aea7..59a09d9 100644 --- a/antd/README.md +++ b/antd/README.md @@ -21,7 +21,7 @@ AUTONOMI_WALLET_KEY="your_key" ANT_PEERS="/ip4/..." cargo run -- --network local # With all options cargo run -- \ - --rest-addr 0.0.0.0:8080 \ + --rest-addr 0.0.0.0:8082 \ --grpc-addr 0.0.0.0:50051 \ --network local \ --peers "/ip4/127.0.0.1/udp/..." \ @@ -36,7 +36,7 @@ All options can be set via CLI flags or environment variables: | Flag | Env Var | Default | Description | |------|---------|---------|-------------| -| `--rest-addr` | `ANTD_REST_ADDR` | `0.0.0.0:8080` | REST API listen address | +| `--rest-addr` | `ANTD_REST_ADDR` | `0.0.0.0:8082` | REST API listen address | | `--grpc-addr` | `ANTD_GRPC_ADDR` | `0.0.0.0:50051` | gRPC listen address | | `--network` | `ANTD_NETWORK` | `default` | Network mode: `default`, `local`, `alpha` | | `--peers` | `ANTD_PEERS` | *(none)* | Comma-separated bootstrap peer multiaddrs | @@ -57,7 +57,7 @@ Additional environment variables consumed by the underlying Autonomi client: ## API Endpoints -### REST API (default: `http://localhost:8080`) +### REST API (default: `http://localhost:8082`) | Method | Endpoint | Description | |--------|----------|-------------| diff --git a/antd/openapi.yaml b/antd/openapi.yaml index f550f4e..3c0a5d9 100644 --- a/antd/openapi.yaml +++ b/antd/openapi.yaml @@ -5,7 +5,7 @@ info: version: 1.0.0 servers: - - url: http://localhost:8080 + - url: http://localhost:8082 description: Local antd daemon tags: diff --git a/docs/architecture.md b/docs/architecture.md index 1b8cc53..899d672 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -20,7 +20,7 @@ Your Application ▼ ┌───────┐ │ antd │ Local daemon process - │ │ REST API (:8080) + gRPC (:50051) + │ │ REST API (:8082) + gRPC (:50051) └───┬───┘ │ │ Autonomi client library (Rust) diff --git a/docs/quickstart-cpp.md b/docs/quickstart-cpp.md index 4f63854..041838d 100644 --- a/docs/quickstart-cpp.md +++ b/docs/quickstart-cpp.md @@ -45,7 +45,7 @@ int main() { // Custom endpoint auto client2 = antd::Client::builder() .transport("rest") - .base_url("http://localhost:8080") + .base_url("http://localhost:8082") .timeout(std::chrono::seconds(30)) .build(); diff --git a/docs/quickstart-csharp.md b/docs/quickstart-csharp.md index 9dafca0..713149e 100644 --- a/docs/quickstart-csharp.md +++ b/docs/quickstart-csharp.md @@ -38,7 +38,7 @@ using var client = AntdClient.CreateRest(); // Custom endpoint using var client2 = AntdClient.CreateRest( - baseUrl: "http://localhost:8080", + baseUrl: "http://localhost:8082", timeout: TimeSpan.FromSeconds(30) ); diff --git a/docs/quickstart-dart.md b/docs/quickstart-dart.md index 696f60b..ae4a8b7 100644 --- a/docs/quickstart-dart.md +++ b/docs/quickstart-dart.md @@ -25,7 +25,7 @@ import 'package:antd/antd.dart'; final client = AntdClient(); // Custom endpoint -final client2 = AntdClient(transport: 'rest', baseUrl: 'http://localhost:8080'); +final client2 = AntdClient(transport: 'rest', baseUrl: 'http://localhost:8082'); // gRPC transport final grpcClient = AntdClient(transport: 'grpc', target: 'localhost:50051'); diff --git a/docs/quickstart-elixir.md b/docs/quickstart-elixir.md index 7ab9b57..48fc861 100644 --- a/docs/quickstart-elixir.md +++ b/docs/quickstart-elixir.md @@ -28,7 +28,7 @@ ant dev start {:ok, client} = Antd.Client.new() # Custom endpoint -{:ok, client} = Antd.Client.new(transport: :rest, base_url: "http://localhost:8080") +{:ok, client} = Antd.Client.new(transport: :rest, base_url: "http://localhost:8082") # gRPC transport {:ok, client} = Antd.Client.new(transport: :grpc, target: "localhost:50051") diff --git a/docs/quickstart-java.md b/docs/quickstart-java.md index c02ae4d..5d01304 100644 --- a/docs/quickstart-java.md +++ b/docs/quickstart-java.md @@ -47,7 +47,7 @@ try (var client = AntdClient.create()) { // Custom endpoint try (var client = AntdClient.builder() .transport("rest") - .baseUrl("http://localhost:8080") + .baseUrl("http://localhost:8082") .timeout(Duration.ofSeconds(30)) .build()) { // use client diff --git a/docs/quickstart-kotlin.md b/docs/quickstart-kotlin.md index 8ce1e42..04629a3 100644 --- a/docs/quickstart-kotlin.md +++ b/docs/quickstart-kotlin.md @@ -39,7 +39,7 @@ val client = AntdClient.createRest() // Custom endpoint val client2 = AntdClient.createRest( - baseUrl = "http://localhost:8080", + baseUrl = "http://localhost:8082", timeout = java.time.Duration.ofSeconds(30), ) diff --git a/docs/quickstart-lua.md b/docs/quickstart-lua.md index 09e3c53..8990366 100644 --- a/docs/quickstart-lua.md +++ b/docs/quickstart-lua.md @@ -23,7 +23,7 @@ local client = antd.new_client() -- Custom endpoint local client = antd.new_client({ transport = "rest", - base_url = "http://localhost:8080", + base_url = "http://localhost:8082", }) -- gRPC transport diff --git a/docs/quickstart-php.md b/docs/quickstart-php.md index 5f9d837..8a69481 100644 --- a/docs/quickstart-php.md +++ b/docs/quickstart-php.md @@ -23,7 +23,7 @@ use Autonomi\Antd\AntdClient; $client = new AntdClient(); // Custom endpoint -$client = new AntdClient(transport: 'rest', baseUrl: 'http://localhost:8080'); +$client = new AntdClient(transport: 'rest', baseUrl: 'http://localhost:8082'); // gRPC transport $client = new AntdClient(transport: 'grpc', target: 'localhost:50051'); diff --git a/docs/quickstart-python.md b/docs/quickstart-python.md index 9d86ee5..69f026b 100644 --- a/docs/quickstart-python.md +++ b/docs/quickstart-python.md @@ -25,7 +25,7 @@ client = AntdClient() aclient = AsyncAntdClient() # Custom endpoint -client = AntdClient(transport="rest", base_url="http://localhost:8080") +client = AntdClient(transport="rest", base_url="http://localhost:8082") # gRPC transport client = AntdClient(transport="grpc", target="localhost:50051") diff --git a/docs/quickstart-ruby.md b/docs/quickstart-ruby.md index f383862..e0e7298 100644 --- a/docs/quickstart-ruby.md +++ b/docs/quickstart-ruby.md @@ -24,7 +24,7 @@ require 'antd' client = Antd::Client.new # Custom endpoint -client = Antd::Client.new(transport: :rest, base_url: "http://localhost:8080") +client = Antd::Client.new(transport: :rest, base_url: "http://localhost:8082") # gRPC transport client = Antd::Client.new(transport: :grpc, target: "localhost:50051") diff --git a/docs/quickstart-rust.md b/docs/quickstart-rust.md index 32c4adb..44b4a24 100644 --- a/docs/quickstart-rust.md +++ b/docs/quickstart-rust.md @@ -33,7 +33,7 @@ async fn main() -> Result<(), antd_client::AntdError> { // Custom endpoint let client = Client::builder() .transport("rest") - .base_url("http://localhost:8080") + .base_url("http://localhost:8082") .timeout(std::time::Duration::from_secs(30)) .build()?; diff --git a/docs/quickstart-swift.md b/docs/quickstart-swift.md index 3102023..fe909ea 100644 --- a/docs/quickstart-swift.md +++ b/docs/quickstart-swift.md @@ -49,7 +49,7 @@ let client = try AntdClient.createRest() // Custom endpoint let client2 = try AntdClient.createRest( - baseURL: URL(string: "http://localhost:8080")!, + baseURL: URL(string: "http://localhost:8082")!, timeout: 30 ) diff --git a/docs/quickstart-zig.md b/docs/quickstart-zig.md index 06e2edf..dff02fe 100644 --- a/docs/quickstart-zig.md +++ b/docs/quickstart-zig.md @@ -54,7 +54,7 @@ pub fn main() !void { // Custom endpoint var client2 = try antd.Client.init(allocator, .{ .transport = .rest, - .base_url = "http://localhost:8080", + .base_url = "http://localhost:8082", }); defer client2.deinit(); From a0a264b6d7ef221df0fc672ebc9fcba3a5a6b3b0 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 10:13:23 +0100 Subject: [PATCH 12/20] Rewrite FFI bindings from v1 autonomi crate to v2 ant-core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FFI was depending on the v1 autonomi crate (from maidsafe/autonomi) which has a completely different API, payment model, and type system from the v2 ant-core used by antd and all REST/gRPC SDKs. Fresh rewrite targeting ant-core: - client.rs: Client with connect(), connect_local(), connect_with_wallet() constructors. chunk_put/get, data_put/get (public+private), file_upload/download, wallet_approve — all using ant-core Client. - wallet.rs: Wallet with from_private_key(), address(), balance methods. - data.rs: PaymentMode parsing helpers shared with client. - lib.rs: UniFFI result types and error mapping from ant-core errors. Removed v1 modules (no equivalent in ant-core yet): - keys.rs, key_derivation.rs (BLS key operations) - graph.rs (graph entries not in ant-core) - files.rs (archive types not in ant-core) - network.rs, payment.rs (replaced by simpler wallet.rs) Payment model simplified: v1 required PaymentOption on every write call. v2 configures wallet at init time, handles payment internally, and supports payment_mode (auto/merkle/single). Co-Authored-By: Claude Opus 4.6 (1M context) --- ffi/rust/Cargo.lock | 2941 ++++++++++++++---------- ffi/rust/ant-ffi/Cargo.toml | 10 +- ffi/rust/ant-ffi/src/client.rs | 691 ++---- ffi/rust/ant-ffi/src/data.rs | 183 +- ffi/rust/ant-ffi/src/files.rs | 263 --- ffi/rust/ant-ffi/src/graph.rs | 128 -- ffi/rust/ant-ffi/src/key_derivation.rs | 237 -- ffi/rust/ant-ffi/src/keys.rs | 63 - ffi/rust/ant-ffi/src/lib.rs | 146 +- ffi/rust/ant-ffi/src/network.rs | 40 - ffi/rust/ant-ffi/src/payment.rs | 46 - ffi/rust/ant-ffi/src/wallet.rs | 64 + 12 files changed, 2072 insertions(+), 2740 deletions(-) delete mode 100644 ffi/rust/ant-ffi/src/files.rs delete mode 100644 ffi/rust/ant-ffi/src/graph.rs delete mode 100644 ffi/rust/ant-ffi/src/key_derivation.rs delete mode 100644 ffi/rust/ant-ffi/src/keys.rs delete mode 100644 ffi/rust/ant-ffi/src/network.rs delete mode 100644 ffi/rust/ant-ffi/src/payment.rs create mode 100644 ffi/rust/ant-ffi/src/wallet.rs diff --git a/ffi/rust/Cargo.lock b/ffi/rust/Cargo.lock index 6f613e4..1dc3da5 100644 --- a/ffi/rust/Cargo.lock +++ b/ffi/rust/Cargo.lock @@ -53,15 +53,18 @@ dependencies = [ ] [[package]] -name = "ahash" -version = "0.8.12" +name = "aes-gcm-siv" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", ] [[package]] @@ -470,7 +473,7 @@ dependencies = [ "lru", "parking_lot", "pin-project", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.18", @@ -514,7 +517,7 @@ dependencies = [ "alloy-transport-http", "futures", "pin-project", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "tokio", @@ -646,7 +649,7 @@ dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", - "heck 0.5.0", + "heck", "indexmap 2.13.0", "proc-macro-error2", "proc-macro2", @@ -665,7 +668,7 @@ dependencies = [ "alloy-json-abi", "const-hex", "dunce", - "heck 0.5.0", + "heck", "macro-string", "proc-macro2", "quote", @@ -728,7 +731,7 @@ dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest", + "reqwest 0.12.28", "serde_json", "tower", "tracing", @@ -824,40 +827,53 @@ dependencies = [ ] [[package]] -name = "ant-bootstrap" -version = "0.2.13" +name = "ant-core" +version = "0.1.1" +source = "git+https://github.com/WithAutonomi/ant-client#c63f546af44ec4e1d749c5f5d93945eb6378d5be" dependencies = [ - "ant-logging", - "ant-protocol", - "atomic-write-file", - "chrono", - "clap", - "custom_debug", - "dirs-next", + "ant-evm", + "ant-node", + "async-stream", + "axum", + "bytes", + "evmlib", + "flate2", + "fs2", "futures", + "futures-core", + "futures-util", + "hex", + "libc", "libp2p", + "lru", + "multihash", + "postcard", "rand 0.8.5", - "reqwest", + "reqwest 0.12.28", + "rmp-serde", + "saorsa-pqc 0.5.0", + "self_encryption", "serde", "serde_json", - "thiserror 1.0.69", + "tar", + "thiserror 2.0.18", "tokio", + "tokio-util", + "toml 0.8.23", + "tower-http", "tracing", - "url", -] - -[[package]] -name = "ant-build-info" -version = "0.1.29" -dependencies = [ - "chrono", - "tracing", - "vergen", + "tracing-subscriber", + "utoipa", + "windows-sys 0.61.2", + "xor_name", + "zip", ] [[package]] name = "ant-evm" version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a83cf15203b24ca3fd13f9d481a3d15ca1c384032095f20d5069d8f9bc16afb" dependencies = [ "ant-merkle", "custom_debug", @@ -878,34 +894,18 @@ dependencies = [ [[package]] name = "ant-ffi" -version = "0.1.0" +version = "0.2.0" dependencies = [ - "autonomi", - "blsttc", + "ant-core", "bytes", + "evmlib", "hex", - "libp2p", - "thiserror 1.0.69", + "rmp-serde", + "thiserror 2.0.18", "tokio", "uniffi", ] -[[package]] -name = "ant-logging" -version = "0.3.0" -dependencies = [ - "chrono", - "dirs-next", - "file-rotate", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", - "tracing-appender", - "tracing-core", - "tracing-subscriber", -] - [[package]] name = "ant-merkle" version = "1.5.1" @@ -916,32 +916,55 @@ dependencies = [ ] [[package]] -name = "ant-protocol" -version = "1.0.13" +name = "ant-node" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9161d53c72cfcc0dd8fee14bff64c0bad06642f074d52c5c91d9ee203acb4042" dependencies = [ - "ant-build-info", + "aes-gcm-siv", "ant-evm", - "ant-merkle", - "blake2", - "blsttc", + "blake3", "bytes", + "chrono", + "clap", "color-eyre", - "crdts", - "custom_debug", - "dirs-next", + "directories", + "evmlib", + "flate2", + "fs2", + "futures", + "heed", "hex", + "hkdf", "libp2p", - "prometheus-client", + "lru", + "multihash", + "objc2", + "objc2-foundation", + "parking_lot", + "postcard", "rand 0.8.5", + "reqwest 0.13.2", "rmp-serde", + "saorsa-core", + "saorsa-pqc 0.5.0", + "self-replace", + "self_encryption", + "semver 1.0.27", "serde", "serde_json", "sha2", - "thiserror 1.0.69", - "tiny-keccak", - "tonic-build", + "tar", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "toml 0.8.23", "tracing", + "tracing-appender", + "tracing-subscriber", "xor_name", + "zip", ] [[package]] @@ -950,6 +973,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -1235,18 +1267,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-compat" version = "0.2.5" @@ -1260,24 +1280,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -1325,32 +1327,19 @@ dependencies = [ ] [[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atomic-write-file" -version = "0.2.3" +name = "atomic-polyfill" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb1e2c1d58618bea806ccca5bbe65dc4e868be16f69ff118a39049389687548" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" dependencies = [ - "nix 0.29.0", - "rand 0.8.5", + "critical-section", ] [[package]] -name = "attohttpc" -version = "0.30.1" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64", - "http", - "log", - "url", -] +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" @@ -1370,38 +1359,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "autonomi" -version = "0.10.1" +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ - "ant-bootstrap", - "ant-evm", - "ant-protocol", - "bip39", - "blst", - "blstrs 0.7.1", - "blsttc", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", "bytes", - "const-hex", - "custom_debug", - "dirs-next", - "evmlib", - "exponential-backoff", - "eyre", - "futures", - "hex", - "libp2p", - "rand 0.8.5", - "rayon", - "rmp-serde", - "self_encryption 0.30.0", - "self_encryption 0.34.3", - "serde", - "sha2", - "thiserror 1.0.69", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", "tracing", - "walkdir", - "xor_name", ] [[package]] @@ -1471,17 +1499,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bip39" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" -dependencies = [ - "bitcoin_hashes", - "serde", - "unicode-normalization", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1518,6 +1535,9 @@ name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] [[package]] name = "bitvec" @@ -1532,12 +1552,17 @@ dependencies = [ ] [[package]] -name = "blake2" -version = "0.10.6" +name = "blake3" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" dependencies = [ - "digest 0.10.7", + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures", ] [[package]] @@ -1550,12 +1575,12 @@ dependencies = [ ] [[package]] -name = "block-padding" -version = "0.3.3" +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "generic-array", + "objc2", ] [[package]] @@ -1570,59 +1595,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "blstrs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff3694b352ece02eb664a09ffb948ee69b35afa2e6ac444a6b8cb9d515deebd" -dependencies = [ - "blst", - "byte-slice-cast", - "ff 0.12.1", - "group 0.12.1", - "pairing 0.22.0", - "rand_core 0.6.4", - "serde", - "subtle", -] - -[[package]] -name = "blstrs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8a8ed6fefbeef4a8c7b460e4110e12c5e22a5b7cf32621aae6ad650c4dcf29" -dependencies = [ - "blst", - "byte-slice-cast", - "ff 0.13.1", - "group 0.13.0", - "pairing 0.23.0", - "rand_core 0.6.4", - "serde", - "subtle", -] - -[[package]] -name = "blsttc" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1186a39763321a0b73d1a10aa4fc067c5d042308509e8f6cc31d2c2a7ac61ac2" -dependencies = [ - "blst", - "blstrs 0.6.2", - "ff 0.12.1", - "group 0.12.1", - "hex", - "hex_fmt", - "pairing 0.22.0", - "rand 0.8.5", - "rand_chacha 0.3.1", - "serde", - "thiserror 1.0.69", - "tiny-keccak", - "zeroize", -] - [[package]] name = "borsh" version = "1.6.0" @@ -1688,6 +1660,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -1703,6 +1681,25 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "c-kzg" version = "2.1.6" @@ -1750,24 +1747,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cbor4ii" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "472931dd4dfcc785075b09be910147f9c6258883fc4591d0dac6116392b2daa6" -dependencies = [ - "serde", -] - [[package]] name = "cc" version = "1.2.56" @@ -1775,9 +1754,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -1867,7 +1854,7 @@ version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -1879,6 +1866,24 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -1913,12 +1918,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "crossbeam-utils", + "bytes", + "memchr", ] [[package]] @@ -1965,6 +1971,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "convert_case" version = "0.10.0" @@ -1984,6 +2002,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2032,16 +2060,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crdts" -version = "7.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387808c885b79055facbd4b2e806a683fe1bc37abc7dfa5fea1974ad2d4137b0" -dependencies = [ - "serde", - "tiny-keccak", -] - [[package]] name = "critical-section" version = "1.2.0" @@ -2076,6 +2094,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -2280,6 +2307,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + [[package]] name = "der" version = "0.7.10" @@ -2325,6 +2358,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -2370,24 +2414,64 @@ dependencies = [ ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "directories" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys 0.4.1", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", - "redox_users", - "winapi", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", ] [[package]] @@ -2401,6 +2485,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + [[package]] name = "dtoa" version = "1.0.11" @@ -2452,6 +2545,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -2488,9 +2582,10 @@ dependencies = [ "base16ct", "crypto-bigint", "digest 0.10.7", - "ff 0.13.1", + "ff", "generic-array", - "group 0.13.0", + "group", + "hkdf", "pkcs8", "rand_core 0.6.4", "sec1", @@ -2500,15 +2595,24 @@ dependencies = [ ] [[package]] -name = "enum-as-inner" +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", + "cfg-if", ] [[package]] @@ -2547,30 +2651,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "evmlib" version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0830ac5a8d0e13e2275782c6375464ea49f255c0b59ccba4ca11f8fb7d67cea5" dependencies = [ "alloy", "exponential-backoff", @@ -2630,24 +2715,12 @@ dependencies = [ "bytes", ] -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "bitvec", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "ff" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -2659,13 +2732,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "file-rotate" -version = "0.7.6" +name = "filetime" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3ed82142801f5b1363f7d463963d114db80f467e860b1cd82228eaebc627a0" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ - "chrono", - "flate2", + "cfg-if", + "libc", + "libredox", ] [[package]] @@ -2674,6 +2748,42 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fips203" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8bdb6454f692ca2a2b45cd554c6828c639d7f9c968cf83a678899ec4443a280" +dependencies = [ + "rand_core 0.6.4", + "sha3", + "subtle", + "zeroize", +] + +[[package]] +name = "fips204" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fb5a367b9846933e271a3c2a992930743f82ae5e8cb7faa780715a80fa0b15" +dependencies = [ + "rand_core 0.6.4", + "sha2", + "sha3", + "zeroize", +] + +[[package]] +name = "fips205" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5626bf5534df4ebdbd2536465d7eaa8a9dc2cdeb7e036e0ecf291dcc80ffb6" +dependencies = [ + "rand_core 0.6.4", + "sha2", + "sha3", + "zeroize", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2686,12 +2796,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.1.9" @@ -2720,6 +2824,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2738,6 +2857,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2802,16 +2937,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.32" @@ -2823,17 +2948,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "futures-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" -dependencies = [ - "futures-io", - "rustls", - "rustls-pki-types", -] - [[package]] name = "futures-sink" version = "0.3.32" @@ -2959,29 +3073,14 @@ dependencies = [ "scroll", ] -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand 0.8.5", - "rand_core 0.6.4", - "rand_xorshift 0.3.0", - "subtle", -] - [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.1", - "rand 0.8.5", + "ff", "rand_core 0.6.4", - "rand_xorshift 0.3.0", "subtle", ] @@ -3005,8 +3104,17 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" @@ -3015,9 +3123,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -3041,15 +3146,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.10.0" @@ -3060,12 +3156,17 @@ dependencies = [ ] [[package]] -name = "heck" -version = "0.3.3" +name = "heapless" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ - "unicode-segmentation", + "atomic-polyfill", + "hash32", + "rustc_version 0.4.1", + "serde", + "spin", + "stable_deref_trait", ] [[package]] @@ -3075,77 +3176,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hermit-abi" -version = "0.5.2" +name = "heed" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "6a56c94661ddfb51aa9cdfbf102cfcc340aa69267f95ebccc4af08d7c530d393" +dependencies = [ + "bitflags", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] [[package]] -name = "hex" -version = "0.4.3" +name = "heed-traits" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" [[package]] -name = "hex-conservative" -version = "0.2.2" +name = "heed-types" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" dependencies = [ - "arrayvec", + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", ] [[package]] -name = "hex_fmt" -version = "0.3.0" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "socket2 0.5.10", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hickory-resolver" -version = "0.25.2" +name = "hex-conservative" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.2", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", + "arrayvec", ] [[package]] @@ -3167,12 +3253,24 @@ dependencies = [ ] [[package]] -name = "home" -version = "0.5.12" +name = "hpke" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "f65d16b699dd1a1fa2d851c970b0c971b388eeeb40f744252b8de48860980c8f" dependencies = [ - "windows-sys 0.61.2", + "aead", + "aes-gcm", + "chacha20poly1305", + "digest 0.10.7", + "generic-array", + "hkdf", + "hmac", + "p256", + "rand_core 0.9.5", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", ] [[package]] @@ -3214,6 +3312,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -3228,6 +3332,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -3250,7 +3355,23 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -3271,9 +3392,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -3288,7 +3411,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -3414,60 +3537,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "if-addrs" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "if-watch" -version = "3.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" -dependencies = [ - "async-io", - "core-foundation", - "fnv", - "futures", - "if-addrs", - "ipnet", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-proto", - "netlink-sys", - "rtnetlink", - "system-configuration", - "tokio", - "windows", -] - -[[package]] -name = "igd-next" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" -dependencies = [ - "async-trait", - "attohttpc", - "bytes", - "futures", - "http", - "http-body-util", - "hyper", - "hyper-util", - "log", - "rand 0.9.2", - "tokio", - "url", - "xmltree", -] - [[package]] name = "impl-codec" version = "0.6.0" @@ -3523,22 +3592,9 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "block-padding", "generic-array", ] -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -3594,6 +3650,60 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -3637,6 +3747,16 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "zeroize", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3673,25 +3793,13 @@ dependencies = [ "futures-timer", "getrandom 0.2.17", "libp2p-allow-block-list", - "libp2p-autonat", "libp2p-connection-limits", "libp2p-core", - "libp2p-dns", - "libp2p-gossipsub", "libp2p-identify", "libp2p-identity", "libp2p-kad", - "libp2p-mdns", "libp2p-metrics", - "libp2p-noise", - "libp2p-quic", - "libp2p-relay", - "libp2p-request-response", "libp2p-swarm", - "libp2p-tcp", - "libp2p-upnp", - "libp2p-websocket", - "libp2p-yamux", "multiaddr", "pin-project", "rw-stream-sink", @@ -3709,31 +3817,6 @@ dependencies = [ "libp2p-swarm", ] -[[package]] -name = "libp2p-autonat" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fab5e25c49a7d48dac83d95d8f3bac0a290d8a5df717012f6e34ce9886396c0b" -dependencies = [ - "async-trait", - "asynchronous-codec", - "either", - "futures", - "futures-bounded", - "futures-timer", - "libp2p-core", - "libp2p-identity", - "libp2p-request-response", - "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", - "rand 0.8.5", - "rand_core 0.6.4", - "thiserror 2.0.18", - "tracing", - "web-time", -] - [[package]] name = "libp2p-connection-limits" version = "0.6.0" @@ -3771,70 +3854,23 @@ dependencies = [ ] [[package]] -name = "libp2p-dns" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" -dependencies = [ - "async-trait", - "futures", - "hickory-resolver", - "libp2p-core", - "libp2p-identity", - "parking_lot", - "smallvec", - "tracing", -] - -[[package]] -name = "libp2p-gossipsub" -version = "0.49.2" +name = "libp2p-identify" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f58e37d8d6848e5c4c9e3c35c6f61133235bff2960c9c00a663b0849301221" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" dependencies = [ - "async-channel", "asynchronous-codec", - "base64", - "byteorder", - "bytes", "either", - "fnv", "futures", + "futures-bounded", "futures-timer", - "getrandom 0.2.17", - "hashlink 0.9.1", - "hex_fmt", "libp2p-core", "libp2p-identity", "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.5", - "regex", - "serde", - "sha2", - "tracing", - "web-time", -] - -[[package]] -name = "libp2p-identify" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" -dependencies = [ - "asynchronous-codec", - "either", - "futures", - "futures-bounded", - "futures-timer", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", - "smallvec", - "thiserror 2.0.18", + "smallvec", + "thiserror 2.0.18", "tracing", ] @@ -3850,7 +3886,6 @@ dependencies = [ "multihash", "quick-protobuf", "rand 0.8.5", - "serde", "sha2", "thiserror 2.0.18", "tracing", @@ -3876,7 +3911,6 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "rand 0.8.5", - "serde", "sha2", "smallvec", "thiserror 2.0.18", @@ -3885,25 +3919,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "libp2p-mdns" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66872d0f1ffcded2788683f76931be1c52e27f343edb93bc6d0bcd8887be443" -dependencies = [ - "futures", - "hickory-proto", - "if-watch", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "rand 0.8.5", - "smallvec", - "socket2 0.5.10", - "tokio", - "tracing", -] - [[package]] name = "libp2p-metrics" version = "0.17.0" @@ -3915,101 +3930,12 @@ dependencies = [ "libp2p-identify", "libp2p-identity", "libp2p-kad", - "libp2p-relay", "libp2p-swarm", "pin-project", "prometheus-client", "web-time", ] -[[package]] -name = "libp2p-noise" -version = "0.46.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc73eacbe6462a0eb92a6527cac6e63f02026e5407f8831bde8293f19217bfbf" -dependencies = [ - "asynchronous-codec", - "bytes", - "futures", - "libp2p-core", - "libp2p-identity", - "multiaddr", - "multihash", - "quick-protobuf", - "rand 0.8.5", - "snow", - "static_assertions", - "thiserror 2.0.18", - "tracing", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "libp2p-quic" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc448b2de9f4745784e3751fe8bc6c473d01b8317edd5ababcb0dec803d843f" -dependencies = [ - "futures", - "futures-timer", - "if-watch", - "libp2p-core", - "libp2p-identity", - "libp2p-tls", - "quinn", - "rand 0.8.5", - "ring", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-relay" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9b0392ed623243ad298326b9f806d51191829ac7585cc825c54c6c67b04d9" -dependencies = [ - "asynchronous-codec", - "bytes", - "either", - "futures", - "futures-bounded", - "futures-timer", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "quick-protobuf", - "quick-protobuf-codec", - "rand 0.8.5", - "static_assertions", - "thiserror 2.0.18", - "tracing", - "web-time", -] - -[[package]] -name = "libp2p-request-response" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9f1cca83488b90102abac7b67d5c36fc65bc02ed47620228af7ed002e6a1478" -dependencies = [ - "async-trait", - "cbor4ii", - "futures", - "futures-bounded", - "libp2p-core", - "libp2p-identity", - "libp2p-swarm", - "rand 0.8.5", - "serde", - "smallvec", - "tracing", -] - [[package]] name = "libp2p-swarm" version = "0.47.1" @@ -4020,130 +3946,28 @@ dependencies = [ "fnv", "futures", "futures-timer", - "hashlink 0.10.0", + "hashlink", "libp2p-core", "libp2p-identity", - "libp2p-swarm-derive", "multistream-select", "rand 0.8.5", "smallvec", - "tokio", "tracing", "web-time", ] -[[package]] -name = "libp2p-swarm-derive" -version = "0.35.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd297cf53f0cb3dee4d2620bb319ae47ef27c702684309f682bdb7e55a18ae9c" -dependencies = [ - "heck 0.5.0", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "libp2p-tcp" -version = "0.44.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6585b9309699f58704ec9ab0bb102eca7a3777170fa91a8678d73ca9cafa93" -dependencies = [ - "futures", - "futures-timer", - "if-watch", - "libc", - "libp2p-core", - "socket2 0.6.2", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-tls" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" -dependencies = [ - "futures", - "futures-rustls", - "libp2p-core", - "libp2p-identity", - "rcgen", - "ring", - "rustls", - "rustls-webpki", - "thiserror 2.0.18", - "x509-parser", - "yasna", -] - -[[package]] -name = "libp2p-upnp" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4757e65fe69399c1a243bbb90ec1ae5a2114b907467bf09f3575e899815bb8d3" -dependencies = [ - "futures", - "futures-timer", - "igd-next", - "libp2p-core", - "libp2p-swarm", - "tokio", - "tracing", -] - -[[package]] -name = "libp2p-websocket" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520e29066a48674c007bc11defe5dce49908c24cafd8fad2f5e1a6a8726ced53" -dependencies = [ - "either", - "futures", - "futures-rustls", - "libp2p-core", - "libp2p-identity", - "parking_lot", - "pin-project-lite", - "rw-stream-sink", - "soketto", - "thiserror 2.0.18", - "tracing", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "libp2p-yamux" -version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f15df094914eb4af272acf9adaa9e287baa269943f32ea348ba29cfb9bfc60d8" -dependencies = [ - "either", - "futures", - "libp2p-core", - "thiserror 2.0.18", - "tracing", - "yamux 0.12.1", - "yamux 0.13.9", -] - [[package]] name = "libredox" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags", "libc", + "plain", + "redox_syscall 0.7.3", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4156,6 +3980,17 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +[[package]] +name = "lmdb-master-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864808e0b19fb6dd3b70ba94ee671b82fce17554cf80aeb0a155c65bb08027df" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -4186,6 +4021,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -4208,12 +4064,42 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4241,23 +4127,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "moka" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "multiaddr" version = "0.18.2" @@ -4296,16 +4165,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" dependencies = [ "core2", - "serde", "unsigned-varint 0.8.0", ] -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - [[package]] name = "multistream-select" version = "0.13.0" @@ -4321,51 +4183,20 @@ dependencies = [ ] [[package]] -name = "netlink-packet-core" -version = "0.8.1" +name = "native-tls" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ - "paste", -] - -[[package]] -name = "netlink-packet-route" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" -dependencies = [ - "bitflags", - "libc", - "log", - "netlink-packet-core", -] - -[[package]] -name = "netlink-proto" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" -dependencies = [ - "bytes", - "futures", - "log", - "netlink-packet-core", - "netlink-sys", - "thiserror 2.0.18", -] - -[[package]] -name = "netlink-sys" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" -dependencies = [ - "bytes", - "futures-util", "libc", "log", - "tokio", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -4378,26 +4209,9 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - [[package]] name = "nom" version = "7.1.3" @@ -4506,6 +4320,45 @@ dependencies = [ "smallvec", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.37.3" @@ -4529,10 +4382,6 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] [[package]] name = "once_cell_polyfill" @@ -4546,6 +4395,56 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "owo-colors" version = "4.3.0" @@ -4553,21 +4452,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] -name = "pairing" -version = "0.22.0" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "group 0.12.1", + "elliptic-curve", + "primeorder", ] [[package]] -name = "pairing" -version = "0.23.0" +name = "page_size" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ - "group 0.13.0", + "libc", + "winapi", ] [[package]] @@ -4598,12 +4499,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -4622,17 +4517,40 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "pem" version = "3.0.6" @@ -4660,13 +4578,45 @@ dependencies = [ ] [[package]] -name = "petgraph" -version = "0.6.5" +name = "phf" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "fixedbitset", - "indexmap 2.13.0", + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", ] [[package]] @@ -4712,24 +4662,16 @@ dependencies = [ ] [[package]] -name = "plain" -version = "0.2.3" +name = "pkg-config" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "polling" -version = "3.11.0" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "poly1305" @@ -4755,10 +4697,17 @@ dependencies = [ ] [[package]] -name = "portable-atomic" -version = "1.13.1" +name = "postcard" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] [[package]] name = "potential_utf" @@ -4794,6 +4743,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -4811,7 +4769,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -4880,66 +4838,13 @@ dependencies = [ "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", - "rand_xorshift 0.4.0", + "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] -[[package]] -name = "prost" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" -dependencies = [ - "bytes", - "heck 0.3.3", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap", - "petgraph", - "prost", - "prost-types", - "regex", - "tempfile", - "which", -] - -[[package]] -name = "prost-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-types" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" -dependencies = [ - "bytes", - "prost", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -4976,10 +4881,9 @@ checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", - "futures-io", "pin-project-lite", "quinn-proto", - "quinn-udp", + "quinn-udp 0.5.14", "rustc-hash", "rustls", "socket2 0.6.2", @@ -4995,6 +4899,7 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -5024,6 +4929,19 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "quinn-udp" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76150b617afc75e6e21ac5f39bc196e80b65415ae48d62dbef8e2519d040ce42" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.45" @@ -5113,15 +5031,6 @@ dependencies = [ "serde", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand_xorshift" version = "0.4.0" @@ -5162,14 +5071,15 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" dependencies = [ "pem", "ring", "rustls-pki-types", "time", + "x509-parser", "yasna", ] @@ -5182,6 +5092,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -5193,6 +5112,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -5250,15 +5180,21 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", + "futures-util", + "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", + "mime", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -5269,22 +5205,56 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", - "webpki-roots 1.0.6", + "webpki-roots", ] [[package]] -name = "resolv-conf" -version = "0.7.6" +name = "reqwest" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "rfc6979" @@ -5339,24 +5309,6 @@ dependencies = [ "serde", ] -[[package]] -name = "rtnetlink" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" -dependencies = [ - "futures-channel", - "futures-util", - "log", - "netlink-packet-core", - "netlink-packet-route", - "netlink-proto", - "netlink-sys", - "nix 0.30.1", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "ruint" version = "1.17.2" @@ -5436,19 +5388,6 @@ dependencies = [ "nom", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.4" @@ -5458,7 +5397,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -5468,6 +5407,8 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -5476,6 +5417,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -5486,59 +5448,290 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-post-quantum" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da3cd9229bac4fae1f589c8f875b3c891a058ddaa26eb3bde16b5e43dc174ce" +dependencies = [ + "aws-lc-rs", + "rustls", + "rustls-webpki", +] + [[package]] name = "rustls-webpki" version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", ] [[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "saorsa-core" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3d05b97f789b0e0b7d54b2fe05f05edfafb94f72d065482fc20ce1e9fab69e" +dependencies = [ + "anyhow", + "async-trait", + "blake3", + "bytes", + "dirs 6.0.0", + "futures", + "hex", + "lru", + "once_cell", + "parking_lot", + "postcard", + "rand 0.8.5", + "saorsa-pqc 0.5.0", + "saorsa-transport", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "uuid", + "wyz", +] + +[[package]] +name = "saorsa-pqc" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +checksum = "56d4bae22bfc65b379efcaae0c9ec5075916a79c05e97d595a4b78fb8ff6545b" dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", + "aead", + "aes-gcm", + "anyhow", + "blake3", + "bytes", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "fips203", + "fips204", + "fips205", + "futures", + "hkdf", + "hmac", + "hpke", + "libc", + "log", + "pbkdf2", + "postcard", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "rayon", + "serde", + "serde_json", + "sha2", + "sha3", + "subtle", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", + "wide", + "x25519-dalek", + "zeroize", ] [[package]] -name = "rw-stream-sink" -version = "0.4.0" +name = "saorsa-pqc" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +checksum = "846eb36cf54149d079fd824aa0aaeaa708dd612ef54eaaf65583c82f90b19f95" dependencies = [ + "aead", + "aes-gcm", + "anyhow", + "blake3", + "bytes", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "fips203", + "fips204", + "fips205", "futures", - "pin-project", - "static_assertions", + "hkdf", + "hmac", + "hpke", + "libc", + "log", + "pbkdf2", + "postcard", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "rayon", + "serde", + "serde_json", + "sha2", + "sha3", + "subtle", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", + "wide", + "x25519-dalek", + "zeroize", ] [[package]] -name = "ryu" -version = "1.0.23" +name = "saorsa-transport" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "9647e1797e760d73568fb6f7035f40945e70b789579bffccd6a5305e9d5d0136" +dependencies = [ + "anyhow", + "async-trait", + "aws-lc-rs", + "blake3", + "bytes", + "chrono", + "clap", + "core-foundation 0.9.4", + "dashmap", + "dirs 5.0.1", + "futures-util", + "hex", + "indexmap 2.13.0", + "keyring", + "libc", + "lru-slab", + "nix", + "once_cell", + "parking_lot", + "pin-project-lite", + "quinn-udp 0.6.1", + "rand 0.8.5", + "rcgen", + "regex", + "reqwest 0.13.2", + "rustc-hash", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-platform-verifier", + "rustls-post-quantum", + "saorsa-pqc 0.4.2", + "serde", + "serde_json", + "serde_yaml", + "slab", + "socket2 0.5.10", + "system-configuration 0.6.1", + "thiserror 2.0.18", + "time", + "tinyvec", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "unicode-width", + "uuid", + "windows", + "x25519-dalek", + "zeroize", +] [[package]] -name = "same-file" -version = "1.0.6" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "winapi-util", + "windows-sys 0.61.2", ] [[package]] @@ -5628,42 +5821,50 @@ dependencies = [ ] [[package]] -name = "self_encryption" -version = "0.30.0" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9439a0cb3efb35e080a1576e3e00a804caab04010adc802aed88cf539b103ed" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "aes", - "bincode", - "brotli", - "bytes", - "cbc", - "hex", - "itertools 0.10.5", - "lazy_static", - "num_cpus", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rayon", - "serde", + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", "tempfile", - "thiserror 1.0.69", - "tiny-keccak", - "tokio", - "xor_name", + "windows-sys 0.52.0", ] [[package]] name = "self_encryption" -version = "0.34.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45480c8fa045f158ab05f445ce7d0b6dfcb6e3028d91b78013271b48daa435b2" +checksum = "c11020d59e8e663ba591026c2b45a08caa74a3a654ac12b2532d5a7c5b97e510" dependencies = [ - "aes", "bincode", + "blake3", "brotli", "bytes", - "cbc", + "chacha20poly1305", "hex", "rand 0.8.5", "rand_chacha 0.3.1", @@ -5746,6 +5947,26 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_test" version = "1.0.177" @@ -5798,6 +6019,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -5865,6 +6099,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -5887,6 +6131,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" @@ -5908,23 +6158,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" -[[package]] -name = "snow" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" -dependencies = [ - "aes-gcm", - "blake2", - "chacha20poly1305", - "curve25519-dalek", - "rand_core 0.6.4", - "ring", - "rustc_version 0.4.1", - "sha2", - "subtle", -] - [[package]] name = "socket2" version = "0.5.10" @@ -5946,18 +6179,12 @@ dependencies = [ ] [[package]] -name = "soketto" -version = "0.8.1" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ - "base64", - "bytes", - "futures", - "httparse", - "log", - "rand 0.8.5", - "sha1", + "lock_api", ] [[package]] @@ -6003,7 +6230,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.117", @@ -6058,6 +6285,15 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -6069,6 +6305,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -6076,7 +6323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6090,18 +6337,23 @@ dependencies = [ "libc", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.26.0" @@ -6111,7 +6363,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -6258,7 +6510,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", @@ -6275,6 +6529,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -6306,6 +6570,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -6319,6 +6584,27 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -6328,6 +6614,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.25.4+spec-1.1.0" @@ -6335,7 +6635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", - "toml_datetime", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "winnow", ] @@ -6350,16 +6650,10 @@ dependencies = [ ] [[package]] -name = "tonic-build" -version = "0.6.2" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" -dependencies = [ - "proc-macro2", - "prost-build", - "quote", - "syn 1.0.109", -] +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tower" @@ -6374,6 +6668,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6412,6 +6707,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6487,12 +6783,17 @@ version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "serde", "serde_json", "sharded-slab", "smallvec", "thread_local", + "time", + "tracing", "tracing-core", "tracing-log", "tracing-serde", @@ -6552,21 +6853,18 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -6601,13 +6899,13 @@ dependencies = [ "fs-err", "glob", "goblin", - "heck 0.5.0", + "heck", "indexmap 2.13.0", "once_cell", "serde", "tempfile", "textwrap", - "toml", + "toml 0.5.11", "uniffi_internal_macros", "uniffi_meta", "uniffi_pipeline", @@ -6664,7 +6962,7 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "toml", + "toml 0.5.11", "uniffi_meta", ] @@ -6675,7 +6973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" dependencies = [ "anyhow", - "siphasher", + "siphasher 0.3.11", "uniffi_internal_macros", "uniffi_pipeline", ] @@ -6687,7 +6985,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap 2.13.0", "tempfile", "uniffi_internal_macros", @@ -6715,6 +7013,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "unsigned-varint" version = "0.7.2" @@ -6758,6 +7062,30 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + [[package]] name = "uuid" version = "1.22.0" @@ -6766,6 +7094,7 @@ checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -6776,16 +7105,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "vergen" -version = "8.3.2" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" -dependencies = [ - "anyhow", - "cfg-if", - "rustversion", - "time", -] +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" @@ -6926,6 +7249,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -6973,12 +7309,12 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.26.11" +name = "webpki-root-certs" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" dependencies = [ - "webpki-roots 1.0.6", + "rustls-pki-types", ] [[package]] @@ -7000,23 +7336,15 @@ dependencies = [ ] [[package]] -name = "which" -version = "4.4.2" +name = "wide" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "bytemuck", + "safe_arch", ] -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - [[package]] name = "winapi" version = "0.3.9" @@ -7050,23 +7378,25 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.62.2" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", + "windows-core 0.58.0", + "windows-targets 0.52.6", ] [[package]] -name = "windows-collections" -version = "0.3.2" +name = "windows-core" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-core", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", ] [[package]] @@ -7075,22 +7405,22 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] -name = "windows-future" -version = "0.3.2" +name = "windows-implement" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ - "windows-core", - "windows-link", - "windows-threading", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -7104,6 +7434,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -7122,13 +7463,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-numerics" -version = "0.3.1" +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-core", "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -7140,6 +7491,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.5.1" @@ -7151,27 +7512,27 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.42.2", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] @@ -7194,6 +7555,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -7243,13 +7619,10 @@ dependencies = [ ] [[package]] -name = "windows-threading" -version = "0.2.1" +name = "windows_aarch64_gnullvm" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -7269,6 +7642,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -7287,6 +7666,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -7317,6 +7702,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7335,6 +7726,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7353,6 +7750,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7371,6 +7774,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -7398,16 +7807,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -7424,7 +7823,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -7435,7 +7834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap 2.13.0", "prettyplease", "syn 2.0.117", @@ -7525,9 +7924,9 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", "data-encoding", @@ -7535,24 +7934,20 @@ dependencies = [ "lazy_static", "nom", "oid-registry", + "ring", "rusticata-macros", "thiserror 2.0.18", "time", ] [[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "xmltree" -version = "0.10.3" +name = "xattr" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ - "xml-rs", + "libc", + "rustix", ] [[package]] @@ -7570,34 +7965,12 @@ dependencies = [ ] [[package]] -name = "yamux" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed0164ae619f2dc144909a9f082187ebb5893693d8c0196e8085283ccd4b776" -dependencies = [ - "futures", - "log", - "nohash-hasher", - "parking_lot", - "pin-project", - "rand 0.8.5", - "static_assertions", -] - -[[package]] -name = "yamux" -version = "0.13.9" +name = "xz2" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c650efd29044140aa63caaf80129996a9e2659a2ab7045a7e061807d02fc8549" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" dependencies = [ - "futures", - "log", - "nohash-hasher", - "parking_lot", - "pin-project", - "rand 0.9.2", - "static_assertions", - "web-time", + "lzma-sys", ] [[package]] @@ -7726,8 +8099,78 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq 0.3.1", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.13.0", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/ffi/rust/ant-ffi/Cargo.toml b/ffi/rust/ant-ffi/Cargo.toml index 1e41fb1..45e4a33 100644 --- a/ffi/rust/ant-ffi/Cargo.toml +++ b/ffi/rust/ant-ffi/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "ant-ffi" -version = "0.1.0" +version = "0.2.0" edition = "2021" [lib] crate-type = ["cdylib", "staticlib", "lib"] [dependencies] -autonomi = { path = "../../../../autonomi/autonomi" } -blsttc = "8" +ant-core = { git = "https://github.com/WithAutonomi/ant-client" } +evmlib = "0.4.9" bytes = "1" hex = "0.4" -libp2p = "0.56.0" -thiserror = "1.0" +rmp-serde = "1" +thiserror = "2" tokio = { version = "1", features = ["rt-multi-thread"] } uniffi = { workspace = true, features = ["tokio"] } diff --git a/ffi/rust/ant-ffi/src/client.rs b/ffi/rust/ant-ffi/src/client.rs index c4a3655..8d34e6c 100644 --- a/ffi/rust/ant-ffi/src/client.rs +++ b/ffi/rust/ant-ffi/src/client.rs @@ -1,568 +1,317 @@ -use autonomi::client::payment::PaymentOption as AutonomiPaymentOption; -use bytes::Bytes; +use std::path::PathBuf; use std::sync::Arc; -use crate::data::{ChunkAddress, DataAddress, DataMapChunk}; -use crate::files::{ - ArchiveAddress, PrivateArchive, PrivateArchiveDataMap, PublicArchive, +use bytes::Bytes; + +use ant_core::data::{ + Client as CoreClient, ClientConfig, CoreNodeConfig, MultiAddr, NodeMode, P2PNode, }; -use crate::graph::{GraphEntry, GraphEntryAddress}; -use crate::keys::{PublicKey, SecretKey}; -use crate::network::Network; -use crate::payment::{PaymentOption, Wallet}; + +use crate::data::{format_payment_mode, parse_payment_mode}; +use crate::wallet::Wallet; use crate::{ - ChunkPutResult, ClientError, DataPutResult, DirUploadPublicResult, DirUploadResult, - FileUploadPublicResult, FileUploadResult, GraphEntryPutResult, - PrivateArchivePutResult, PublicArchivePutResult, - UploadResult, + ChunkPutResult, ClientError, DataPutPrivateResult, DataPutPublicResult, FilePutPublicResult, }; -/// Autonomi network client +/// Autonomi network client (wraps ant-core Client). +/// +/// Provides direct access to the Autonomi network without needing +/// an antd daemon. Suitable for mobile apps (Android/iOS). #[derive(uniffi::Object)] pub struct Client { - inner: Arc, + inner: CoreClient, } #[uniffi::export(async_runtime = "tokio")] impl Client { - // ===== Init Methods ===== - - #[uniffi::constructor] - pub async fn init() -> Result, ClientError> { - let client = - autonomi::Client::init() - .await - .map_err(|e| ClientError::InitializationFailed { - reason: e.to_string(), - })?; - Ok(Arc::new(Self { - inner: Arc::new(client), - })) - } - + /// Connect to a local test network. #[uniffi::constructor] - pub async fn init_local() -> Result, ClientError> { - let client = autonomi::Client::init_local().await.map_err(|e| { - ClientError::InitializationFailed { - reason: e.to_string(), - } - })?; - Ok(Arc::new(Self { - inner: Arc::new(client), - })) - } - - #[uniffi::constructor] - pub async fn init_with_peers( - peers: Vec, - evm_network: Arc, - data_dir: Option, - ) -> Result, ClientError> { - use std::str::FromStr; - - if let Some(dir) = data_dir { - unsafe { - std::env::set_var("HOME", &dir); - std::env::set_var("TMPDIR", &dir); - } - } - - let multiaddrs: Vec<_> = peers - .iter() - .filter_map(|p| autonomi::Multiaddr::from_str(p).ok()) - .collect(); - - if multiaddrs.is_empty() { - return Err(ClientError::InitializationFailed { - reason: "No valid peer addresses provided".to_string(), - }); - } - - let local = !multiaddrs.iter().any(|addr| { - addr.iter().any(|component| { - use libp2p::multiaddr::Protocol; - matches!(component, Protocol::Ip4(ip) if !ip.is_private() && !ip.is_loopback()) - }) - }); - - let config = autonomi::ClientConfig { - bootstrap_config: autonomi::BootstrapConfig { - local, - initial_peers: multiaddrs, - ..Default::default() - }, - evm_network: evm_network.inner.clone(), - strategy: Default::default(), - network_id: None, - }; - - let client = autonomi::Client::init_with_config(config) - .await + pub async fn connect_local() -> Result, ClientError> { + let builder = CoreNodeConfig::builder() + .mode(NodeMode::Client) + .port(0) + .local(true) + .allow_loopback(true) + .ipv6(false); + + let config = builder + .build() .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(Arc::new(Self { - inner: Arc::new(client), - })) - } - - // ===== Data Methods ===== - - pub async fn data_put_public( - &self, - data: Vec, - payment: PaymentOption, - ) -> Result { - let bytes = Bytes::from(data); - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) - } - }; - - let (price, address) = self - .inner - .data_put_public(bytes, autonomi_payment) + let node = P2PNode::new(config) .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(UploadResult { - price: price.to_string(), - address: address.to_hex(), - }) - } - - pub async fn data_get_public(&self, address_hex: String) -> Result, ClientError> { - let data_address = crate::data::DataAddress::from_hex(address_hex).map_err(|e| { - ClientError::InvalidAddress { - reason: e.to_string(), - } - })?; - - let bytes = self - .inner - .data_get_public(&data_address.inner) + node.start() .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(bytes.to_vec()) + let client = CoreClient::from_node(Arc::new(node), ClientConfig::default()); + + Ok(Arc::new(Self { inner: client })) } - pub async fn data_put( - &self, - data: Vec, - payment: PaymentOption, - ) -> Result { - let bytes = Bytes::from(data); - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) - } - }; + /// Connect to the network using explicit bootstrap peers. + #[uniffi::constructor] + pub async fn connect(peers: Vec) -> Result, ClientError> { + let mut builder = CoreNodeConfig::builder() + .mode(NodeMode::Client) + .port(0); + + for peer_str in &peers { + let addr: MultiAddr = peer_str + .parse() + .map_err(|e| ClientError::InitializationFailed { + reason: format!("invalid peer address {peer_str}: {e}"), + })?; + builder = builder.bootstrap_peer(addr); + } - let (cost, data_map) = self - .inner - .data_put(bytes, autonomi_payment) - .await - .map_err(|e| ClientError::NetworkError { + let config = builder + .build() + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(DataPutResult { - cost: cost.to_string(), - data_map: Arc::new(DataMapChunk { inner: data_map }), - }) - } - - pub async fn data_get(&self, data_map: Arc) -> Result, ClientError> { - let bytes = self - .inner - .data_get(&data_map.inner) + let node = P2PNode::new(config) .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(bytes.to_vec()) - } - pub async fn data_cost(&self, data: Vec) -> Result { - let bytes = Bytes::from(data); - let cost = self - .inner - .data_cost(bytes) + node.start() .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(cost.to_string()) - } - // ===== Chunk Methods ===== - - pub async fn chunk_put( - &self, - data: Vec, - payment: PaymentOption, - ) -> Result { - let chunk = autonomi::Chunk::new(Bytes::from(data)); - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) + // Wait briefly for peer connections + for _ in 0..20 { + if !node.connected_peers().await.is_empty() { + break; } - }; + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + } - let (cost, addr) = self - .inner - .chunk_put(&chunk, autonomi_payment) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + let client = CoreClient::from_node(Arc::new(node), ClientConfig::default()); - Ok(ChunkPutResult { - cost: cost.to_string(), - address: Arc::new(ChunkAddress { inner: addr }), - }) + Ok(Arc::new(Self { inner: client })) } - pub async fn chunk_get(&self, addr: Arc) -> Result, ClientError> { - let chunk = self - .inner - .chunk_get(&addr.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - Ok(chunk.value.to_vec()) - } + /// Connect to the network with a wallet configured for write operations. + /// + /// Takes the wallet private key and EVM network details directly, + /// since the wallet must be constructed fresh for ownership transfer. + #[uniffi::constructor] + pub async fn connect_with_wallet( + peers: Vec, + private_key: String, + rpc_url: String, + payment_token_address: String, + data_payments_address: String, + ) -> Result, ClientError> { + let mut builder = CoreNodeConfig::builder() + .mode(NodeMode::Client) + .port(0); - pub async fn chunk_cost(&self, addr: Arc) -> Result { - let cost = self - .inner - .chunk_cost(&addr.inner) - .await - .map_err(|e| ClientError::NetworkError { + for peer_str in &peers { + let addr: MultiAddr = peer_str + .parse() + .map_err(|e| ClientError::InitializationFailed { + reason: format!("invalid peer address {peer_str}: {e}"), + })?; + builder = builder.bootstrap_peer(addr); + } + + let config = builder + .build() + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(cost.to_string()) - } - - // ===== Graph Entry Methods ===== - pub async fn graph_entry_get( - &self, - addr: Arc, - ) -> Result, ClientError> { - let entry = self - .inner - .graph_entry_get(&addr.inner) + let node = P2PNode::new(config) .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(Arc::new(GraphEntry { inner: entry })) - } - pub async fn graph_entry_check_existence( - &self, - addr: Arc, - ) -> Result { - let exists = self - .inner - .graph_entry_check_existence(&addr.inner) + node.start() .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::InitializationFailed { reason: e.to_string(), })?; - Ok(exists) - } - pub async fn graph_entry_put( - &self, - entry: Arc, - payment: PaymentOption, - ) -> Result { - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) + for _ in 0..20 { + if !node.connected_peers().await.is_empty() { + break; } - }; + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + } - let (cost, addr) = self - .inner - .graph_entry_put(entry.inner.clone(), autonomi_payment) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), + let network = evmlib::Network::new_custom( + &rpc_url, + &payment_token_address, + &data_payments_address, + None, + ); + let wallet = evmlib::wallet::Wallet::new_from_private_key(network, &private_key) + .map_err(|e| ClientError::InitializationFailed { + reason: format!("failed to create wallet: {e}"), })?; - Ok(GraphEntryPutResult { - cost: cost.to_string(), - address: Arc::new(GraphEntryAddress { inner: addr }), - }) - } + let client = + CoreClient::from_node(Arc::new(node), ClientConfig::default()).with_wallet(wallet); - pub async fn graph_entry_cost(&self, key: Arc) -> Result { - let cost = self - .inner - .graph_entry_cost(&key.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - Ok(cost.to_string()) + Ok(Arc::new(Self { inner: client })) } - // ===== Archive Methods ===== + // ===== Chunk Operations ===== - pub async fn archive_cost( - &self, - archive: Arc, - ) -> Result { - let cost = self - .inner - .archive_cost(&archive.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - Ok(cost.to_string()) + /// Store a chunk on the network. + pub async fn chunk_put(&self, data: Vec) -> Result { + let address = self.inner.chunk_put(Bytes::from(data)).await?; + Ok(ChunkPutResult { + address: hex::encode(address), + }) } - pub async fn archive_get_public( - &self, - address: Arc, - ) -> Result, ClientError> { - let archive = self + /// Retrieve a chunk by hex-encoded address. + pub async fn chunk_get(&self, address_hex: String) -> Result, ClientError> { + let address = hex_to_address(&address_hex)?; + let chunk = self .inner - .archive_get_public(&address.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), + .chunk_get(&address) + .await? + .ok_or_else(|| ClientError::NotFound { + reason: format!("chunk {address_hex} not found"), })?; - Ok(Arc::new(PublicArchive { inner: archive })) + Ok(chunk.content.to_vec()) } - pub async fn archive_put_public( - &self, - archive: Arc, - payment: PaymentOption, - ) -> Result { - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) - } - }; - - let (cost, addr) = self - .inner - .archive_put_public(&archive.inner, autonomi_payment) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - - Ok(PublicArchivePutResult { - cost: cost.to_string(), - address: Arc::new(ArchiveAddress { inner: addr }), - }) + /// Check if a chunk exists on the network. + pub async fn chunk_exists(&self, address_hex: String) -> Result { + let address = hex_to_address(&address_hex)?; + Ok(self.inner.chunk_exists(&address).await?) } - pub async fn archive_get( - &self, - data_map: Arc, - ) -> Result, ClientError> { - let archive = self - .inner - .archive_get(&data_map.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - Ok(Arc::new(PrivateArchive { inner: archive })) - } + // ===== Data Operations ===== - pub async fn archive_put( + /// Upload public data. Returns the address of the stored data map. + pub async fn data_put_public( &self, - archive: Arc, - payment: PaymentOption, - ) -> Result { - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) - } - }; + data: Vec, + payment_mode: String, + ) -> Result { + let mode = parse_payment_mode(&payment_mode).map_err(|e| ClientError::InvalidInput { + reason: e, + })?; - let (cost, data_map) = self + let result = self .inner - .archive_put(&archive.inner, autonomi_payment) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + .data_upload_with_mode(Bytes::from(data), mode) + .await?; - Ok(PrivateArchivePutResult { - cost: cost.to_string(), - data_map: Arc::new(DataMapChunk { inner: data_map }), + let address = self.inner.data_map_store(&result.data_map).await?; + + Ok(DataPutPublicResult { + address: hex::encode(address), + chunks_stored: result.chunks_stored as u64, + payment_mode_used: format_payment_mode(result.payment_mode_used), }) } - // ===== File Operations ===== - - pub async fn file_cost( - &self, - path: String, - follow_symlinks: bool, - include_hidden: bool, - ) -> Result { - let path = std::path::PathBuf::from(path); - let cost = self - .inner - .file_cost(&path, follow_symlinks, include_hidden) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - Ok(cost.to_string()) + /// Retrieve public data by hex-encoded address. + pub async fn data_get_public(&self, address_hex: String) -> Result, ClientError> { + let address = hex_to_address(&address_hex)?; + let data_map = self.inner.data_map_fetch(&address).await?; + let content = self.inner.data_download(&data_map).await?; + Ok(content.to_vec()) } - pub async fn file_upload( + /// Upload private data. Returns the serialized data map (hex). + pub async fn data_put_private( &self, - path: String, - payment: PaymentOption, - ) -> Result { - let path = std::path::PathBuf::from(path); - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) - } - }; + data: Vec, + payment_mode: String, + ) -> Result { + let mode = parse_payment_mode(&payment_mode).map_err(|e| ClientError::InvalidInput { + reason: e, + })?; - let (cost, data_map) = self + let result = self .inner - .file_content_upload(path, autonomi_payment.into()) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + .data_upload_with_mode(Bytes::from(data), mode) + .await?; - Ok(FileUploadResult { - cost: cost.to_string(), - data_map: Arc::new(DataMapChunk { inner: data_map }), - }) - } - - pub async fn file_upload_public( - &self, - path: String, - payment: PaymentOption, - ) -> Result { - let path = std::path::PathBuf::from(path); - let autonomi_payment = match payment { - PaymentOption::WalletPayment { wallet_ref } => { - AutonomiPaymentOption::Wallet(wallet_ref.inner.clone()) + let data_map_bytes = rmp_serde::to_vec(&result.data_map).map_err(|e| { + ClientError::InternalError { + reason: format!("failed to serialize data map: {e}"), } - }; - - let (cost, addr) = self - .inner - .file_content_upload_public(path, autonomi_payment.into()) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + })?; - Ok(FileUploadPublicResult { - cost: cost.to_string(), - address: Arc::new(DataAddress { inner: addr }), + Ok(DataPutPrivateResult { + data_map: hex::encode(data_map_bytes), + chunks_stored: result.chunks_stored as u64, + payment_mode_used: format_payment_mode(result.payment_mode_used), }) } - pub async fn file_download( - &self, - data_map: Arc, - path: String, - ) -> Result<(), ClientError> { - let path = std::path::PathBuf::from(path); - self.inner - .file_download(&data_map.inner, path) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), + /// Retrieve private data using a hex-encoded data map. + pub async fn data_get_private(&self, data_map_hex: String) -> Result, ClientError> { + let data_map_bytes = + hex::decode(&data_map_hex).map_err(|e| ClientError::InvalidInput { + reason: format!("invalid hex: {e}"), })?; - Ok(()) - } - - pub async fn file_download_public( - &self, - address: Arc, - path: String, - ) -> Result<(), ClientError> { - let path = std::path::PathBuf::from(path); - self.inner - .file_download_public(&address.inner, path) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), + let data_map: ant_core::data::DataMap = + rmp_serde::from_slice(&data_map_bytes).map_err(|e| ClientError::InvalidInput { + reason: format!("invalid data map: {e}"), })?; - Ok(()) + let content = self.inner.data_download(&data_map).await?; + Ok(content.to_vec()) } - pub async fn dir_upload( - &self, - path: String, - wallet: Arc, - ) -> Result { - let path = std::path::PathBuf::from(path); - - let (cost, data_map) = self - .inner - .dir_upload(path, &wallet.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; - - Ok(DirUploadResult { - cost: cost.to_string(), - data_map: Arc::new(PrivateArchiveDataMap { inner: data_map }), - }) - } + // ===== File Operations ===== - pub async fn dir_upload_public( + /// Upload a file from disk (public). Returns the address. + pub async fn file_upload_public( &self, path: String, - wallet: Arc, - ) -> Result { - let path = std::path::PathBuf::from(path); + payment_mode: String, + ) -> Result { + let mode = parse_payment_mode(&payment_mode).map_err(|e| ClientError::InvalidInput { + reason: e, + })?; + let file_path = PathBuf::from(&path); - let (cost, addr) = self + let result = self .inner - .dir_upload_public(path, &wallet.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + .file_upload_with_mode(&file_path, mode) + .await?; - Ok(DirUploadPublicResult { - cost: cost.to_string(), - address: Arc::new(ArchiveAddress { inner: addr }), + let address = self.inner.data_map_store(&result.data_map).await?; + + Ok(FilePutPublicResult { + address: hex::encode(address), }) } - pub async fn dir_download( + /// Download a file to disk by hex-encoded address. + pub async fn file_download_public( &self, - data_map: Arc, - path: String, + address_hex: String, + dest_path: String, ) -> Result<(), ClientError> { - let path = std::path::PathBuf::from(path); + let address = hex_to_address(&address_hex)?; + let data_map = self.inner.data_map_fetch(&address).await?; + let dest = PathBuf::from(&dest_path); self.inner - .dir_download(&data_map.inner, path) + .file_download(&data_map, &dest) .await .map_err(|e| ClientError::NetworkError { reason: e.to_string(), @@ -570,18 +319,28 @@ impl Client { Ok(()) } - pub async fn dir_download_public( - &self, - address: Arc, - path: String, - ) -> Result<(), ClientError> { - let path = std::path::PathBuf::from(path); + // ===== Wallet Operations ===== + + /// Approve token spend for storage payments (one-time). + pub async fn wallet_approve(&self) -> Result<(), ClientError> { self.inner - .dir_download_public(&address.inner, path) + .approve_token_spend() .await - .map_err(|e| ClientError::NetworkError { + .map_err(|e| ClientError::PaymentError { reason: e.to_string(), })?; Ok(()) } } + +/// Parse a hex string into a 32-byte address. +fn hex_to_address(hex: &str) -> Result<[u8; 32], ClientError> { + let bytes = hex::decode(hex).map_err(|e| ClientError::InvalidInput { + reason: format!("invalid hex address: {e}"), + })?; + bytes + .try_into() + .map_err(|_| ClientError::InvalidInput { + reason: "address must be 32 bytes".into(), + }) +} diff --git a/ffi/rust/ant-ffi/src/data.rs b/ffi/rust/ant-ffi/src/data.rs index f347d3f..30075c1 100644 --- a/ffi/rust/ant-ffi/src/data.rs +++ b/ffi/rust/ant-ffi/src/data.rs @@ -1,168 +1,35 @@ -use autonomi::data::{ - DataAddress as AutonomiDataAddress, private::DataMapChunk as AutonomiDataMapChunk, -}; -use autonomi::{Chunk as AutonomiChunk, ChunkAddress as AutonomiChunkAddress, XorName}; -use bytes::Bytes; -use std::sync::Arc; +/// Re-export ant-core types used in FFI signatures. +pub use ant_core::data::PaymentMode; -#[derive(Debug, uniffi::Error, thiserror::Error)] -pub enum DataError { - #[error("Invalid data: {reason}")] - InvalidData { reason: String }, - #[error("Parsing failed: {reason}")] - ParsingFailed { reason: String }, +/// Result of a data upload operation (internal, not exposed via UniFFI). +pub struct DataUploadResult { + pub data_map: ant_core::data::DataMap, + pub chunks_stored: usize, + pub payment_mode_used: PaymentMode, } -#[derive(uniffi::Object, Clone, Debug)] -pub struct Chunk { - pub(crate) inner: AutonomiChunk, +/// Result of a file upload operation (internal, not exposed via UniFFI). +pub struct FileUploadResult { + pub data_map: ant_core::data::DataMap, + pub chunks_stored: usize, + pub payment_mode_used: PaymentMode, } -#[uniffi::export] -impl Chunk { - #[uniffi::constructor] - pub fn new(value: Vec) -> Arc { - Arc::new(Self { - inner: AutonomiChunk::new(Bytes::from(value)), - }) - } - - pub fn value(&self) -> Vec { - self.inner.value().to_vec() - } - - pub fn address(&self) -> Arc { - Arc::new(ChunkAddress { - inner: *self.inner.address(), - }) - } - - pub fn network_address(&self) -> String { - self.inner.network_address().to_string() - } - - pub fn size(&self) -> u64 { - self.inner.size() as u64 - } - - pub fn is_too_big(&self) -> bool { - self.inner.is_too_big() - } -} - -#[uniffi::export] -pub fn chunk_max_raw_size() -> u64 { - AutonomiChunk::MAX_RAW_SIZE as u64 -} - -#[uniffi::export] -pub fn chunk_max_size() -> u64 { - AutonomiChunk::MAX_SIZE as u64 -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct ChunkAddress { - pub(crate) inner: AutonomiChunkAddress, -} - -#[uniffi::export] -impl ChunkAddress { - #[uniffi::constructor] - pub fn new(bytes: Vec) -> Result, DataError> { - if bytes.len() != 32 { - return Err(DataError::InvalidData { - reason: format!("XorName must be exactly 32 bytes, got {}", bytes.len()), - }); - } - let mut array = [0u8; 32]; - array.copy_from_slice(&bytes); - Ok(Arc::new(Self { - inner: AutonomiChunkAddress::new(XorName(array)), - })) - } - - #[uniffi::constructor] - pub fn from_content(data: Vec) -> Arc { - Arc::new(Self { - inner: AutonomiChunkAddress::new(XorName::from_content(&data)), - }) - } - - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, DataError> { - let inner = AutonomiChunkAddress::from_hex(&hex).map_err(|e| DataError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } - - pub fn to_bytes(&self) -> Vec { - self.inner.xorname().0.to_vec() - } -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct DataAddress { - pub(crate) inner: AutonomiDataAddress, -} - -#[uniffi::export] -impl DataAddress { - #[uniffi::constructor] - pub fn new(bytes: Vec) -> Result, DataError> { - if bytes.len() != 32 { - return Err(DataError::InvalidData { - reason: format!("XorName must be exactly 32 bytes, got {}", bytes.len()), - }); - } - let mut array = [0u8; 32]; - array.copy_from_slice(&bytes); - Ok(Arc::new(Self { - inner: AutonomiDataAddress::new(XorName(array)), - })) - } - - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, DataError> { - let inner = AutonomiDataAddress::from_hex(&hex).map_err(|e| DataError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } - - pub fn to_bytes(&self) -> Vec { - self.inner.xorname().0.to_vec() +/// Parse a payment mode string into ant-core's PaymentMode. +pub fn parse_payment_mode(mode: &str) -> Result { + match mode { + "auto" => Ok(PaymentMode::Auto), + "merkle" => Ok(PaymentMode::Merkle), + "single" => Ok(PaymentMode::Single), + other => Err(format!("invalid payment_mode: {other:?}. Use \"auto\", \"merkle\", or \"single\"")), } } -#[derive(uniffi::Object, Clone, Debug)] -pub struct DataMapChunk { - pub(crate) inner: AutonomiDataMapChunk, -} - -#[uniffi::export] -impl DataMapChunk { - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, DataError> { - let inner = AutonomiDataMapChunk::from_hex(&hex).map_err(|e| DataError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } - - pub fn address(&self) -> String { - self.inner.address().to_string() +/// Format a PaymentMode for FFI results. +pub fn format_payment_mode(mode: PaymentMode) -> String { + match mode { + PaymentMode::Auto => "auto".into(), + PaymentMode::Merkle => "merkle".into(), + PaymentMode::Single => "single".into(), } } diff --git a/ffi/rust/ant-ffi/src/files.rs b/ffi/rust/ant-ffi/src/files.rs deleted file mode 100644 index a75a52c..0000000 --- a/ffi/rust/ant-ffi/src/files.rs +++ /dev/null @@ -1,263 +0,0 @@ -use autonomi::files::archive_private::PrivateArchiveDataMap as AutonomiPrivateArchiveDataMap; -use autonomi::files::archive_public::ArchiveAddress as AutonomiArchiveAddress; -use autonomi::files::{ - Metadata as AutonomiMetadata, PrivateArchive as AutonomiPrivateArchive, - PublicArchive as AutonomiPublicArchive, -}; -use std::sync::Arc; - -use crate::data::{DataAddress, DataMapChunk}; - -#[derive(Debug, uniffi::Error, thiserror::Error)] -pub enum ArchiveError { - #[error("Invalid archive: {reason}")] - InvalidArchive { reason: String }, - #[error("Parsing failed: {reason}")] - ParsingFailed { reason: String }, - #[error("File not found: {path}")] - FileNotFound { path: String }, -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct Metadata { - pub(crate) inner: AutonomiMetadata, -} - -#[uniffi::export] -impl Metadata { - #[uniffi::constructor] - pub fn new(size: u64) -> Arc { - Arc::new(Self { - inner: AutonomiMetadata::new_with_size(size), - }) - } - - #[uniffi::constructor] - pub fn with_timestamps(size: u64, created: u64, modified: u64) -> Arc { - Arc::new(Self { - inner: AutonomiMetadata { - size, - created, - modified, - extra: None, - }, - }) - } - - pub fn size(&self) -> u64 { - self.inner.size - } - - pub fn created(&self) -> u64 { - self.inner.created - } - - pub fn modified(&self) -> u64 { - self.inner.modified - } -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct ArchiveAddress { - pub(crate) inner: AutonomiArchiveAddress, -} - -#[uniffi::export] -impl ArchiveAddress { - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, ArchiveError> { - let inner = - AutonomiArchiveAddress::from_hex(&hex).map_err(|e| ArchiveError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct PrivateArchiveDataMap { - pub(crate) inner: AutonomiPrivateArchiveDataMap, -} - -#[uniffi::export] -impl PrivateArchiveDataMap { - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, ArchiveError> { - let inner = AutonomiPrivateArchiveDataMap::from_hex(&hex).map_err(|e| { - ArchiveError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - } - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} - -#[derive(uniffi::Record)] -pub struct PublicArchiveFileEntry { - pub path: String, - pub address: Arc, - pub metadata: Arc, -} - -#[derive(uniffi::Record)] -pub struct PrivateArchiveFileEntry { - pub path: String, - pub data_map: Arc, - pub metadata: Arc, -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct PublicArchive { - pub(crate) inner: AutonomiPublicArchive, -} - -#[uniffi::export] -impl PublicArchive { - #[uniffi::constructor] - pub fn new() -> Arc { - Arc::new(Self { - inner: AutonomiPublicArchive::new(), - }) - } - - pub fn add_file( - &self, - path: String, - address: Arc, - metadata: Arc, - ) -> Arc { - let mut archive = self.inner.clone(); - archive.add_file( - std::path::PathBuf::from(path), - address.inner, - metadata.inner.clone(), - ); - Arc::new(Self { inner: archive }) - } - - pub fn rename_file( - &self, - old_path: String, - new_path: String, - ) -> Result, ArchiveError> { - let mut archive = self.inner.clone(); - archive - .rename_file( - &std::path::PathBuf::from(&old_path), - &std::path::PathBuf::from(&new_path), - ) - .map_err(|e| ArchiveError::InvalidArchive { - reason: format!("Failed to rename file: {}", e), - })?; - Ok(Arc::new(Self { inner: archive })) - } - - pub fn files(&self) -> Vec { - self.inner - .map() - .iter() - .map(|(path, (addr, meta))| PublicArchiveFileEntry { - path: path.to_string_lossy().to_string(), - address: Arc::new(DataAddress { inner: *addr }), - metadata: Arc::new(Metadata { - inner: meta.clone(), - }), - }) - .collect() - } - - pub fn file_count(&self) -> u64 { - self.inner.map().len() as u64 - } - - pub fn addresses(&self) -> Vec { - self.inner - .addresses() - .into_iter() - .map(|a| a.to_hex()) - .collect() - } -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct PrivateArchive { - pub(crate) inner: AutonomiPrivateArchive, -} - -#[uniffi::export] -impl PrivateArchive { - #[uniffi::constructor] - pub fn new() -> Arc { - Arc::new(Self { - inner: AutonomiPrivateArchive::new(), - }) - } - - pub fn add_file( - &self, - path: String, - data_map: Arc, - metadata: Arc, - ) -> Arc { - let mut archive = self.inner.clone(); - archive.add_file( - std::path::PathBuf::from(path), - data_map.inner.clone(), - metadata.inner.clone(), - ); - Arc::new(Self { inner: archive }) - } - - pub fn rename_file( - &self, - old_path: String, - new_path: String, - ) -> Result, ArchiveError> { - let mut archive = self.inner.clone(); - archive - .rename_file( - &std::path::PathBuf::from(&old_path), - &std::path::PathBuf::from(&new_path), - ) - .map_err(|e| ArchiveError::InvalidArchive { - reason: format!("Failed to rename file: {}", e), - })?; - Ok(Arc::new(Self { inner: archive })) - } - - pub fn files(&self) -> Vec { - self.inner - .map() - .iter() - .map(|(path, (data_map, meta))| PrivateArchiveFileEntry { - path: path.to_string_lossy().to_string(), - data_map: Arc::new(DataMapChunk { - inner: data_map.clone(), - }), - metadata: Arc::new(Metadata { - inner: meta.clone(), - }), - }) - .collect() - } - - pub fn file_count(&self) -> u64 { - self.inner.map().len() as u64 - } - - pub fn data_maps(&self) -> Vec> { - self.inner - .data_maps() - .into_iter() - .map(|dm| Arc::new(DataMapChunk { inner: dm })) - .collect() - } -} diff --git a/ffi/rust/ant-ffi/src/graph.rs b/ffi/rust/ant-ffi/src/graph.rs deleted file mode 100644 index 0fd090c..0000000 --- a/ffi/rust/ant-ffi/src/graph.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::keys::{PublicKey, SecretKey}; -use autonomi::client::graph::{ - GraphEntry as AutonomiGraphEntry, GraphEntryAddress as AutonomiGraphEntryAddress, -}; -use std::sync::Arc; - -#[derive(Debug, uniffi::Error, thiserror::Error)] -pub enum GraphEntryError { - #[error("Invalid content: {reason}")] - InvalidContent { reason: String }, - #[error("Parsing failed: {reason}")] - ParsingFailed { reason: String }, -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct GraphEntryAddress { - pub(crate) inner: AutonomiGraphEntryAddress, -} - -#[uniffi::export] -impl GraphEntryAddress { - #[uniffi::constructor] - pub fn new(public_key: Arc) -> Arc { - Arc::new(Self { - inner: AutonomiGraphEntryAddress::new(public_key.inner), - }) - } - - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, GraphEntryError> { - let inner = AutonomiGraphEntryAddress::from_hex(&hex).map_err(|e| { - GraphEntryError::ParsingFailed { - reason: e.to_string(), - } - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} - -#[derive(uniffi::Record, Clone, Debug)] -pub struct GraphDescendant { - pub public_key: Arc, - pub content: Vec, -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct GraphEntry { - pub(crate) inner: AutonomiGraphEntry, -} - -#[uniffi::export] -impl GraphEntry { - #[uniffi::constructor] - pub fn new( - owner: Arc, - parents: Vec>, - content: Vec, - descendants: Vec, - ) -> Result, GraphEntryError> { - if content.len() != 32 { - return Err(GraphEntryError::InvalidContent { - reason: format!("Content must be exactly 32 bytes, got {}", content.len()), - }); - } - - let mut content_array = [0u8; 32]; - content_array.copy_from_slice(&content); - - let descendants_mapped: Vec<(blsttc::PublicKey, [u8; 32])> = descendants - .into_iter() - .map(|d| { - if d.content.len() != 32 { - return Err(GraphEntryError::InvalidContent { - reason: format!( - "Descendant content must be exactly 32 bytes, got {}", - d.content.len() - ), - }); - } - let mut desc_content = [0u8; 32]; - desc_content.copy_from_slice(&d.content); - Ok((d.public_key.inner, desc_content)) - }) - .collect::, GraphEntryError>>()?; - - let inner = AutonomiGraphEntry::new( - &owner.inner, - parents.into_iter().map(|p| p.inner).collect(), - content_array, - descendants_mapped, - ); - - Ok(Arc::new(Self { inner })) - } - - pub fn address(&self) -> Arc { - Arc::new(GraphEntryAddress { - inner: self.inner.address(), - }) - } - - pub fn content(&self) -> Vec { - self.inner.content.to_vec() - } - - pub fn parents(&self) -> Vec> { - self.inner - .parents - .iter() - .map(|&p| Arc::new(PublicKey { inner: p })) - .collect() - } - - pub fn descendants(&self) -> Vec { - self.inner - .descendants - .iter() - .map(|&(pk, c)| GraphDescendant { - public_key: Arc::new(PublicKey { inner: pk }), - content: c.to_vec(), - }) - .collect() - } -} diff --git a/ffi/rust/ant-ffi/src/key_derivation.rs b/ffi/rust/ant-ffi/src/key_derivation.rs deleted file mode 100644 index 3a97e28..0000000 --- a/ffi/rust/ant-ffi/src/key_derivation.rs +++ /dev/null @@ -1,237 +0,0 @@ -use autonomi::client::key_derivation::{ - DerivationIndex as AutonomiDerivationIndex, DerivedPubkey as AutonomiDerivedPubkey, - DerivedSecretKey as AutonomiDerivedSecretKey, MainPubkey as AutonomiMainPubkey, - MainSecretKey as AutonomiMainSecretKey, -}; -use blsttc::Signature as AutonomiSignature; -use blsttc::rand as bls_rand; -use std::sync::Arc; - -use crate::keys::{KeyError, PublicKey, SecretKey}; - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct DerivationIndex { - pub(crate) inner: AutonomiDerivationIndex, -} - -#[uniffi::export] -impl DerivationIndex { - #[uniffi::constructor] - pub fn random() -> Arc { - Arc::new(Self { - inner: AutonomiDerivationIndex::random(&mut bls_rand::thread_rng()), - }) - } - - #[uniffi::constructor] - pub fn from_bytes(bytes: Vec) -> Result, KeyError> { - if bytes.len() != 32 { - return Err(KeyError::InvalidKey { - reason: format!( - "DerivationIndex must be exactly 32 bytes, got {}", - bytes.len() - ), - }); - } - let mut array = [0u8; 32]; - array.copy_from_slice(&bytes); - Ok(Arc::new(Self { - inner: AutonomiDerivationIndex::from_bytes(array), - })) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.into_bytes().to_vec() - } -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct Signature { - pub(crate) inner: AutonomiSignature, -} - -#[uniffi::export] -impl Signature { - #[uniffi::constructor] - pub fn from_bytes(bytes: Vec) -> Result, KeyError> { - if bytes.len() != 96 { - return Err(KeyError::InvalidKey { - reason: format!("Signature must be exactly 96 bytes, got {}", bytes.len()), - }); - } - let mut array = [0u8; 96]; - array.copy_from_slice(&bytes); - AutonomiSignature::from_bytes(array) - .map(|inner| Arc::new(Self { inner })) - .map_err(|e| KeyError::ParsingFailed { - reason: format!("Invalid signature: {}", e), - }) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes().to_vec() - } - - pub fn parity(&self) -> bool { - self.inner.parity() - } - - pub fn to_hex(&self) -> String { - hex::encode(self.inner.to_bytes()) - } -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct MainSecretKey { - pub(crate) inner: AutonomiMainSecretKey, -} - -#[uniffi::export] -impl MainSecretKey { - #[uniffi::constructor] - pub fn new(secret_key: Arc) -> Arc { - Arc::new(Self { - inner: AutonomiMainSecretKey::new(secret_key.inner.clone()), - }) - } - - #[uniffi::constructor] - pub fn random() -> Arc { - Arc::new(Self { - inner: AutonomiMainSecretKey::random(), - }) - } - - pub fn public_key(&self) -> Arc { - Arc::new(MainPubkey { - inner: self.inner.public_key(), - }) - } - - pub fn sign(&self, msg: Vec) -> Arc { - Arc::new(Signature { - inner: self.inner.sign(&msg), - }) - } - - pub fn derive_key(&self, index: Arc) -> Arc { - Arc::new(DerivedSecretKey { - inner: self.inner.derive_key(&index.inner), - }) - } - - pub fn random_derived_key(&self) -> Arc { - Arc::new(DerivedSecretKey { - inner: self.inner.random_derived_key(&mut bls_rand::thread_rng()), - }) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes() - } -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct MainPubkey { - pub(crate) inner: AutonomiMainPubkey, -} - -#[uniffi::export] -impl MainPubkey { - #[uniffi::constructor] - pub fn new(public_key: Arc) -> Arc { - Arc::new(Self { - inner: AutonomiMainPubkey::new(public_key.inner), - }) - } - - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, KeyError> { - AutonomiMainPubkey::from_hex(&hex) - .map(|inner| Arc::new(Self { inner })) - .map_err(|e| KeyError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - }) - } - - pub fn verify(&self, signature: Arc, msg: Vec) -> bool { - self.inner.verify(&signature.inner, &msg) - } - - pub fn derive_key(&self, index: Arc) -> Arc { - Arc::new(DerivedPubkey { - inner: self.inner.derive_key(&index.inner), - }) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes().to_vec() - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct DerivedSecretKey { - pub(crate) inner: AutonomiDerivedSecretKey, -} - -#[uniffi::export] -impl DerivedSecretKey { - #[uniffi::constructor] - pub fn new(secret_key: Arc) -> Arc { - Arc::new(Self { - inner: AutonomiDerivedSecretKey::new(secret_key.inner.clone()), - }) - } - - pub fn public_key(&self) -> Arc { - Arc::new(DerivedPubkey { - inner: self.inner.public_key(), - }) - } - - pub fn sign(&self, msg: Vec) -> Arc { - Arc::new(Signature { - inner: self.inner.sign(&msg), - }) - } -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct DerivedPubkey { - pub(crate) inner: AutonomiDerivedPubkey, -} - -#[uniffi::export] -impl DerivedPubkey { - #[uniffi::constructor] - pub fn new(public_key: Arc) -> Arc { - Arc::new(Self { - inner: AutonomiDerivedPubkey::new(public_key.inner), - }) - } - - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, KeyError> { - AutonomiDerivedPubkey::from_hex(&hex) - .map(|inner| Arc::new(Self { inner })) - .map_err(|e| KeyError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - }) - } - - pub fn verify(&self, signature: Arc, msg: Vec) -> bool { - self.inner.verify(&signature.inner, &msg) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes().to_vec() - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} diff --git a/ffi/rust/ant-ffi/src/keys.rs b/ffi/rust/ant-ffi/src/keys.rs deleted file mode 100644 index b00a3a6..0000000 --- a/ffi/rust/ant-ffi/src/keys.rs +++ /dev/null @@ -1,63 +0,0 @@ -use blsttc::{PublicKey as AutonomiPublicKey, SecretKey as AutonomiSecretKey}; -use std::sync::Arc; - -#[derive(Debug, uniffi::Error, thiserror::Error)] -pub enum KeyError { - #[error("Invalid key: {reason}")] - InvalidKey { reason: String }, - #[error("Parsing failed: {reason}")] - ParsingFailed { reason: String }, -} - -#[derive(uniffi::Object, Clone, Debug)] -pub struct SecretKey { - pub(crate) inner: AutonomiSecretKey, -} - -#[uniffi::export] -impl SecretKey { - #[uniffi::constructor] - pub fn random() -> Arc { - Arc::new(Self { - inner: AutonomiSecretKey::random(), - }) - } - - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, KeyError> { - let inner = AutonomiSecretKey::from_hex(&hex).map_err(|e| KeyError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } - - pub fn public_key(&self) -> Arc { - Arc::new(PublicKey { - inner: self.inner.public_key(), - }) - } -} - -#[derive(uniffi::Object, Clone, Copy, Debug)] -pub struct PublicKey { - pub(crate) inner: AutonomiPublicKey, -} - -#[uniffi::export] -impl PublicKey { - #[uniffi::constructor] - pub fn from_hex(hex: String) -> Result, KeyError> { - let inner = AutonomiPublicKey::from_hex(&hex).map_err(|e| KeyError::ParsingFailed { - reason: format!("Failed to parse hex: {}", e), - })?; - Ok(Arc::new(Self { inner })) - } - - pub fn to_hex(&self) -> String { - self.inner.to_hex() - } -} diff --git a/ffi/rust/ant-ffi/src/lib.rs b/ffi/rust/ant-ffi/src/lib.rs index d0a0498..21b25de 100644 --- a/ffi/rust/ant-ffi/src/lib.rs +++ b/ffi/rust/ant-ffi/src/lib.rs @@ -1,119 +1,95 @@ -use std::sync::Arc; - mod client; mod data; -mod files; -mod graph; -mod key_derivation; -mod keys; -mod network; -mod payment; +mod wallet; + pub use client::Client; -pub use data::{Chunk, ChunkAddress, DataAddress, DataError, DataMapChunk}; -pub use files::{ - ArchiveAddress, ArchiveError, Metadata, PrivateArchive, PrivateArchiveDataMap, - PrivateArchiveFileEntry, PublicArchive, PublicArchiveFileEntry, -}; -pub use graph::{GraphDescendant, GraphEntry, GraphEntryAddress, GraphEntryError}; -pub use key_derivation::{ - DerivationIndex, DerivedPubkey, DerivedSecretKey, MainPubkey, MainSecretKey, Signature, -}; -pub use keys::{KeyError, PublicKey, SecretKey}; -pub use network::{Network, NetworkError}; -pub use payment::PaymentOption; +pub use data::{DataUploadResult, FileUploadResult}; +pub use wallet::Wallet; uniffi::setup_scaffolding!(); // ===== Result types ===== -/// Result of uploading data to the network -#[derive(uniffi::Record)] -pub struct UploadResult { - pub price: String, - pub address: String, -} - -/// Result of uploading a chunk to the network +/// Result of storing a chunk on the network. #[derive(uniffi::Record)] pub struct ChunkPutResult { - pub cost: String, - pub address: Arc, -} - -/// Result of uploading private data to the network -#[derive(uniffi::Record)] -pub struct DataPutResult { - pub cost: String, - pub data_map: Arc, -} - -/// Result of uploading a graph entry to the network -#[derive(uniffi::Record)] -pub struct GraphEntryPutResult { - pub cost: String, - pub address: Arc, -} - -/// Result of uploading a public archive to the network -#[derive(uniffi::Record)] -pub struct PublicArchivePutResult { - pub cost: String, - pub address: Arc, -} - -/// Result of uploading a private archive to the network -#[derive(uniffi::Record)] -pub struct PrivateArchivePutResult { - pub cost: String, - pub data_map: Arc, -} - -/// Result of uploading a file to the network (private) -#[derive(uniffi::Record)] -pub struct FileUploadResult { - pub cost: String, - pub data_map: Arc, + /// Hex-encoded chunk address (32 bytes). + pub address: String, } -/// Result of uploading a file to the network (public) +/// Result of a public data upload (data map stored as public chunk). #[derive(uniffi::Record)] -pub struct FileUploadPublicResult { - pub cost: String, - pub address: Arc, +pub struct DataPutPublicResult { + /// Hex-encoded address of the stored data map. + pub address: String, + /// Number of chunks stored. + pub chunks_stored: u64, + /// Payment mode that was used: "auto", "merkle", or "single". + pub payment_mode_used: String, } -/// Result of uploading a directory to the network (private) +/// Result of a private data upload (data map returned to caller). #[derive(uniffi::Record)] -pub struct DirUploadResult { - pub cost: String, - pub data_map: Arc, +pub struct DataPutPrivateResult { + /// Hex-encoded serialized data map (caller keeps this secret). + pub data_map: String, + /// Number of chunks stored. + pub chunks_stored: u64, + /// Payment mode that was used. + pub payment_mode_used: String, } -/// Result of uploading a public directory to the network +/// Result of uploading a file (public). #[derive(uniffi::Record)] -pub struct DirUploadPublicResult { - pub cost: String, - pub address: Arc, +pub struct FilePutPublicResult { + /// Hex-encoded address of the stored data map. + pub address: String, } // ===== Error types ===== -/// Error type for Autonomi Client operations +/// Error type for client operations. #[derive(Debug, uniffi::Error, thiserror::Error)] pub enum ClientError { + #[error("Initialization failed: {reason}")] + InitializationFailed { reason: String }, #[error("Network error: {reason}")] NetworkError { reason: String }, - #[error("Client initialization failed: {reason}")] - InitializationFailed { reason: String }, - #[error("Invalid data address: {reason}")] - InvalidAddress { reason: String }, + #[error("Payment error: {reason}")] + PaymentError { reason: String }, + #[error("Invalid input: {reason}")] + InvalidInput { reason: String }, + #[error("Not found: {reason}")] + NotFound { reason: String }, + #[error("Already exists")] + AlreadyExists, + #[error("Wallet not configured")] + WalletNotConfigured, + #[error("Internal error: {reason}")] + InternalError { reason: String }, +} + +/// Map ant-core errors to FFI errors. +impl From for ClientError { + fn from(e: ant_core::data::Error) -> Self { + use ant_core::data::Error; + match e { + Error::AlreadyStored => ClientError::AlreadyExists, + Error::InvalidData(msg) => ClientError::InvalidInput { reason: msg }, + Error::Payment(msg) => ClientError::PaymentError { reason: msg }, + Error::Network(msg) => ClientError::NetworkError { reason: msg }, + Error::Timeout(msg) => ClientError::NetworkError { reason: format!("timeout: {msg}") }, + Error::InsufficientPeers(msg) => ClientError::NetworkError { reason: msg }, + other => ClientError::InternalError { reason: other.to_string() }, + } + } } -/// Error type for Wallet operations +/// Error type for wallet operations. #[derive(Debug, uniffi::Error, thiserror::Error)] pub enum WalletError { #[error("Wallet creation failed: {reason}")] CreationFailed { reason: String }, - #[error("Balance check failed: {reason}")] - BalanceCheckFailed { reason: String }, + #[error("Operation failed: {reason}")] + OperationFailed { reason: String }, } diff --git a/ffi/rust/ant-ffi/src/network.rs b/ffi/rust/ant-ffi/src/network.rs deleted file mode 100644 index 5be2e4b..0000000 --- a/ffi/rust/ant-ffi/src/network.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::sync::Arc; - -#[derive(Debug, uniffi::Error, thiserror::Error)] -pub enum NetworkError { - #[error("Network creation failed: {reason}")] - CreationFailed { reason: String }, -} - -#[derive(uniffi::Object)] -pub struct Network { - pub(crate) inner: autonomi::Network, -} - -#[uniffi::export] -impl Network { - #[uniffi::constructor] - pub fn new(is_local: bool) -> Result, NetworkError> { - let network = - autonomi::Network::new(is_local).map_err(|e| NetworkError::CreationFailed { - reason: e.to_string(), - })?; - Ok(Arc::new(Self { inner: network })) - } - - #[uniffi::constructor] - pub fn custom( - rpc_url: String, - payment_token_address: String, - data_payments_address: String, - royalties_pk_hex: Option, - ) -> Arc { - let network = autonomi::Network::new_custom( - &rpc_url, - &payment_token_address, - &data_payments_address, - royalties_pk_hex.as_deref(), - ); - Arc::new(Self { inner: network }) - } -} diff --git a/ffi/rust/ant-ffi/src/payment.rs b/ffi/rust/ant-ffi/src/payment.rs deleted file mode 100644 index c5e3904..0000000 --- a/ffi/rust/ant-ffi/src/payment.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::sync::Arc; - -use crate::network::Network; -use crate::WalletError; - -/// Wallet for paying for operations on the Autonomi network -#[derive(uniffi::Object)] -pub struct Wallet { - pub(crate) inner: autonomi::Wallet, -} - -#[uniffi::export(async_runtime = "tokio")] -impl Wallet { - #[uniffi::constructor] - pub fn new_from_private_key( - network: Arc, - private_key: String, - ) -> Result, WalletError> { - let wallet = autonomi::Wallet::new_from_private_key(network.inner.clone(), &private_key) - .map_err(|e| WalletError::CreationFailed { - reason: e.to_string(), - })?; - Ok(Arc::new(Self { inner: wallet })) - } - - pub fn address(&self) -> String { - self.inner.address().to_string() - } - - pub async fn balance_of_tokens(&self) -> Result { - let balance = - self.inner - .balance_of_tokens() - .await - .map_err(|e| WalletError::BalanceCheckFailed { - reason: e.to_string(), - })?; - Ok(balance.to_string()) - } -} - -/// Payment option for paid operations -#[derive(uniffi::Enum)] -pub enum PaymentOption { - WalletPayment { wallet_ref: Arc }, -} diff --git a/ffi/rust/ant-ffi/src/wallet.rs b/ffi/rust/ant-ffi/src/wallet.rs new file mode 100644 index 0000000..566fc4e --- /dev/null +++ b/ffi/rust/ant-ffi/src/wallet.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use crate::WalletError; + +/// EVM wallet for paying storage costs. +#[derive(uniffi::Object)] +pub struct Wallet { + pub(crate) inner: evmlib::wallet::Wallet, +} + +#[uniffi::export(async_runtime = "tokio")] +impl Wallet { + /// Create a wallet from an EVM private key. + /// + /// Uses the default Autonomi EVM network configuration. + #[uniffi::constructor] + pub fn from_private_key( + private_key: String, + rpc_url: String, + payment_token_address: String, + data_payments_address: String, + ) -> Result, WalletError> { + let network = evmlib::Network::new_custom( + &rpc_url, + &payment_token_address, + &data_payments_address, + None, + ); + let wallet = evmlib::wallet::Wallet::new_from_private_key(network, &private_key) + .map_err(|e| WalletError::CreationFailed { + reason: e.to_string(), + })?; + Ok(Arc::new(Self { inner: wallet })) + } + + /// Get the wallet's public address (hex with 0x prefix). + pub fn address(&self) -> String { + format!("{:#x}", self.inner.address()) + } + + /// Get the wallet's token balance (atto tokens as decimal string). + pub async fn balance_of_tokens(&self) -> Result { + let balance = self + .inner + .balance_of_tokens() + .await + .map_err(|e| WalletError::OperationFailed { + reason: e.to_string(), + })?; + Ok(balance.to_string()) + } + + /// Get the wallet's gas token balance (wei as decimal string). + pub async fn balance_of_gas_tokens(&self) -> Result { + let balance = self + .inner + .balance_of_gas_tokens() + .await + .map_err(|e| WalletError::OperationFailed { + reason: e.to_string(), + })?; + Ok(balance.to_string()) + } +} From 6cea8fb9f757d9d64e87737c8cacad55c83aa855 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 10:21:46 +0100 Subject: [PATCH 13/20] Remove v1 autonomi repo prerequisite and update gem metadata - README: Remove outdated prerequisite to clone maidsafe/autonomi repo (antd now uses ant-core via git dependency, no sibling clone needed) - Ruby gemspec: Update author/email from MaidSafe to WithAutonomi Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 3 +-- antd-ruby/antd.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d405cc..fd560e4 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,8 @@ All data and file upload operations accept an optional `payment_mode` parameter **Required:** -- **Rust** toolchain — for building antd and the Autonomi network +- **Rust** toolchain — for building antd - **Python 3.10+** — for the dev CLI (`ant-dev`) and MCP server -- **autonomi** repo cloned as a sibling: `git clone https://github.com/maidsafe/autonomi ../autonomi` **Language-specific** (install only what you need): diff --git a/antd-ruby/antd.gemspec b/antd-ruby/antd.gemspec index e551f03..83ea26e 100644 --- a/antd-ruby/antd.gemspec +++ b/antd-ruby/antd.gemspec @@ -5,8 +5,8 @@ require_relative "lib/antd/version" Gem::Specification.new do |spec| spec.name = "antd" spec.version = Antd::VERSION - spec.authors = ["MaidSafe"] - spec.email = ["dev@maidsafe.net"] + spec.authors = ["WithAutonomi"] + spec.email = ["dev@autonomi.com"] spec.summary = "Ruby SDK for the antd daemon" spec.description = "REST client for the antd daemon — the gateway to the Autonomi decentralized network." From 5e2f9945907ca55b02a307f9cb3d39310de8b49f Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 11:24:37 +0100 Subject: [PATCH 14/20] Add external signer support for two-phase uploads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables applications like indelible to manage their own wallet keys without passing private keys to antd. The upload flow is split into prepare (encrypt + collect quotes) and finalize (submit proofs after external payment). antd changes: - POST /v1/upload/prepare — encrypts file, collects quotes, returns PaymentIntent with quote hashes, amounts, contract addresses, RPC URL - POST /v1/upload/finalize — accepts tx_hash map, builds proofs, uploads chunks, returns address - AppState holds pending uploads in-memory (server-side state since PreparedUpload contains non-serializable P2P types) - main.rs: configure EVM network even without wallet (with_evm_network) so prepare works in external-signer-only mode - Updated ant-core to b9c08d61 (includes PR #10 external signer) SDK bindings: prepare_upload() and finalize_upload() added to all 15 language SDKs + 2 new MCP tools. Docs updated. No private key crosses the REST API — the key stays in the external signer (WalletConnect, HSM, app key store). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 10 +- antd-cpp/include/antd/client.hpp | 10 ++ antd-cpp/include/antd/models.hpp | 23 ++++ antd-cpp/src/client.cpp | 48 ++++++++ antd-csharp/Antd.Sdk/AntdRestClient.cs | 38 ++++++ antd-csharp/Antd.Sdk/IAntdClient.cs | 4 + antd-csharp/Antd.Sdk/Models.cs | 15 +++ antd-dart/lib/src/client.dart | 20 +++ antd-dart/lib/src/models.dart | 84 +++++++++++++ antd-elixir/lib/antd/client.ex | 62 ++++++++++ antd-elixir/lib/antd/models.ex | 41 +++++++ antd-go/client.go | 47 ++++++++ antd-go/models.go | 23 ++++ .../java/com/autonomi/antd/AntdClient.java | 39 ++++++ .../antd/models/FinalizeUploadResult.java | 9 ++ .../com/autonomi/antd/models/PaymentInfo.java | 10 ++ .../antd/models/PrepareUploadResult.java | 21 ++++ antd-js/src/index.ts | 3 + antd-js/src/models.ts | 23 ++++ antd-js/src/rest-client.ts | 41 +++++++ .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 22 ++++ .../kotlin/com/autonomi/sdk/IAntdClient.kt | 4 + .../main/kotlin/com/autonomi/sdk/Models.kt | 39 ++++++ antd-lua/src/antd/client.lua | 50 ++++++++ antd-mcp/src/antd_mcp/server.py | 74 ++++++++++++ antd-php/src/AntdClient.php | 78 ++++++++++++ antd-py/src/antd/__init__.py | 3 + antd-py/src/antd/_rest.py | 89 ++++++++++++++ antd-py/src/antd/models.py | 26 ++++ antd-ruby/lib/antd/client.rb | 36 ++++++ antd-ruby/lib/antd/models.rb | 9 ++ antd-rust/src/client.rs | 60 +++++++++ antd-rust/src/models.rs | 37 ++++++ .../Sources/AntdSdk/AntdRestClient.swift | 36 ++++++ antd-swift/Sources/AntdSdk/Models.swift | 43 +++++++ antd-zig/src/antd.zig | 22 ++++ antd/Cargo.lock | 5 +- antd/src/main.rs | 21 +++- antd/src/rest/mod.rs | 4 + antd/src/rest/upload.rs | 114 ++++++++++++++++++ antd/src/state.rs | 6 +- antd/src/types.rs | 49 ++++++++ llms-full.txt | 25 ++++ llms.txt | 2 + skill.md | 18 +++ 45 files changed, 1438 insertions(+), 5 deletions(-) create mode 100644 antd-java/src/main/java/com/autonomi/antd/models/FinalizeUploadResult.java create mode 100644 antd-java/src/main/java/com/autonomi/antd/models/PaymentInfo.java create mode 100644 antd-java/src/main/java/com/autonomi/antd/models/PrepareUploadResult.java create mode 100644 antd/src/rest/upload.rs diff --git a/README.md b/README.md index fd560e4..a86fd8d 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ This is especially useful in managed mode, where a parent process (e.g. indelibl If no port file is found, all SDKs fall back to the default REST endpoint (`http://localhost:8082`) or gRPC target (`localhost:50051`). +### External Signer Support + +All SDKs support two-phase uploads for applications that manage their own wallet (browser wallets, hardware signers, etc.): + +1. **`prepare_upload(path)`** -- returns payment details (quote hashes, amounts, contract addresses, RPC URL) +2. Your application submits EVM payment transactions using its own signer +3. **`finalize_upload(upload_id, tx_hashes)`** -- confirms payments and stores data on the network + ### Payment Modes All data and file upload operations accept an optional `payment_mode` parameter (defaults to `"auto"`): @@ -75,7 +83,7 @@ All data and file upload operations accept an optional `payment_mode` parameter | Component | Language | Description | |-----------|----------|-------------| | [`antd/`](antd/) | Rust | REST + gRPC gateway daemon | -| [`antd-mcp/`](antd-mcp/) | Python | MCP server exposing 14 tools for AI agents (Claude, etc.) | +| [`antd-mcp/`](antd-mcp/) | Python | MCP server exposing 19 tools for AI agents (Claude, etc.) | | [`ant-dev/`](ant-dev/) | Python | Developer CLI for local environment management | ### Language SDKs diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index ad3b7ee..aefe0c5 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -124,6 +125,15 @@ class Client { /// Approve the wallet to spend tokens on payment contracts (one-time operation). bool wallet_approve(); + // --- External Signer (Two-Phase Upload) --- + + /// Prepare a file upload for external signing. + PrepareUploadResult prepare_upload(std::string_view path); + + /// Finalize an upload after an external signer has submitted payment transactions. + FinalizeUploadResult finalize_upload(std::string_view upload_id, + const std::map& tx_hashes); + private: struct Impl; std::unique_ptr impl_; diff --git a/antd-cpp/include/antd/models.hpp b/antd-cpp/include/antd/models.hpp index 4311237..618f3e5 100644 --- a/antd-cpp/include/antd/models.hpp +++ b/antd-cpp/include/antd/models.hpp @@ -57,4 +57,27 @@ struct WalletBalance { std::string gas_balance; // atto tokens as string }; +/// A single payment required for an upload. +struct PaymentInfo { + std::string quote_hash; // hex + std::string rewards_address; // hex + std::string amount; // atto tokens as string +}; + +/// Result of preparing an upload for external signing. +struct PrepareUploadResult { + std::string upload_id; // hex identifier + std::vector payments; + std::string total_amount; + std::string data_payments_address; // contract address + std::string payment_token_address; // token contract address + std::string rpc_url; // EVM RPC URL +}; + +/// Result of finalizing an externally-signed upload. +struct FinalizeUploadResult { + std::string address; // hex address of stored data + int64_t chunks_stored{0}; +}; + } // namespace antd diff --git a/antd-cpp/src/client.cpp b/antd-cpp/src/client.cpp index a9671f7..49baf71 100644 --- a/antd-cpp/src/client.cpp +++ b/antd-cpp/src/client.cpp @@ -357,4 +357,52 @@ bool Client::wallet_approve() { return j.value("approved", false); } +// --------------------------------------------------------------------------- +// External Signer (Two-Phase Upload) +// --------------------------------------------------------------------------- + +PrepareUploadResult Client::prepare_upload(std::string_view path) { + auto j = impl_->do_json("POST", "/v1/upload/prepare", json{ + {"path", std::string(path)}, + }); + + PrepareUploadResult result; + result.upload_id = j.value("upload_id", ""); + result.total_amount = j.value("total_amount", ""); + result.data_payments_address = j.value("data_payments_address", ""); + result.payment_token_address = j.value("payment_token_address", ""); + result.rpc_url = j.value("rpc_url", ""); + + if (j.contains("payments") && j["payments"].is_array()) { + for (const auto& p : j["payments"]) { + if (p.is_object()) { + result.payments.push_back(PaymentInfo{ + .quote_hash = p.value("quote_hash", ""), + .rewards_address = p.value("rewards_address", ""), + .amount = p.value("amount", ""), + }); + } + } + } + + return result; +} + +FinalizeUploadResult Client::finalize_upload(std::string_view upload_id, + const std::map& tx_hashes) { + json hashes = json::object(); + for (const auto& [k, v] : tx_hashes) { + hashes[k] = v; + } + + auto j = impl_->do_json("POST", "/v1/upload/finalize", json{ + {"upload_id", std::string(upload_id)}, + {"tx_hashes", hashes}, + }); + return FinalizeUploadResult{ + .address = j.value("address", ""), + .chunks_stored = j.value("chunks_stored", int64_t{0}), + }; +} + } // namespace antd diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index 6030f80..2e26928 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -255,6 +255,27 @@ public async Task WalletApproveAsync() return resp.Approved; } + // ── External Signer (Two-Phase Upload) ── + + /// + /// Prepares a file upload for external signing. + /// + public async Task PrepareUploadAsync(string path) + { + var resp = await PostJsonAsync("/v1/upload/prepare", new { path }); + var payments = resp.Payments?.Select(p => new PaymentInfo(p.QuoteHash, p.RewardsAddress, p.Amount)).ToList() ?? []; + return new PrepareUploadResult(resp.UploadId, payments, resp.TotalAmount, resp.DataPaymentsAddress, resp.PaymentTokenAddress, resp.RpcUrl); + } + + /// + /// 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 }); + return new FinalizeUploadResult(resp.Address, resp.ChunksStored); + } + // ── Internal DTOs for JSON deserialization ── private sealed record HealthResponseDto( @@ -304,4 +325,21 @@ private sealed record WalletBalanceDto( private sealed record WalletApproveDto( [property: JsonPropertyName("approved")] bool Approved); + + private sealed record PaymentInfoDto( + [property: JsonPropertyName("quote_hash")] string QuoteHash, + [property: JsonPropertyName("rewards_address")] string RewardsAddress, + [property: JsonPropertyName("amount")] string Amount); + + private sealed record PrepareUploadDto( + [property: JsonPropertyName("upload_id")] string UploadId, + [property: JsonPropertyName("payments")] List? Payments, + [property: JsonPropertyName("total_amount")] string TotalAmount, + [property: JsonPropertyName("data_payments_address")] string DataPaymentsAddress, + [property: JsonPropertyName("payment_token_address")] string PaymentTokenAddress, + [property: JsonPropertyName("rpc_url")] string RpcUrl); + + private sealed record FinalizeUploadDto( + [property: JsonPropertyName("address")] string Address, + [property: JsonPropertyName("chunks_stored")] long ChunksStored); } diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index c63b1a4..6ee945b 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -35,4 +35,8 @@ public interface IAntdClient : IDisposable Task WalletAddressAsync(); Task WalletBalanceAsync(); Task WalletApproveAsync(); + + // External Signer (Two-Phase Upload) + Task PrepareUploadAsync(string path); + Task FinalizeUploadAsync(string uploadId, Dictionary txHashes); } diff --git a/antd-csharp/Antd.Sdk/Models.cs b/antd-csharp/Antd.Sdk/Models.cs index 6f67c80..17f9380 100644 --- a/antd-csharp/Antd.Sdk/Models.cs +++ b/antd-csharp/Antd.Sdk/Models.cs @@ -23,3 +23,18 @@ public sealed record WalletAddress(string Address); /// Wallet balance from the antd daemon. public sealed record WalletBalance(string Balance, string GasBalance); + +/// A single payment required for an upload. +public sealed record PaymentInfo(string QuoteHash, string RewardsAddress, string Amount); + +/// Result of preparing an upload for external signing. +public sealed record PrepareUploadResult( + string UploadId, + List Payments, + string TotalAmount, + string DataPaymentsAddress, + string PaymentTokenAddress, + string RpcUrl); + +/// Result of finalizing an externally-signed upload. +public sealed record FinalizeUploadResult(string Address, long ChunksStored); diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index a19e741..1f23435 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -328,4 +328,24 @@ class AntdClient { final json = await _doJson('POST', '/v1/wallet/approve', {}); return json!['approved'] as bool; } + + // --- External Signer (Two-Phase Upload) --- + + /// Prepares a file upload for external signing. + Future prepareUpload(String path) async { + final json = await _doJson('POST', '/v1/upload/prepare', {'path': path}); + return PrepareUploadResult.fromJson(json!); + } + + /// Finalizes an upload after an external signer has submitted payment transactions. + Future finalizeUpload( + String uploadId, + Map txHashes, + ) async { + final json = await _doJson('POST', '/v1/upload/finalize', { + 'upload_id': uploadId, + 'tx_hashes': txHashes, + }); + return FinalizeUploadResult.fromJson(json!); + } } diff --git a/antd-dart/lib/src/models.dart b/antd-dart/lib/src/models.dart index 7e28774..425ae2c 100644 --- a/antd-dart/lib/src/models.dart +++ b/antd-dart/lib/src/models.dart @@ -216,3 +216,87 @@ class WalletBalance { String toString() => 'WalletBalance(balance: $balance, gasBalance: $gasBalance)'; } + +/// A single payment required for an upload. +class PaymentInfo { + final String quoteHash; + final String rewardsAddress; + final String amount; + + const PaymentInfo({ + required this.quoteHash, + required this.rewardsAddress, + required this.amount, + }); + + factory PaymentInfo.fromJson(Map json) { + return PaymentInfo( + quoteHash: json['quote_hash'] as String? ?? '', + rewardsAddress: json['rewards_address'] as String? ?? '', + amount: json['amount'] as String? ?? '', + ); + } + + @override + String toString() => + 'PaymentInfo(quoteHash: $quoteHash, rewardsAddress: $rewardsAddress, amount: $amount)'; +} + +/// Result of preparing an upload for external signing. +class PrepareUploadResult { + final String uploadId; + final List payments; + final String totalAmount; + final String dataPaymentsAddress; + final String paymentTokenAddress; + final String rpcUrl; + + const PrepareUploadResult({ + required this.uploadId, + required this.payments, + required this.totalAmount, + required this.dataPaymentsAddress, + required this.paymentTokenAddress, + required this.rpcUrl, + }); + + factory PrepareUploadResult.fromJson(Map json) { + return PrepareUploadResult( + uploadId: json['upload_id'] as String? ?? '', + payments: (json['payments'] as List?) + ?.map((e) => PaymentInfo.fromJson(e as Map)) + .toList() ?? + [], + totalAmount: json['total_amount'] as String? ?? '', + dataPaymentsAddress: json['data_payments_address'] as String? ?? '', + paymentTokenAddress: json['payment_token_address'] as String? ?? '', + rpcUrl: json['rpc_url'] as String? ?? '', + ); + } + + @override + String toString() => + 'PrepareUploadResult(uploadId: $uploadId, payments: $payments, totalAmount: $totalAmount)'; +} + +/// Result of finalizing an externally-signed upload. +class FinalizeUploadResult { + final String address; + final int chunksStored; + + const FinalizeUploadResult({ + required this.address, + required this.chunksStored, + }); + + factory FinalizeUploadResult.fromJson(Map json) { + return FinalizeUploadResult( + address: json['address'] as String? ?? '', + chunksStored: (json['chunks_stored'] as num?)?.toInt() ?? 0, + ); + } + + @override + String toString() => + 'FinalizeUploadResult(address: $address, chunksStored: $chunksStored)'; +} diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index 01d5b7f..55b5fca 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -496,6 +496,68 @@ defmodule Antd.Client do @spec wallet_approve!(t()) :: boolean() def wallet_approve!(client), do: unwrap!(wallet_approve(client)) + # --------------------------------------------------------------------------- + # External Signer (Two-Phase Upload) + # --------------------------------------------------------------------------- + + @doc "Prepares a file upload for external signing." + @spec prepare_upload(t(), String.t()) :: {:ok, Antd.PrepareUploadResult.t()} | {:error, Exception.t()} + def prepare_upload(%__MODULE__{} = client, path) do + case do_json(client, :post, "/v1/upload/prepare", %{path: path}) do + {:ok, body} -> + payments = + (body["payments"] || []) + |> Enum.map(fn p -> + %Antd.PaymentInfo{ + quote_hash: p["quote_hash"], + rewards_address: p["rewards_address"], + amount: p["amount"] + } + end) + + {:ok, + %Antd.PrepareUploadResult{ + upload_id: body["upload_id"], + payments: payments, + total_amount: body["total_amount"], + data_payments_address: body["data_payments_address"], + payment_token_address: body["payment_token_address"], + rpc_url: body["rpc_url"] + }} + + {:error, _} = err -> + err + end + end + + @doc "Like `prepare_upload/2` but raises on error." + @spec prepare_upload!(t(), String.t()) :: Antd.PrepareUploadResult.t() + def prepare_upload!(client, path), do: unwrap!(prepare_upload(client, path)) + + @doc "Finalizes an upload after an external signer has submitted payment transactions." + @spec finalize_upload(t(), String.t(), map()) :: {:ok, Antd.FinalizeUploadResult.t()} | {:error, Exception.t()} + def finalize_upload(%__MODULE__{} = client, upload_id, tx_hashes) do + payload = %{upload_id: upload_id, tx_hashes: tx_hashes} + + case do_json(client, :post, "/v1/upload/finalize", payload) do + {:ok, body} -> + {:ok, + %Antd.FinalizeUploadResult{ + address: body["address"], + chunks_stored: body["chunks_stored"] + }} + + {:error, _} = err -> + err + end + end + + @doc "Like `finalize_upload/3` but raises on error." + @spec finalize_upload!(t(), String.t(), map()) :: Antd.FinalizeUploadResult.t() + def finalize_upload!(client, upload_id, tx_hashes) do + unwrap!(finalize_upload(client, upload_id, tx_hashes)) + end + # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- diff --git a/antd-elixir/lib/antd/models.ex b/antd-elixir/lib/antd/models.ex index 655493e..bc1cf7d 100644 --- a/antd-elixir/lib/antd/models.ex +++ b/antd-elixir/lib/antd/models.ex @@ -96,3 +96,44 @@ defmodule Antd.WalletBalance do gas_balance: String.t() } end + +defmodule Antd.PaymentInfo do + @moduledoc "A single payment required for an upload." + + @enforce_keys [:quote_hash, :rewards_address, :amount] + defstruct [:quote_hash, :rewards_address, :amount] + + @type t :: %__MODULE__{ + quote_hash: String.t(), + rewards_address: String.t(), + amount: String.t() + } +end + +defmodule Antd.PrepareUploadResult do + @moduledoc "Result of preparing an upload for external signing." + + @enforce_keys [:upload_id, :payments, :total_amount, :data_payments_address, :payment_token_address, :rpc_url] + defstruct [:upload_id, :payments, :total_amount, :data_payments_address, :payment_token_address, :rpc_url] + + @type t :: %__MODULE__{ + upload_id: String.t(), + payments: [Antd.PaymentInfo.t()], + total_amount: String.t(), + data_payments_address: String.t(), + payment_token_address: String.t(), + rpc_url: String.t() + } +end + +defmodule Antd.FinalizeUploadResult do + @moduledoc "Result of finalizing an externally-signed upload." + + @enforce_keys [:address, :chunks_stored] + defstruct [:address, :chunks_stored] + + @type t :: %__MODULE__{ + address: String.t(), + chunks_stored: integer() + } +end diff --git a/antd-go/client.go b/antd-go/client.go index d819296..c6e813a 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -488,3 +488,50 @@ func (c *Client) WalletApprove(ctx context.Context) error { _ = j return nil } + +// --- External Signer (Two-Phase Upload) --- + +// PrepareUpload prepares a file upload for external signing. +// Returns payment details that an external signer must process before calling FinalizeUpload. +func (c *Client) PrepareUpload(ctx context.Context, path string) (*PrepareUploadResult, error) { + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/upload/prepare", map[string]any{ + "path": path, + }) + if err != nil { + return nil, err + } + var payments []PaymentInfo + for _, p := range arrAt(j, "payments") { + if pm, ok := p.(map[string]any); ok { + payments = append(payments, PaymentInfo{ + QuoteHash: str(pm, "quote_hash"), + RewardsAddress: str(pm, "rewards_address"), + Amount: str(pm, "amount"), + }) + } + } + return &PrepareUploadResult{ + UploadID: str(j, "upload_id"), + Payments: payments, + TotalAmount: str(j, "total_amount"), + DataPaymentsAddress: str(j, "data_payments_address"), + PaymentTokenAddress: str(j, "payment_token_address"), + RPCUrl: str(j, "rpc_url"), + }, nil +} + +// FinalizeUpload finalizes an upload after an external signer has submitted payment transactions. +// txHashes maps quote_hash to tx_hash for each payment. +func (c *Client) FinalizeUpload(ctx context.Context, uploadID string, txHashes map[string]string) (*FinalizeUploadResult, error) { + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/upload/finalize", map[string]any{ + "upload_id": uploadID, + "tx_hashes": txHashes, + }) + if err != nil { + return nil, err + } + return &FinalizeUploadResult{ + Address: str(j, "address"), + ChunksStored: num64(j, "chunks_stored"), + }, nil +} diff --git a/antd-go/models.go b/antd-go/models.go index d8d9d74..11045f9 100644 --- a/antd-go/models.go +++ b/antd-go/models.go @@ -50,3 +50,26 @@ type WalletBalance struct { Balance string `json:"balance"` // token balance in atto GasBalance string `json:"gas_balance"` // gas balance in wei } + +// PaymentInfo describes a single payment required for an upload. +type PaymentInfo struct { + QuoteHash string `json:"quote_hash"` // hex + RewardsAddress string `json:"rewards_address"` // hex + Amount string `json:"amount"` // atto tokens as string +} + +// PrepareUploadResult is the result of preparing an upload for external signing. +type PrepareUploadResult struct { + UploadID string `json:"upload_id"` // hex identifier + Payments []PaymentInfo `json:"payments"` // payments to sign + TotalAmount string `json:"total_amount"` // total atto tokens + DataPaymentsAddress string `json:"data_payments_address"` // contract address + PaymentTokenAddress string `json:"payment_token_address"` // token contract address + RPCUrl string `json:"rpc_url"` // EVM RPC URL +} + +// FinalizeUploadResult is the result of finalizing an externally-signed upload. +type FinalizeUploadResult struct { + Address string `json:"address"` // hex address of stored data + ChunksStored int64 `json:"chunks_stored"` // number of chunks stored +} diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index 1d27156..b5ad47c 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -381,4 +381,43 @@ public boolean walletApprove() { Object approved = j.get("approved"); return approved instanceof Boolean b && b; } + + // ── External Signer (Two-Phase Upload) ── + + /** + * Prepares a file upload for external signing. + * Returns payment details that an external signer must process before calling {@link #finalizeUpload}. + * + * @param path local file path to upload + * @return PrepareUploadResult with upload_id, payments, and contract details + */ + public PrepareUploadResult prepareUpload(String path) { + String body = Json.object("path", path); + Map j = doJson("POST", "/v1/upload/prepare", body); + List payments = new ArrayList<>(); + for (Map pm : listOfMaps(j, "payments")) { + payments.add(new PaymentInfo(str(pm, "quote_hash"), str(pm, "rewards_address"), str(pm, "amount"))); + } + return new PrepareUploadResult( + str(j, "upload_id"), + Collections.unmodifiableList(payments), + str(j, "total_amount"), + str(j, "data_payments_address"), + str(j, "payment_token_address"), + str(j, "rpc_url") + ); + } + + /** + * Finalizes an upload after an external signer has submitted payment transactions. + * + * @param uploadId the upload ID returned by {@link #prepareUpload} + * @param txHashes map of quote_hash to tx_hash for each payment + * @return FinalizeUploadResult with address and chunks_stored + */ + public FinalizeUploadResult finalizeUpload(String uploadId, Map txHashes) { + String body = Json.object("upload_id", uploadId, "tx_hashes", txHashes); + Map j = doJson("POST", "/v1/upload/finalize", body); + return new FinalizeUploadResult(str(j, "address"), num(j, "chunks_stored")); + } } diff --git a/antd-java/src/main/java/com/autonomi/antd/models/FinalizeUploadResult.java b/antd-java/src/main/java/com/autonomi/antd/models/FinalizeUploadResult.java new file mode 100644 index 0000000..98ce3d5 --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/models/FinalizeUploadResult.java @@ -0,0 +1,9 @@ +package com.autonomi.antd.models; + +/** + * Result of finalizing an externally-signed upload. + * + * @param address hex address of the stored data + * @param chunksStored number of chunks stored + */ +public record FinalizeUploadResult(String address, long chunksStored) {} diff --git a/antd-java/src/main/java/com/autonomi/antd/models/PaymentInfo.java b/antd-java/src/main/java/com/autonomi/antd/models/PaymentInfo.java new file mode 100644 index 0000000..e8a7d74 --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/models/PaymentInfo.java @@ -0,0 +1,10 @@ +package com.autonomi.antd.models; + +/** + * A single payment required for an upload. + * + * @param quoteHash hex-encoded quote hash + * @param rewardsAddress hex-encoded rewards address + * @param amount amount in atto tokens as string + */ +public record PaymentInfo(String quoteHash, String rewardsAddress, String amount) {} diff --git a/antd-java/src/main/java/com/autonomi/antd/models/PrepareUploadResult.java b/antd-java/src/main/java/com/autonomi/antd/models/PrepareUploadResult.java new file mode 100644 index 0000000..2ed9db5 --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/models/PrepareUploadResult.java @@ -0,0 +1,21 @@ +package com.autonomi.antd.models; + +import java.util.List; + +/** + * Result of preparing an upload for external signing. + * + * @param uploadId hex identifier for this upload session + * @param payments payments that must be signed externally + * @param totalAmount total amount across all payments + * @param dataPaymentsAddress data payments contract address + * @param paymentTokenAddress payment token contract address + * @param rpcUrl EVM RPC URL for submitting transactions + */ +public record PrepareUploadResult( + String uploadId, + List payments, + String totalAmount, + String dataPaymentsAddress, + String paymentTokenAddress, + String rpcUrl) {} diff --git a/antd-js/src/index.ts b/antd-js/src/index.ts index ac2f5e1..bd1b53c 100644 --- a/antd-js/src/index.ts +++ b/antd-js/src/index.ts @@ -7,6 +7,9 @@ export type { Archive, WalletAddress, WalletBalance, + PaymentInfo, + PrepareUploadResult, + FinalizeUploadResult, } from "./models.js"; export { diff --git a/antd-js/src/models.ts b/antd-js/src/models.ts index 0a423e4..e39048d 100644 --- a/antd-js/src/models.ts +++ b/antd-js/src/models.ts @@ -48,3 +48,26 @@ export interface WalletBalance { balance: string; // atto tokens as string gasBalance: string; // atto tokens as string } + +/** A single payment required for an upload. */ +export interface PaymentInfo { + quoteHash: string; // hex + rewardsAddress: string; // hex + amount: string; // atto tokens +} + +/** Result of preparing an upload for external signing. */ +export interface PrepareUploadResult { + uploadId: string; // hex identifier + payments: PaymentInfo[]; + totalAmount: string; + dataPaymentsAddress: string; // contract address + paymentTokenAddress: string; // token contract address + rpcUrl: string; // EVM RPC URL +} + +/** Result of finalizing an externally-signed upload. */ +export interface FinalizeUploadResult { + address: string; // hex address of stored data + chunksStored: number; +} diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index 895e550..f0f4846 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -3,9 +3,12 @@ import { fromHttpStatus } from "./errors.js"; import type { Archive, ArchiveEntry, + FinalizeUploadResult, GraphDescendant, GraphEntry, HealthStatus, + PaymentInfo, + PrepareUploadResult, PutResult, WalletAddress, WalletBalance, @@ -309,4 +312,42 @@ export class RestClient { const j = await this.postJson<{ approved: boolean }>("/v1/wallet/approve", {}); return j.approved; } + + // ---- External Signer (Two-Phase Upload) ---- + + /** Prepare a file upload for external signing. */ + async prepareUpload(path: string): Promise { + const j = await this.postJson<{ + upload_id: string; + payments: { quote_hash: string; rewards_address: string; amount: string }[]; + total_amount: string; + data_payments_address: string; + payment_token_address: string; + rpc_url: string; + }>("/v1/upload/prepare", { path }); + return { + uploadId: j.upload_id, + payments: (j.payments ?? []).map((p) => ({ + quoteHash: p.quote_hash, + rewardsAddress: p.rewards_address, + amount: p.amount, + })), + totalAmount: j.total_amount, + dataPaymentsAddress: j.data_payments_address, + paymentTokenAddress: j.payment_token_address, + rpcUrl: j.rpc_url, + }; + } + + /** Finalize an upload after an external signer has submitted payment transactions. */ + async finalizeUpload( + uploadId: string, + txHashes: Record, + ): Promise { + const j = await this.postJson<{ address: string; chunks_stored: number }>( + "/v1/upload/finalize", + { upload_id: uploadId, tx_hashes: txHashes }, + ); + return { address: j.address, chunksStored: j.chunks_stored }; + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index 4651805..dbb1e66 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -277,4 +277,26 @@ class AntdRestClient( val resp = postJson("/v1/wallet/approve", body) return resp.approved } + + // ── External Signer (Two-Phase Upload) ── + + /** Prepares a file upload for external signing. */ + override suspend fun prepareUpload(path: String): PrepareUploadResult { + val body = buildJsonObject { put("path", path) }.toString() + val resp = postJson("/v1/upload/prepare", body) + val payments = resp.payments?.map { PaymentInfo(it.quoteHash, it.rewardsAddress, it.amount) } ?: emptyList() + return PrepareUploadResult(resp.uploadId, payments, resp.totalAmount, resp.dataPaymentsAddress, resp.paymentTokenAddress, resp.rpcUrl) + } + + /** Finalizes an upload after an external signer has submitted payment transactions. */ + override suspend fun finalizeUpload(uploadId: String, txHashes: Map): FinalizeUploadResult { + val body = buildJsonObject { + put("upload_id", uploadId) + put("tx_hashes", buildJsonObject { + txHashes.forEach { (k, v) -> put(k, v) } + }) + }.toString() + val resp = postJson("/v1/upload/finalize", body) + return FinalizeUploadResult(resp.address, resp.chunksStored) + } } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt index 57d2ef9..0cd5288 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt @@ -43,4 +43,8 @@ interface IAntdClient : Closeable { suspend fun walletAddress(): WalletAddress suspend fun walletBalance(): WalletBalance suspend fun walletApprove(): Boolean + + // External Signer (Two-Phase Upload) + suspend fun prepareUpload(path: String): PrepareUploadResult + suspend fun finalizeUpload(uploadId: String, txHashes: Map): FinalizeUploadResult } diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt index a0271cf..bf74161 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt @@ -27,6 +27,22 @@ data class WalletAddress(val address: String) /** Wallet balance response. */ data class WalletBalance(val balance: String, val gasBalance: String) +/** A single payment required for an upload. */ +data class PaymentInfo(val quoteHash: String, val rewardsAddress: String, val amount: String) + +/** Result of preparing an upload for external signing. */ +data class PrepareUploadResult( + val uploadId: String, + val payments: List, + val totalAmount: String, + val dataPaymentsAddress: String, + val paymentTokenAddress: String, + val rpcUrl: String, +) + +/** Result of finalizing an externally-signed upload. */ +data class FinalizeUploadResult(val address: String, val chunksStored: Long) + // ── Internal DTOs for JSON deserialization ── @Serializable @@ -100,3 +116,26 @@ internal data class WalletBalanceDto( internal data class WalletApproveDto( val approved: Boolean, ) + +@Serializable +internal data class PaymentInfoDto( + @SerialName("quote_hash") val quoteHash: String, + @SerialName("rewards_address") val rewardsAddress: String, + val amount: String, +) + +@Serializable +internal data class PrepareUploadDto( + @SerialName("upload_id") val uploadId: String, + val payments: List? = null, + @SerialName("total_amount") val totalAmount: String, + @SerialName("data_payments_address") val dataPaymentsAddress: String, + @SerialName("payment_token_address") val paymentTokenAddress: String, + @SerialName("rpc_url") val rpcUrl: String, +) + +@Serializable +internal data class FinalizeUploadDto( + val address: String, + @SerialName("chunks_stored") val chunksStored: Long, +) diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index 46c779d..3af9ae7 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -448,6 +448,56 @@ function Client:wallet_approve() return j.approved == true, nil end +-- ── External Signer (Two-Phase Upload) ── + +--- Prepare a file upload for external signing. +-- @param path string local file path +-- @return table|nil PrepareUploadResult, error|nil +function Client:prepare_upload(path) + local j, _, err = self:_do_json("POST", "/v1/upload/prepare", { + path = path, + }) + if err then return nil, err end + + local payments = {} + if j.payments and type(j.payments) == "table" then + for _, p in ipairs(j.payments) do + if type(p) == "table" then + payments[#payments + 1] = { + quote_hash = str(p, "quote_hash"), + rewards_address = str(p, "rewards_address"), + amount = str(p, "amount"), + } + end + end + end + + return { + upload_id = str(j, "upload_id"), + payments = payments, + total_amount = str(j, "total_amount"), + data_payments_address = str(j, "data_payments_address"), + payment_token_address = str(j, "payment_token_address"), + rpc_url = str(j, "rpc_url"), + }, nil +end + +--- Finalize an upload after an external signer has submitted payment transactions. +-- @param upload_id string the upload ID from prepare_upload +-- @param tx_hashes table map of quote_hash to tx_hash +-- @return table|nil FinalizeUploadResult, error|nil +function Client:finalize_upload(upload_id, tx_hashes) + local j, _, err = self:_do_json("POST", "/v1/upload/finalize", { + upload_id = upload_id, + tx_hashes = tx_hashes, + }) + if err then return nil, err end + return { + address = str(j, "address"), + chunks_stored = num(j, "chunks_stored"), + }, nil +end + --- Create a client using daemon port discovery. -- Falls back to the default base URL if discovery fails. -- @param opts table optional settings: { timeout = number } diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index c9db591..fbb8a4c 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -610,6 +610,80 @@ async def archive_put( return _err(exc, network) +# --------------------------------------------------------------------------- +# Tool 18: prepare_upload +# --------------------------------------------------------------------------- + + +@mcp.tool() +async def prepare_upload( + path: str, +) -> str: + """Prepare a file upload for external signing (two-phase upload). + + Returns payment details including contract addresses, quote hashes, and + amounts that an external signer must process before calling finalize_upload. + + Args: + path: Absolute path to the local file to upload. + + Returns: + JSON with upload_id, payments array (quote_hash, rewards_address, amount), + total_amount, data_payments_address, payment_token_address, and rpc_url. + """ + client, network = _get_ctx() + try: + result = await client.prepare_upload(path) + return _ok({ + "upload_id": result.upload_id, + "payments": [ + { + "quote_hash": p.quote_hash, + "rewards_address": p.rewards_address, + "amount": p.amount, + } + for p in result.payments + ], + "total_amount": result.total_amount, + "data_payments_address": result.data_payments_address, + "payment_token_address": result.payment_token_address, + "rpc_url": result.rpc_url, + }, network) + except AntdError as exc: + return _err_antd(exc, network) + except Exception as exc: + return _err(exc, network) + + +# --------------------------------------------------------------------------- +# Tool 19: finalize_upload +# --------------------------------------------------------------------------- + + +@mcp.tool() +async def finalize_upload( + upload_id: str, + tx_hashes: dict[str, str], +) -> str: + """Finalize a two-phase upload after payment transactions are submitted. + + Args: + upload_id: The upload ID returned by prepare_upload. + tx_hashes: Map of quote_hash to tx_hash for each payment. + + Returns: + JSON with address (hex) and chunks_stored count. + """ + client, network = _get_ctx() + try: + result = await client.finalize_upload(upload_id, tx_hashes) + return _ok({"address": result.address, "chunks_stored": result.chunks_stored}, network) + except AntdError as exc: + return _err_antd(exc, network) + except Exception as exc: + return _err(exc, network) + + # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index 0d50547..ba95d3b 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -892,4 +892,82 @@ public function fileCostAsync(string $path, bool $isPublic, bool $includeArchive fn(?array $json) => $json['cost'] ?? '', ); } + + // --- External Signer (Two-Phase Upload) --- + + /** + * Prepare a file upload for external signing. + * + * @return array{upload_id: string, payments: array, total_amount: string, data_payments_address: string, payment_token_address: string, rpc_url: string} + */ + public function prepareUpload(string $path): array + { + $json = $this->doJson('POST', '/v1/upload/prepare', ['path' => $path]); + return [ + 'upload_id' => $json['upload_id'] ?? '', + 'payments' => $json['payments'] ?? [], + 'total_amount' => $json['total_amount'] ?? '', + 'data_payments_address' => $json['data_payments_address'] ?? '', + 'payment_token_address' => $json['payment_token_address'] ?? '', + 'rpc_url' => $json['rpc_url'] ?? '', + ]; + } + + /** + * Async: Prepare a file upload for external signing. + * + * @return PromiseInterface + */ + public function prepareUploadAsync(string $path): PromiseInterface + { + return $this->doJsonAsync('POST', '/v1/upload/prepare', ['path' => $path])->then( + fn(?array $json) => [ + 'upload_id' => $json['upload_id'] ?? '', + 'payments' => $json['payments'] ?? [], + 'total_amount' => $json['total_amount'] ?? '', + 'data_payments_address' => $json['data_payments_address'] ?? '', + 'payment_token_address' => $json['payment_token_address'] ?? '', + 'rpc_url' => $json['rpc_url'] ?? '', + ], + ); + } + + /** + * Finalize an upload after an external signer has submitted payment transactions. + * + * @param string $uploadId The upload ID from prepareUpload. + * @param array $txHashes Map of quote_hash to tx_hash. + * @return array{address: string, chunks_stored: int} + */ + public function finalizeUpload(string $uploadId, array $txHashes): array + { + $json = $this->doJson('POST', '/v1/upload/finalize', [ + 'upload_id' => $uploadId, + 'tx_hashes' => $txHashes, + ]); + return [ + 'address' => $json['address'] ?? '', + 'chunks_stored' => (int) ($json['chunks_stored'] ?? 0), + ]; + } + + /** + * Async: Finalize an upload after an external signer has submitted payment transactions. + * + * @param string $uploadId The upload ID from prepareUpload. + * @param array $txHashes Map of quote_hash to tx_hash. + * @return PromiseInterface + */ + public function finalizeUploadAsync(string $uploadId, array $txHashes): PromiseInterface + { + return $this->doJsonAsync('POST', '/v1/upload/finalize', [ + 'upload_id' => $uploadId, + 'tx_hashes' => $txHashes, + ])->then( + fn(?array $json) => [ + 'address' => $json['address'] ?? '', + 'chunks_stored' => (int) ($json['chunks_stored'] ?? 0), + ], + ); + } } diff --git a/antd-py/src/antd/__init__.py b/antd-py/src/antd/__init__.py index 872ef66..bb4f57a 100644 --- a/antd-py/src/antd/__init__.py +++ b/antd-py/src/antd/__init__.py @@ -14,9 +14,12 @@ from .models import ( Archive, ArchiveEntry, + FinalizeUploadResult, GraphDescendant, GraphEntry, HealthStatus, + PaymentInfo, + PrepareUploadResult, PutResult, WalletAddress, WalletBalance, diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 65bc11f..09c8155 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -11,9 +11,12 @@ from .models import ( Archive, ArchiveEntry, + FinalizeUploadResult, GraphDescendant, GraphEntry, HealthStatus, + PaymentInfo, + PrepareUploadResult, PutResult, WalletAddress, WalletBalance, @@ -256,6 +259,49 @@ def wallet_approve(self) -> bool: j = resp.json() return j.get("approved", False) + # --- External Signer (Two-Phase Upload) --- + + def prepare_upload(self, path: str) -> PrepareUploadResult: + """Prepare a file upload for external signing. + + Returns payment details that an external signer must process + before calling finalize_upload. + """ + resp = self._http.post("/v1/upload/prepare", json={"path": path}) + _check(resp) + j = resp.json() + payments = [ + PaymentInfo( + quote_hash=p["quote_hash"], + rewards_address=p["rewards_address"], + amount=p["amount"], + ) + for p in j.get("payments", []) + ] + return PrepareUploadResult( + upload_id=j["upload_id"], + payments=payments, + total_amount=j.get("total_amount", ""), + data_payments_address=j.get("data_payments_address", ""), + payment_token_address=j.get("payment_token_address", ""), + rpc_url=j.get("rpc_url", ""), + ) + + def finalize_upload(self, upload_id: str, tx_hashes: dict[str, str]) -> FinalizeUploadResult: + """Finalize an upload after an external signer has submitted payment transactions. + + Args: + upload_id: The upload ID returned by prepare_upload. + tx_hashes: Map of quote_hash to tx_hash for each payment. + """ + resp = self._http.post("/v1/upload/finalize", json={ + "upload_id": upload_id, + "tx_hashes": tx_hashes, + }) + _check(resp) + j = resp.json() + return FinalizeUploadResult(address=j["address"], chunks_stored=j.get("chunks_stored", 0)) + class AsyncRestClient: """Asynchronous REST client for the antd daemon.""" @@ -470,3 +516,46 @@ async def wallet_approve(self) -> bool: _check(resp) j = resp.json() return j.get("approved", False) + + # --- External Signer (Two-Phase Upload) --- + + async def prepare_upload(self, path: str) -> PrepareUploadResult: + """Prepare a file upload for external signing. + + Returns payment details that an external signer must process + before calling finalize_upload. + """ + resp = await self._http.post("/v1/upload/prepare", json={"path": path}) + _check(resp) + j = resp.json() + payments = [ + PaymentInfo( + quote_hash=p["quote_hash"], + rewards_address=p["rewards_address"], + amount=p["amount"], + ) + for p in j.get("payments", []) + ] + return PrepareUploadResult( + upload_id=j["upload_id"], + payments=payments, + total_amount=j.get("total_amount", ""), + data_payments_address=j.get("data_payments_address", ""), + payment_token_address=j.get("payment_token_address", ""), + rpc_url=j.get("rpc_url", ""), + ) + + async def finalize_upload(self, upload_id: str, tx_hashes: dict[str, str]) -> FinalizeUploadResult: + """Finalize an upload after an external signer has submitted payment transactions. + + Args: + upload_id: The upload ID returned by prepare_upload. + tx_hashes: Map of quote_hash to tx_hash for each payment. + """ + resp = await self._http.post("/v1/upload/finalize", json={ + "upload_id": upload_id, + "tx_hashes": tx_hashes, + }) + _check(resp) + j = resp.json() + return FinalizeUploadResult(address=j["address"], chunks_stored=j.get("chunks_stored", 0)) diff --git a/antd-py/src/antd/models.py b/antd-py/src/antd/models.py index a3b460c..081cedd 100644 --- a/antd-py/src/antd/models.py +++ b/antd-py/src/antd/models.py @@ -61,3 +61,29 @@ class WalletBalance: """Wallet balance from the antd daemon.""" balance: str # atto tokens as string gas_balance: str # atto gas tokens as string + + +@dataclass(frozen=True) +class PaymentInfo: + """A single payment required for an upload.""" + quote_hash: str # hex + rewards_address: str # hex + amount: str # atto tokens as string + + +@dataclass(frozen=True) +class PrepareUploadResult: + """Result of preparing an upload for external signing.""" + upload_id: str # hex identifier + payments: list[PaymentInfo] = field(default_factory=list) + total_amount: str = "" + data_payments_address: str = "" # contract address + payment_token_address: str = "" # token contract address + rpc_url: str = "" # EVM RPC URL + + +@dataclass(frozen=True) +class FinalizeUploadResult: + """Result of finalizing an externally-signed upload.""" + address: str # hex address of stored data + chunks_stored: int = 0 diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index 6b4694f..12e5f20 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -263,6 +263,42 @@ def wallet_approve j["approved"] == true end + # --- External Signer (Two-Phase Upload) --- + + # Prepare a file upload for external signing. + # @param path [String] local file path + # @return [PrepareUploadResult] + def prepare_upload(path) + j = do_json(:post, "/v1/upload/prepare", { path: path }) + payments = (j["payments"] || []).map do |p| + PaymentInfo.new( + quote_hash: p["quote_hash"], + rewards_address: p["rewards_address"], + amount: p["amount"] + ) + end + PrepareUploadResult.new( + upload_id: j["upload_id"], + payments: payments, + total_amount: j["total_amount"], + data_payments_address: j["data_payments_address"], + payment_token_address: j["payment_token_address"], + rpc_url: j["rpc_url"] + ) + end + + # Finalize an upload after an external signer has submitted payment transactions. + # @param upload_id [String] the upload ID from prepare_upload + # @param tx_hashes [Hash] map of quote_hash to tx_hash + # @return [FinalizeUploadResult] + def finalize_upload(upload_id, tx_hashes) + j = do_json(:post, "/v1/upload/finalize", { + upload_id: upload_id, + tx_hashes: tx_hashes + }) + FinalizeUploadResult.new(address: j["address"], chunks_stored: j["chunks_stored"].to_i) + end + private def b64_encode(data) diff --git a/antd-ruby/lib/antd/models.rb b/antd-ruby/lib/antd/models.rb index c4f5c39..cec0f97 100644 --- a/antd-ruby/lib/antd/models.rb +++ b/antd-ruby/lib/antd/models.rb @@ -24,4 +24,13 @@ module Antd # Wallet balance result. WalletBalance = Struct.new(:balance, :gas_balance, keyword_init: true) + + # A single payment required for an upload. + PaymentInfo = Struct.new(:quote_hash, :rewards_address, :amount, keyword_init: true) + + # Result of preparing an upload for external signing. + PrepareUploadResult = Struct.new(:upload_id, :payments, :total_amount, :data_payments_address, :payment_token_address, :rpc_url, keyword_init: true) + + # Result of finalizing an externally-signed upload. + FinalizeUploadResult = Struct.new(:address, :chunks_stored, keyword_init: true) end diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index e13ba17..4a7d050 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -561,4 +561,64 @@ impl Client { let j = j.unwrap_or_default(); Ok(j.get("approved").and_then(|v| v.as_bool()).unwrap_or(false)) } + + // --- External Signer (Two-Phase Upload) --- + + /// Prepares a file upload for external signing. + /// Returns payment details that an external signer must process before calling + /// [`finalize_upload`](Self::finalize_upload). + pub async fn prepare_upload(&self, path: &str) -> Result { + let (j, _) = self + .do_json( + reqwest::Method::POST, + "/v1/upload/prepare", + Some(json!({ "path": path })), + ) + .await?; + let j = j.unwrap_or_default(); + let payments = j + .get("payments") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .map(|p| PaymentInfo { + quote_hash: Self::str_field(p, "quote_hash"), + rewards_address: Self::str_field(p, "rewards_address"), + amount: Self::str_field(p, "amount"), + }) + .collect() + }) + .unwrap_or_default(); + Ok(PrepareUploadResult { + upload_id: Self::str_field(&j, "upload_id"), + payments, + total_amount: Self::str_field(&j, "total_amount"), + data_payments_address: Self::str_field(&j, "data_payments_address"), + payment_token_address: Self::str_field(&j, "payment_token_address"), + rpc_url: Self::str_field(&j, "rpc_url"), + }) + } + + /// Finalizes an upload after an external signer has submitted payment transactions. + pub async fn finalize_upload( + &self, + upload_id: &str, + tx_hashes: &std::collections::HashMap, + ) -> Result { + let (j, _) = self + .do_json( + reqwest::Method::POST, + "/v1/upload/finalize", + Some(json!({ + "upload_id": upload_id, + "tx_hashes": tx_hashes, + })), + ) + .await?; + let j = j.unwrap_or_default(); + Ok(FinalizeUploadResult { + address: Self::str_field(&j, "address"), + chunks_stored: Self::i64_field(&j, "chunks_stored"), + }) + } } diff --git a/antd-rust/src/models.rs b/antd-rust/src/models.rs index bec5577..510a9ca 100644 --- a/antd-rust/src/models.rs +++ b/antd-rust/src/models.rs @@ -65,3 +65,40 @@ pub struct WalletBalance { /// Gas balance in atto tokens as a string. pub gas_balance: String, } + +/// A single payment required for an upload. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentInfo { + /// Hex-encoded quote hash. + pub quote_hash: String, + /// Hex-encoded rewards address. + pub rewards_address: String, + /// Amount in atto tokens as a string. + pub amount: String, +} + +/// Result of preparing an upload for external signing. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrepareUploadResult { + /// Hex identifier for this upload session. + pub upload_id: String, + /// Payments that must be signed externally. + pub payments: Vec, + /// Total amount across all payments. + pub total_amount: String, + /// Data payments contract address. + pub data_payments_address: String, + /// Payment token contract address. + pub payment_token_address: String, + /// EVM RPC URL for submitting transactions. + pub rpc_url: String, +} + +/// Result of finalizing an externally-signed upload. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FinalizeUploadResult { + /// Hex address of the stored data. + pub address: String, + /// Number of chunks stored. + pub chunks_stored: i64, +} diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index b7cae2e..d101253 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -211,6 +211,22 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { let resp: WalletApproveDTO = try await postJSON("/v1/wallet/approve", body: [:] as [String: Any]) return resp.approved } + + // MARK: - External Signer (Two-Phase Upload) + + /// Prepares a file upload for external signing. + public func prepareUpload(path: String) async throws -> PrepareUploadResult { + let resp: PrepareUploadDTO = try await postJSON("/v1/upload/prepare", body: ["path": path]) + let payments = (resp.payments ?? []).map { PaymentInfo(quoteHash: $0.quoteHash, rewardsAddress: $0.rewardsAddress, amount: $0.amount) } + return PrepareUploadResult(uploadId: resp.uploadId, payments: payments, totalAmount: resp.totalAmount, dataPaymentsAddress: resp.dataPaymentsAddress, paymentTokenAddress: resp.paymentTokenAddress, rpcUrl: resp.rpcUrl) + } + + /// Finalizes an upload after an external signer has submitted payment transactions. + public func finalizeUpload(uploadId: String, txHashes: [String: String]) async throws -> FinalizeUploadResult { + let body: [String: Any] = ["upload_id": uploadId, "tx_hashes": txHashes] + let resp: FinalizeUploadDTO = try await postJSON("/v1/upload/finalize", body: body) + return FinalizeUploadResult(address: resp.address, chunksStored: resp.chunksStored) + } } // MARK: - Internal DTOs @@ -275,6 +291,26 @@ private struct WalletApproveDTO: Decodable { let approved: Bool } +private struct PaymentInfoDTO: Decodable { + let quoteHash: String + let rewardsAddress: String + let amount: String +} + +private struct PrepareUploadDTO: Decodable { + let uploadId: String + let payments: [PaymentInfoDTO]? + let totalAmount: String + let dataPaymentsAddress: String + let paymentTokenAddress: String + let rpcUrl: String +} + +private struct FinalizeUploadDTO: Decodable { + let address: String + let chunksStored: Int64 +} + extension JSONDecoder { static let snakeCase: JSONDecoder = { let decoder = JSONDecoder() diff --git a/antd-swift/Sources/AntdSdk/Models.swift b/antd-swift/Sources/AntdSdk/Models.swift index 59c763a..fdfa405 100644 --- a/antd-swift/Sources/AntdSdk/Models.swift +++ b/antd-swift/Sources/AntdSdk/Models.swift @@ -93,3 +93,46 @@ public struct WalletBalance: Sendable, Equatable { self.gasBalance = gasBalance } } + +/// A single payment required for an upload. +public struct PaymentInfo: Sendable, Equatable { + public let quoteHash: String + public let rewardsAddress: String + public let amount: String + + public init(quoteHash: String, rewardsAddress: String, amount: String) { + self.quoteHash = quoteHash + self.rewardsAddress = rewardsAddress + self.amount = amount + } +} + +/// Result of preparing an upload for external signing. +public struct PrepareUploadResult: Sendable, Equatable { + public let uploadId: String + public let payments: [PaymentInfo] + public let totalAmount: String + public let dataPaymentsAddress: String + public let paymentTokenAddress: String + public let rpcUrl: String + + public init(uploadId: String, payments: [PaymentInfo], totalAmount: String, dataPaymentsAddress: String, paymentTokenAddress: String, rpcUrl: String) { + self.uploadId = uploadId + self.payments = payments + self.totalAmount = totalAmount + self.dataPaymentsAddress = dataPaymentsAddress + self.paymentTokenAddress = paymentTokenAddress + self.rpcUrl = rpcUrl + } +} + +/// Result of finalizing an externally-signed upload. +public struct FinalizeUploadResult: Sendable, Equatable { + public let address: String + public let chunksStored: Int64 + + public init(address: String, chunksStored: Int64) { + self.address = address + self.chunksStored = chunksStored + } +} diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index 9bee1af..0ab5d0b 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -402,6 +402,28 @@ pub const Client = struct { return json_helpers.parsePutResult(self.allocator, resp, "address"); } + // --- External Signer (Two-Phase Upload) --- + + /// Prepare a file upload for external signing. + /// Returns raw JSON response body that the caller must parse. + pub fn prepareUpload(self: *Client, path: []const u8) ![]const u8 { + const req_body = try json_helpers.buildJsonBody(self.allocator, &.{ + .{ .key = "path", .value = .{ .string = path } }, + }); + defer self.allocator.free(req_body); + const resp = try self.doRequest(.POST, "/v1/upload/prepare", req_body) orelse return error.JsonError; + return resp; + } + + /// Finalize an upload after an external signer has submitted payment transactions. + /// Returns raw JSON response body that the caller must parse. + pub fn finalizeUpload(self: *Client, upload_id: []const u8, tx_hashes_json: []const u8) ![]const u8 { + // Caller must provide a pre-built JSON body with upload_id and tx_hashes + const resp = try self.doRequest(.POST, "/v1/upload/finalize", tx_hashes_json) orelse return error.JsonError; + _ = upload_id; + return resp; + } + /// Estimate the cost of uploading a file. pub fn fileCost(self: *Client, path: []const u8, is_public: bool, include_archive: bool) ![]const u8 { const req_body = try json_helpers.buildJsonBody(self.allocator, &.{ diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 76b5889..54ab4e1 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -828,8 +828,8 @@ dependencies = [ [[package]] name = "ant-core" -version = "0.1.0" -source = "git+https://github.com/WithAutonomi/ant-client#c979f145a15c310693bfbb801e1500252af36cad" +version = "0.1.1" +source = "git+https://github.com/WithAutonomi/ant-client#b9c08d612a6d0f72c8a4466c1d6233028ca16e15" dependencies = [ "ant-evm", "ant-node", @@ -859,6 +859,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-util", + "toml", "tower-http", "tracing", "tracing-subscriber", diff --git a/antd/src/main.rs b/antd/src/main.rs index 24e9604..7e123c9 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -157,13 +157,32 @@ async fn main() -> Result<(), Box> { } } } else { - tracing::info!("no AUTONOMI_WALLET_KEY set — write operations will fail"); + // No wallet — but still configure EVM network if available, + // to enable external signer flow (prepare-upload/finalize-upload) + let rpc_url = std::env::var("EVM_RPC_URL").ok(); + if let Some(rpc_url) = rpc_url { + let token_addr = std::env::var("EVM_PAYMENT_TOKEN_ADDRESS") + .unwrap_or_default(); + let payments_addr = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") + .unwrap_or_default(); + let network = evmlib::Network::new_custom( + &rpc_url, + &token_addr, + &payments_addr, + None, + ); + client = client.with_evm_network(network); + tracing::info!(%rpc_url, "EVM network configured (external signer mode)"); + } else { + tracing::info!("no AUTONOMI_WALLET_KEY or EVM_RPC_URL set — write operations will fail"); + } } let state = Arc::new(AppState { client: Arc::new(client), network: config.network.clone(), bootstrap_peers, + pending_uploads: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), }); // Build REST router diff --git a/antd/src/rest/mod.rs b/antd/src/rest/mod.rs index 469e834..b872616 100644 --- a/antd/src/rest/mod.rs +++ b/antd/src/rest/mod.rs @@ -14,6 +14,7 @@ pub mod data; pub mod events; pub mod files; pub mod graph; +pub mod upload; pub mod wallet; pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router { @@ -44,6 +45,9 @@ pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router .route("/v1/archives/public", post(files::archive_put_public)) // Cost .route("/v1/cost/file", post(files::file_cost)) + // External signer (two-phase upload) + .route("/v1/upload/prepare", post(upload::prepare_upload)) + .route("/v1/upload/finalize", post(upload::finalize_upload)) // Wallet .route("/v1/wallet/address", get(wallet::wallet_address)) .route("/v1/wallet/balance", get(wallet::wallet_balance)) diff --git a/antd/src/rest/upload.rs b/antd/src/rest/upload.rs new file mode 100644 index 0000000..e478319 --- /dev/null +++ b/antd/src/rest/upload.rs @@ -0,0 +1,114 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + +use axum::extract::State; +use axum::Json; + +use crate::error::AntdError; +use crate::state::AppState; +use crate::types::*; + +/// Phase 1: Prepare a file upload for external signing. +/// +/// Encrypts the file, collects storage quotes from the network, and returns +/// a payment intent with an upload_id. The caller signs and submits the EVM +/// payment transaction externally, then calls finalize with the tx hashes. +/// +/// The prepared upload state is held server-side (not serialized to the client) +/// so that internal types like PeerId and peer addresses are preserved. +pub async fn prepare_upload( + State(state): State>, + Json(req): Json, +) -> Result, AntdError> { + let path = PathBuf::from(&req.path); + if !path.exists() { + return Err(AntdError::BadRequest(format!("file not found: {}", req.path))); + } + + let client = state.client.clone(); + let prepared = tokio::spawn(async move { + client.file_prepare_upload(&path).await + .map_err(AntdError::from_core) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + // Build payment entries from the intent + let payments: Vec = prepared.payment_intent.payments.iter().map(|(quote_hash, rewards_addr, amount)| { + PaymentEntry { + quote_hash: format!("{:#x}", quote_hash), + rewards_address: format!("{:#x}", rewards_addr), + amount: amount.to_string(), + } + }).collect(); + + let total_amount = prepared.payment_intent.total_amount.to_string(); + + // Generate a unique upload ID and store the prepared state + let upload_id = hex::encode(rand::random::<[u8; 16]>()); + state.pending_uploads.lock().await.insert(upload_id.clone(), prepared); + + // EVM network details from env + let rpc_url = std::env::var("EVM_RPC_URL") + .unwrap_or_else(|_| "http://127.0.0.1:8545".to_string()); + let payment_token_address = std::env::var("EVM_PAYMENT_TOKEN_ADDRESS") + .unwrap_or_default(); + let data_payments_address = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") + .unwrap_or_default(); + + Ok(Json(PrepareUploadResponse { + upload_id, + payments, + total_amount, + data_payments_address, + payment_token_address, + rpc_url, + })) +} + +/// Phase 2: Finalize an upload after external payment. +/// +/// Takes the upload_id from prepare and a map of quote_hash → tx_hash +/// from the on-chain payment. Builds proofs and uploads chunks. +pub async fn finalize_upload( + State(state): State>, + Json(req): Json, +) -> Result, AntdError> { + // Remove the prepared upload from server state + let prepared = state.pending_uploads.lock().await + .remove(&req.upload_id) + .ok_or_else(|| AntdError::NotFound(format!( + "upload_id {} not found — it may have expired or already been finalized", + req.upload_id + )))?; + + // Parse tx_hashes from hex strings + let tx_hash_map: HashMap = + req.tx_hashes + .iter() + .map(|(quote_hex, tx_hex)| { + let quote_bytes: [u8; 32] = hex::decode(quote_hex.trim_start_matches("0x")) + .map_err(|e| AntdError::BadRequest(format!("invalid quote_hash {quote_hex}: {e}")))? + .try_into() + .map_err(|_| AntdError::BadRequest("quote_hash must be 32 bytes".into()))?; + let tx_bytes: [u8; 32] = hex::decode(tx_hex.trim_start_matches("0x")) + .map_err(|e| AntdError::BadRequest(format!("invalid tx_hash {tx_hex}: {e}")))? + .try_into() + .map_err(|_| AntdError::BadRequest("tx_hash must be 32 bytes".into()))?; + Ok((quote_bytes.into(), tx_bytes.into())) + }) + .collect::>()?; + + let client = state.client.clone(); + let (address, chunks_stored) = tokio::spawn(async move { + let result = client.finalize_upload(prepared, &tx_hash_map).await + .map_err(AntdError::from_core)?; + let address = client.data_map_store(&result.data_map).await + .map_err(AntdError::from_core)?; + Ok::<_, AntdError>((address, result.chunks_stored)) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + Ok(Json(FinalizeUploadResponse { + address: hex::encode(address), + chunks_stored: chunks_stored as u64, + })) +} diff --git a/antd/src/state.rs b/antd/src/state.rs index e755d4a..9ac06c6 100644 --- a/antd/src/state.rs +++ b/antd/src/state.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; use std::sync::Arc; -use ant_core::data::{Client, MultiAddr}; +use ant_core::data::{Client, MultiAddr, PreparedUpload}; +use tokio::sync::Mutex; /// Shared application state passed to all handlers. #[derive(Clone)] @@ -11,4 +13,6 @@ pub struct AppState { pub network: String, /// Bootstrap peer addresses. pub bootstrap_peers: Vec, + /// Pending prepared uploads awaiting external payment (upload_id → state). + pub pending_uploads: Arc>>, } diff --git a/antd/src/types.rs b/antd/src/types.rs index 3001628..f951eb3 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -92,6 +92,55 @@ pub struct GraphEntryCostRequest { pub public_key: String, // hex } +// ── External Signer (two-phase upload) ── + +#[derive(Deserialize)] +pub struct PrepareUploadRequest { + pub path: String, +} + +#[derive(Serialize)] +pub struct PrepareUploadResponse { + /// Opaque token to pass back to finalize (hex-encoded serialized state). + pub upload_id: String, + /// Payment entries: each has quote_hash, rewards_address, amount. + pub payments: Vec, + /// Total amount to pay (atto tokens as decimal string). + pub total_amount: String, + /// Data payments contract address (hex with 0x prefix). + pub data_payments_address: String, + /// Payment token contract address (hex with 0x prefix). + pub payment_token_address: String, + /// EVM RPC URL for submitting transactions. + pub rpc_url: String, +} + +#[derive(Serialize)] +pub struct PaymentEntry { + /// Quote hash (hex, 32 bytes). + pub quote_hash: String, + /// Rewards address (hex with 0x prefix, 20 bytes). + pub rewards_address: String, + /// Amount to pay (atto tokens as decimal string). + pub amount: String, +} + +#[derive(Deserialize)] +pub struct FinalizeUploadRequest { + /// The upload_id returned from prepare. + pub upload_id: String, + /// Map of quote_hash (hex) → tx_hash (hex) from on-chain payment. + pub tx_hashes: std::collections::HashMap, +} + +#[derive(Serialize)] +pub struct FinalizeUploadResponse { + /// Hex-encoded address of the stored data map. + pub address: String, + /// Number of chunks stored. + pub chunks_stored: u64, +} + // ── Files ── #[derive(Deserialize)] diff --git a/llms-full.txt b/llms-full.txt index 5e89218..10cb8bc 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -373,6 +373,31 @@ Request: `{}` (empty body) Response: `{"approved": true}` Returns 400 if no wallet is configured. +#### `POST /v1/upload/prepare` +Prepare a file upload for external signing (two-phase upload). Instead of antd paying +from its built-in wallet, this returns payment details for an external signer. + +Request: `{"path": "/path/to/file"}` +Response: +```json +{ + "upload_id": "hex-string", + "payments": [ + { "quote_hash": "0x...", "rewards_address": "0x...", "amount": "123" } + ], + "total_amount": "456", + "data_payments_address": "0x...", + "payment_token_address": "0x...", + "rpc_url": "http://..." +} +``` + +#### `POST /v1/upload/finalize` +Finalize an upload after an external signer has submitted payment transactions on-chain. + +Request: `{"upload_id": "hex-string", "tx_hashes": {"0xquotehash": "0xtxhash", ...}}` +Response: `{"address": "hex", "chunks_stored": 5}` + --- ## gRPC API — All Services diff --git a/llms.txt b/llms.txt index 0237bff..b7840ac 100644 --- a/llms.txt +++ b/llms.txt @@ -63,6 +63,8 @@ data = client.data_get_public(result.address) | GET | `/v1/wallet/address` | Get wallet public address | | GET | `/v1/wallet/balance` | Get wallet token and gas balances | | POST | `/v1/wallet/approve` | Approve wallet to spend tokens on payment contracts | +| POST | `/v1/upload/prepare` | Prepare upload for external signer (two-phase upload) | +| POST | `/v1/upload/finalize` | Finalize externally-signed upload | ## gRPC Services diff --git a/skill.md b/skill.md index 739bca9..5456218 100644 --- a/skill.md +++ b/skill.md @@ -107,6 +107,24 @@ entry2 = client.graph_entry_put(key2, parents=[entry1.address], content=content2 **When to suggest this:** Developer needs an audit log, version chain, social graph, or any linked data structure. +### Pattern 4: External Signer (Two-Phase Upload) + +When the application manages its own wallet (e.g. a browser wallet or hardware signer), use the two-phase upload flow instead of the daemon's built-in wallet: + +```python +# Phase 1: Prepare — get payment details +prep = client.prepare_upload("/path/to/file") +# prep.upload_id, prep.payments, prep.total_amount, prep.data_payments_address, etc. + +# ... external signer submits EVM transactions for each payment ... + +# Phase 2: Finalize — confirm payments and store data +result = client.finalize_upload(prep.upload_id, {"0xquotehash": "0xtxhash", ...}) +print(f"Stored at: {result.address}, chunks: {result.chunks_stored}") +``` + +**When to suggest this:** Developer has their own wallet/signer and doesn't want to use antd's built-in wallet. Common in web apps, mobile apps, or enterprise integrations. + ## Key Rules 1. **Every write costs tokens.** Always offer to estimate cost first with the `*_cost` methods (now fully implemented for both data and files). Before the first storage operation, the wallet must be approved via `wallet_approve()`. Reads are free. From 43533506528383475c4fa4757c668ae4ab0e73ac Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 14:44:16 +0100 Subject: [PATCH 15/20] Update antd README and root prerequisites for current architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit antd README: - Complete rewrite reflecting current state: ant-core dependency, port discovery, external signer endpoints, wallet/EVM config - Fixed stale references: antctl → ant dev, ANT_PEERS → ANTD_PEERS - Added all new endpoints: wallet, upload prepare/finalize, cost - Added --rest-port/--grpc-port flags, external signer mode docs - Corrected endpoint paths to match actual routes - Updated project structure (port_file.rs, upload.rs, wallet.rs) - Marked graph and archive endpoints as stubs Root README: - Added ant-node as prerequisite for local testnet Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 1 + antd/README.md | 113 +++++++++++++++++++++++++++++++------------------ 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index a86fd8d..7eca23a 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ All data and file upload operations accept an optional `payment_mode` parameter - **Rust** toolchain — for building antd - **Python 3.10+** — for the dev CLI (`ant-dev`) and MCP server +- **ant-node** repo cloned as sibling (for local testnet only): `git clone https://github.com/WithAutonomi/ant-node ../ant-node` **Language-specific** (install only what you need): diff --git a/antd/README.md b/antd/README.md index 59a09d9..ae6e416 100644 --- a/antd/README.md +++ b/antd/README.md @@ -10,25 +10,34 @@ cargo build # Debug build cargo build --release # Release build ``` +antd depends on `ant-core` (from [WithAutonomi/ant-client](https://github.com/WithAutonomi/ant-client)) which is fetched automatically via Cargo git dependency. No sibling repos are needed to build antd itself. + ## Running ```bash # Default (connects to the default Autonomi network) cargo run -# Local testnet -AUTONOMI_WALLET_KEY="your_key" ANT_PEERS="/ip4/..." cargo run -- --network local +# Local testnet (use `ant dev start` to start a devnet first) +ANTD_PEERS="/ip4/..." \ +AUTONOMI_WALLET_KEY="hex_key" \ +EVM_RPC_URL="http://127.0.0.1:8545" \ +EVM_PAYMENT_TOKEN_ADDRESS="0x..." \ +EVM_DATA_PAYMENTS_ADDRESS="0x..." \ +cargo run -- --network local + +# With dynamic ports (for managed mode / port discovery) +cargo run -- --network local --rest-port 0 --grpc-port 0 +``` + +Or use the `ant dev start` CLI to start a full local testnet automatically: -# With all options -cargo run -- \ - --rest-addr 0.0.0.0:8082 \ - --grpc-addr 0.0.0.0:50051 \ - --network local \ - --peers "/ip4/127.0.0.1/udp/..." \ - --cors +```bash +pip install -e ant-dev/ +ant dev start # Starts devnet + antd with all env vars configured ``` -Or use the `ant dev start` CLI to start a full local testnet automatically. +Note: `ant dev start` requires the [ant-node](https://github.com/WithAutonomi/ant-node) repo cloned as a sibling to ant-sdk (for the `ant-devnet` binary). ## Configuration @@ -38,22 +47,28 @@ All options can be set via CLI flags or environment variables: |------|---------|---------|-------------| | `--rest-addr` | `ANTD_REST_ADDR` | `0.0.0.0:8082` | REST API listen address | | `--grpc-addr` | `ANTD_GRPC_ADDR` | `0.0.0.0:50051` | gRPC listen address | -| `--network` | `ANTD_NETWORK` | `default` | Network mode: `default`, `local`, `alpha` | +| `--rest-port` | `ANTD_REST_PORT` | *(from addr)* | Override REST port (use 0 for OS-assigned) | +| `--grpc-port` | `ANTD_GRPC_PORT` | *(from addr)* | Override gRPC port (use 0 for OS-assigned) | +| `--network` | `ANTD_NETWORK` | `default` | Network mode: `default`, `local` | | `--peers` | `ANTD_PEERS` | *(none)* | Comma-separated bootstrap peer multiaddrs | -| `--cors` | `ANTD_CORS` | `false` | Enable CORS headers for browser access | +| `--cors` | `ANTD_CORS` | `false` | Enable CORS headers (restricted to localhost) | -Additional environment variables consumed by the underlying Autonomi client: +### Wallet & EVM Configuration | Env Var | Description | |---------|-------------| -| `AUTONOMI_WALLET_KEY` | Hex-encoded wallet secret key for payments | -| `ANT_PEERS` | Bootstrap peer multiaddrs (alternative to `--peers`) | +| `AUTONOMI_WALLET_KEY` | Hex-encoded wallet private key for payments (direct wallet mode) | +| `EVM_RPC_URL` | EVM JSON-RPC endpoint (default: `http://127.0.0.1:8545`) | +| `EVM_PAYMENT_TOKEN_ADDRESS` | Payment token contract address | +| `EVM_DATA_PAYMENTS_ADDRESS` | Data payments contract address | + +antd supports two wallet modes: +- **Direct wallet**: Set `AUTONOMI_WALLET_KEY` — antd signs payment transactions internally +- **External signer**: Set only `EVM_RPC_URL` (no private key) — use the two-phase upload API (`/v1/upload/prepare` + `/v1/upload/finalize`) to sign transactions externally -## Network Modes +## Port Discovery -- **`default`** — Connects to the public Autonomi mainnet. -- **`local`** — Connects to a local testnet started via `antctl local run`. Requires `ANT_PEERS` from the bootstrap cache. -- **`alpha`** — Connects to the alpha/test network. +On startup, antd writes a `daemon.port` file containing the actual REST port, gRPC port, and PID. All SDKs read this file for automatic daemon discovery. See the [root README](../README.md#port-discovery) for details. ## API Endpoints @@ -61,25 +76,38 @@ Additional environment variables consumed by the underlying Autonomi client: | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | `/health` | Health check | -| `POST` | `/v1/data/public` | Store public data | +| **Health** | | | +| `GET` | `/health` | Health check and network status | +| **Data** | | | +| `POST` | `/v1/data/public` | Store public data (accepts `payment_mode`) | | `GET` | `/v1/data/public/{address}` | Retrieve public data | | `POST` | `/v1/data/private` | Store private (encrypted) data | -| `POST` | `/v1/data/private/get` | Retrieve private data by data map | +| `GET` | `/v1/data/private` | Retrieve private data by data map | | `POST` | `/v1/data/cost` | Estimate data storage cost | +| **Chunks** | | | | `POST` | `/v1/chunks` | Store a raw chunk | | `GET` | `/v1/chunks/{address}` | Retrieve a chunk | +| **Files** | | | +| `POST` | `/v1/files/upload/public` | Upload a file (accepts `payment_mode`) | +| `POST` | `/v1/files/download/public` | Download a file | +| `POST` | `/v1/dirs/upload/public` | Upload a directory | +| `POST` | `/v1/dirs/download/public` | Download a directory | +| `POST` | `/v1/cost/file` | Estimate file upload cost | +| **External Signer** | | | +| `POST` | `/v1/upload/prepare` | Prepare file upload for external signing | +| `POST` | `/v1/upload/finalize` | Finalize upload with external tx hashes | +| **Wallet** | | | +| `GET` | `/v1/wallet/address` | Get wallet public address | +| `GET` | `/v1/wallet/balance` | Get token and gas balances | +| `POST` | `/v1/wallet/approve` | Approve token spend for payment contracts | +| **Graph** *(stub)* | | | | `POST` | `/v1/graph` | Create a graph entry | | `GET` | `/v1/graph/{address}` | Get a graph entry | | `HEAD` | `/v1/graph/{address}` | Check graph entry existence | | `POST` | `/v1/graph/cost` | Estimate graph entry cost | -| `POST` | `/v1/files/upload` | Upload a file | -| `POST` | `/v1/files/download` | Download a file | -| `POST` | `/v1/files/upload/dir` | Upload a directory | -| `POST` | `/v1/files/download/dir` | Download a directory | -| `GET` | `/v1/files/archive/{address}` | Get archive manifest | -| `POST` | `/v1/files/archive` | Create archive manifest | -| `POST` | `/v1/files/cost` | Estimate file upload cost | +| **Archives** *(stub)* | | | +| `GET` | `/v1/archives/public/{address}` | Get archive manifest | +| `POST` | `/v1/archives/public` | Create archive manifest | ### gRPC API (default: `localhost:50051`) @@ -88,8 +116,8 @@ gRPC services mirror the REST API. Proto definitions are in `proto/antd/v1/`: - `HealthService` — Health check - `DataService` — Public/private data operations - `ChunkService` — Raw chunk operations -- `GraphService` — Graph entry operations -- `FileService` — File upload/download +- `GraphService` — Graph entry operations *(stub)* +- `FileService` — File upload/download *(stub)* - `EventService` — Event streaming ## Project Structure @@ -107,18 +135,21 @@ antd/ │ ├── files.proto │ └── events.proto └── src/ - ├── main.rs # Entry point + ├── main.rs # Entry point, P2P node + client setup ├── config.rs # CLI/env configuration - ├── error.rs # Error types - ├── state.rs # Shared daemon state - ├── types.rs # Common types + ├── error.rs # Error types + ant-core error mapping + ├── state.rs # Shared daemon state (Client, pending uploads) + ├── port_file.rs # Port file write/cleanup for SDK discovery + ├── types.rs # Request/response types ├── rest/ # Axum REST handlers - │ ├── mod.rs - │ ├── data.rs - │ ├── chunks.rs - │ ├── graph.rs - │ ├── files.rs - │ └── events.rs + │ ├── mod.rs # Router + CORS + │ ├── data.rs # Data put/get/cost + │ ├── chunks.rs # Chunk put/get + │ ├── files.rs # File/dir upload/download/cost + │ ├── upload.rs # External signer two-phase upload + │ ├── wallet.rs # Wallet address/balance/approve + │ ├── graph.rs # Graph entries (stub) + │ └── events.rs # Event streaming (stub) └── grpc/ # Tonic gRPC service ├── mod.rs └── service.rs From b42228b46f657adf334fc84793226a1e5f9736b4 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 14:56:54 +0100 Subject: [PATCH 16/20] Pass merkle payments contract address through all start scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit antd was passing None for merkle_payments_address to evmlib::Network::new_custom, which meant merkle batch payments couldn't work on local testnets. The devnet manifest includes the merkle contract address but it wasn't being forwarded. Fixes: - antd main.rs: read EVM_MERKLE_PAYMENTS_ADDRESS env var, pass to Network::new_custom (both wallet and external-signer paths) - scripts/start-local.sh: extract merkle_payments_address from devnet manifest, pass as env var - scripts/start-local.ps1: same - ant-dev cmd_start.py: pass all EVM env vars (was only passing ANTD_PEERS and AUTONOMI_WALLET_KEY — missing rpc_url, token addr, payments addr, and merkle addr) - antd README: document EVM_MERKLE_PAYMENTS_ADDRESS Co-Authored-By: Claude Opus 4.6 (1M context) --- ant-dev/src/ant_dev/cmd_start.py | 7 +++++++ antd/README.md | 1 + antd/src/main.rs | 6 ++++-- scripts/start-local.ps1 | 12 +++++++----- scripts/start-local.sh | 2 ++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ant-dev/src/ant_dev/cmd_start.py b/ant-dev/src/ant_dev/cmd_start.py index e09e9d1..70ab6f1 100644 --- a/ant-dev/src/ant_dev/cmd_start.py +++ b/ant-dev/src/ant_dev/cmd_start.py @@ -122,6 +122,13 @@ def run(args) -> None: } if wallet_key: antd_env["AUTONOMI_WALLET_KEY"] = wallet_key + if manifest.get("evm"): + evm = manifest["evm"] + antd_env["EVM_RPC_URL"] = evm.get("rpc_url", "") + antd_env["EVM_PAYMENT_TOKEN_ADDRESS"] = evm.get("payment_token_address", "") + antd_env["EVM_DATA_PAYMENTS_ADDRESS"] = evm.get("data_payments_address", "") + if evm.get("merkle_payments_address"): + antd_env["EVM_MERKLE_PAYMENTS_ADDRESS"] = evm["merkle_payments_address"] antd_cmd = ["cargo", "run", "--", "--network", "local"] antd_proc = start_process(antd_cmd, cwd=antd_dir, env=antd_env, log_file=LOG_FILE) diff --git a/antd/README.md b/antd/README.md index ae6e416..e9661d4 100644 --- a/antd/README.md +++ b/antd/README.md @@ -61,6 +61,7 @@ All options can be set via CLI flags or environment variables: | `EVM_RPC_URL` | EVM JSON-RPC endpoint (default: `http://127.0.0.1:8545`) | | `EVM_PAYMENT_TOKEN_ADDRESS` | Payment token contract address | | `EVM_DATA_PAYMENTS_ADDRESS` | Data payments contract address | +| `EVM_MERKLE_PAYMENTS_ADDRESS` | Merkle batch payments contract address (optional) | antd supports two wallet modes: - **Direct wallet**: Set `AUTONOMI_WALLET_KEY` — antd signs payment transactions internally diff --git a/antd/src/main.rs b/antd/src/main.rs index 7e123c9..fbe1eef 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -140,12 +140,13 @@ async fn main() -> Result<(), Box> { .unwrap_or_default(); let payments_addr = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") .unwrap_or_default(); + let merkle_addr = std::env::var("EVM_MERKLE_PAYMENTS_ADDRESS").ok(); tracing::info!(%rpc_url, "loading EVM wallet..."); let network = evmlib::Network::new_custom( &rpc_url, &token_addr, &payments_addr, - None, + merkle_addr.as_deref(), ); match Wallet::new_from_private_key(network, &wallet_key) { Ok(w) => { @@ -165,11 +166,12 @@ async fn main() -> Result<(), Box> { .unwrap_or_default(); let payments_addr = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") .unwrap_or_default(); + let merkle_addr = std::env::var("EVM_MERKLE_PAYMENTS_ADDRESS").ok(); let network = evmlib::Network::new_custom( &rpc_url, &token_addr, &payments_addr, - None, + merkle_addr.as_deref(), ); client = client.with_evm_network(network); tracing::info!(%rpc_url, "EVM network configured (external signer mode)"); diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index 66db118..7df05de 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -90,6 +90,7 @@ $walletKey = $manifest.evm.wallet_private_key -replace '^0x', '' $evmRpcUrl = $manifest.evm.rpc_url $evmTokenAddr = $manifest.evm.payment_token_address $evmPaymentsAddr = $manifest.evm.data_payments_address +$evmMerkleAddr = if ($manifest.evm.merkle_payments_address) { $manifest.evm.merkle_payments_address } else { "" } Write-Host " Devnet ready: $($manifest.node_count) nodes, base port $($manifest.base_port)" -ForegroundColor Green Write-Host " EVM: $evmRpcUrl" -ForegroundColor Green @@ -97,11 +98,12 @@ Write-Host " EVM: $evmRpcUrl" -ForegroundColor Green # ── 3. Start antd ── Write-Host "[2/3] Starting antd..." -ForegroundColor Yellow $antdEnv = @{ - ANTD_PEERS = $bootstrapPeers - AUTONOMI_WALLET_KEY = $walletKey - EVM_RPC_URL = $evmRpcUrl - EVM_PAYMENT_TOKEN_ADDRESS = $evmTokenAddr - EVM_DATA_PAYMENTS_ADDRESS = $evmPaymentsAddr + ANTD_PEERS = $bootstrapPeers + AUTONOMI_WALLET_KEY = $walletKey + EVM_RPC_URL = $evmRpcUrl + EVM_PAYMENT_TOKEN_ADDRESS = $evmTokenAddr + EVM_DATA_PAYMENTS_ADDRESS = $evmPaymentsAddr + EVM_MERKLE_PAYMENTS_ADDRESS = $evmMerkleAddr } # Merge with current environment $mergedEnv = [System.Collections.Generic.Dictionary[string,string]]::new() diff --git a/scripts/start-local.sh b/scripts/start-local.sh index 243c79c..1050e2b 100644 --- a/scripts/start-local.sh +++ b/scripts/start-local.sh @@ -99,6 +99,7 @@ print(k[2:] if k.startswith('0x') else k) EVM_RPC_URL=$(python -c "import json; print(json.load(open('$MANIFEST_FILE'))['evm']['rpc_url'])" 2>/dev/null) EVM_TOKEN_ADDR=$(python -c "import json; print(json.load(open('$MANIFEST_FILE'))['evm']['payment_token_address'])" 2>/dev/null) EVM_PAYMENTS_ADDR=$(python -c "import json; print(json.load(open('$MANIFEST_FILE'))['evm']['data_payments_address'])" 2>/dev/null) +EVM_MERKLE_ADDR=$(python -c "import json; print(json.load(open('$MANIFEST_FILE'))['evm'].get('merkle_payments_address', ''))" 2>/dev/null) NODE_COUNT=$(python -c "import json; print(json.load(open('$MANIFEST_FILE')).get('node_count', '?'))" 2>/dev/null) BASE_PORT=$(python -c "import json; print(json.load(open('$MANIFEST_FILE')).get('base_port', '?'))" 2>/dev/null) @@ -113,6 +114,7 @@ echo -e "${YELLOW}[2/3] Starting antd...${NC}" EVM_RPC_URL="$EVM_RPC_URL" \ EVM_PAYMENT_TOKEN_ADDRESS="$EVM_TOKEN_ADDR" \ EVM_DATA_PAYMENTS_ADDRESS="$EVM_PAYMENTS_ADDR" \ + EVM_MERKLE_PAYMENTS_ADDRESS="$EVM_MERKLE_ADDR" \ cargo run -- --network local 2>&1) & ANTD_PID=$! From 599827a9aabfdb5b06a58289ac70c3d4422ac61e Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 16:06:10 +0100 Subject: [PATCH 17/20] Remove private key from state file and console output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wallet private key was being: - Written to ~/.ant-dev/state.json in plaintext - Printed (first 10 chars) to console on startup - Displayed in full via `ant dev wallet show` While these are ephemeral Anvil test keys on local devnet, the pattern is unsafe if someone uses a real key. Fixes: - State file: store "wallet_configured": true instead of the key - Console: show "Wallet: configured" instead of key prefix - `ant dev wallet show`: queries antd for address/balance via REST instead of reading the key from state - Scripts: same change for start-local.sh and start-local.ps1 The private key remains only in the AUTONOMI_WALLET_KEY env var passed to the antd process — it never touches disk or console. Co-Authored-By: Claude Opus 4.6 (1M context) --- ant-dev/src/ant_dev/cmd_start.py | 4 ++-- ant-dev/src/ant_dev/cmd_status.py | 6 +++--- ant-dev/src/ant_dev/cmd_wallet.py | 34 +++++++++++++++++++++---------- scripts/start-local.ps1 | 2 +- scripts/start-local.sh | 2 +- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/ant-dev/src/ant_dev/cmd_start.py b/ant-dev/src/ant_dev/cmd_start.py index 70ab6f1..02225a6 100644 --- a/ant-dev/src/ant_dev/cmd_start.py +++ b/ant-dev/src/ant_dev/cmd_start.py @@ -142,7 +142,7 @@ def run(args) -> None: save_state({ "devnet_pid": devnet_proc.pid, "antd_pid": antd_proc.pid, - "wallet_key": wallet_key or "", + "wallet_configured": bool(wallet_key), "bootstrap_peers": bootstrap_peers, }) @@ -154,7 +154,7 @@ def run(args) -> None: print(white(f" REST: {rest_url}")) print(white(" gRPC: localhost:50051")) if wallet_key: - print(white(f" Key: {wallet_key[:10]}...")) + print(white(" Wallet: configured")) print() print(gray("Quick test:")) print(gray(f" curl {rest_url}/health")) diff --git a/ant-dev/src/ant_dev/cmd_status.py b/ant-dev/src/ant_dev/cmd_status.py index 17cf856..98e3635 100644 --- a/ant-dev/src/ant_dev/cmd_status.py +++ b/ant-dev/src/ant_dev/cmd_status.py @@ -72,8 +72,8 @@ def run(args) -> None: except (httpx.HTTPError, OSError): print(f" REST health: {red('unreachable')}") - # Wallet key - if key := state.get("wallet_key"): - print(f" Wallet key: {key[:10]}...") + # Wallet status + if state.get("wallet_configured"): + print(f" Wallet: {green('configured')}") print() diff --git a/ant-dev/src/ant_dev/cmd_wallet.py b/ant-dev/src/ant_dev/cmd_wallet.py index e28ae2d..4491432 100644 --- a/ant-dev/src/ant_dev/cmd_wallet.py +++ b/ant-dev/src/ant_dev/cmd_wallet.py @@ -1,4 +1,4 @@ -"""``ant dev wallet [show|fund]`` — Show or fund the test wallet.""" +"""``ant dev wallet [show|fund]`` — Show wallet info or fund the test wallet.""" from __future__ import annotations @@ -25,30 +25,42 @@ def _discover_rest_url() -> str: def run(args) -> None: state = load_state() action = args.action + rest_url = _discover_rest_url() if action == "show": - key = state.get("wallet_key") - if not key: - print("No wallet key found. Is the local environment running?") + if not state.get("wallet_configured"): + print("No wallet configured. Is the local environment running?") print(" ant dev start") sys.exit(1) - print(f"Wallet key: {key[:10]}...{key[-6:]}") - print(f"Full key: {key}") + # Query wallet address and balance from antd (never display the key) + try: + addr_resp = httpx.get(f"{rest_url}/v1/wallet/address", timeout=5) + bal_resp = httpx.get(f"{rest_url}/v1/wallet/balance", timeout=5) + if addr_resp.status_code == 200 and bal_resp.status_code == 200: + address = addr_resp.json().get("address", "unknown") + balance = bal_resp.json().get("balance", "unknown") + gas = bal_resp.json().get("gas_balance", "unknown") + print(f"Address: {address}") + print(f"Balance: {balance} atto") + print(f"Gas balance: {gas} wei") + else: + print("Wallet not available. Is antd running with AUTONOMI_WALLET_KEY?") + sys.exit(1) + except (httpx.HTTPError, OSError): + print("Cannot reach antd daemon. Is it running?") + sys.exit(1) elif action == "fund": - key = state.get("wallet_key") - if not key: - print("No wallet key found. Start the environment first.") + if not state.get("wallet_configured"): + print("No wallet configured. Start the environment first.") sys.exit(1) # On local testnet the EVM testnet already funds this key. - # Verify via health check. try: rest_url = _discover_rest_url() r = httpx.get(f"{rest_url}/health", timeout=5) data = r.json() if data.get("status") == "ok" or data.get("ok", False): print("Wallet is already funded on local testnet.") - print(f"Key: {key[:10]}...") else: print("Daemon is not healthy. Check: ant dev status") except (httpx.HTTPError, OSError): diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index 7df05de..67e8775 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -162,7 +162,7 @@ if ($ready) { Write-Host "" Write-Host " REST: http://localhost:8082" -ForegroundColor White Write-Host " gRPC: localhost:50051" -ForegroundColor White - Write-Host " Key: $($walletKey.Substring(0,10))..." -ForegroundColor White + Write-Host " Wallet: configured" -ForegroundColor White Write-Host "" Write-Host "Run tests:" -ForegroundColor Gray Write-Host " .\scripts\test-api.ps1" -ForegroundColor Gray diff --git a/scripts/start-local.sh b/scripts/start-local.sh index 1050e2b..e7d0a5f 100644 --- a/scripts/start-local.sh +++ b/scripts/start-local.sh @@ -139,7 +139,7 @@ if $READY; then echo "" echo -e "${WHITE} REST: http://localhost:8082${NC}" echo -e "${WHITE} gRPC: localhost:50051${NC}" - echo -e "${WHITE} Key: ${WALLET_KEY:0:10}...${NC}" + echo -e "${WHITE} Wallet: configured${NC}" echo "" echo -e "${GRAY}Run tests:${NC}" echo -e "${GRAY} ./scripts/test-api.sh${NC}" From 1cc35913b86d06d48a96bb927bf9383dc983e848 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 16:56:31 +0100 Subject: [PATCH 18/20] Add external signer support for data uploads (POST /v1/data/prepare) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ant-core PR #15 merged (data_prepare_upload), the external signer flow now works for both files and raw data. antd: - POST /v1/data/prepare: accepts base64 data, encrypts, collects quotes, returns PaymentIntent (same response as /v1/upload/prepare) - Uses same finalize endpoint (POST /v1/upload/finalize) - Updated ant-core to 7bcb6cbc (includes data_prepare_upload) SDK bindings: prepare_data_upload() added across all 15 languages + MCP server. Returns same PrepareUploadResult type — finalize_upload() works for both file and data uploads unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) --- antd-cpp/include/antd/client.hpp | 4 + antd-cpp/src/client.cpp | 27 +++++++ antd-csharp/Antd.Sdk/AntdRestClient.cs | 11 +++ antd-csharp/Antd.Sdk/IAntdClient.cs | 1 + antd-dart/lib/src/client.dart | 7 ++ antd-elixir/lib/antd/client.ex | 34 +++++++++ antd-go/client.go | 39 +++++++++- antd-go/models.go | 5 +- .../java/com/autonomi/antd/AntdClient.java | 25 +++++++ antd-js/src/rest-client.ts | 24 ++++++ .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 8 ++ .../kotlin/com/autonomi/sdk/IAntdClient.kt | 1 + antd-lua/src/antd/client.lua | 33 +++++++++ antd-mcp/src/antd_mcp/server.py | 49 +++++++++++- antd-php/src/AntdClient.php | 40 ++++++++++ antd-py/src/antd/_rest.py | 54 ++++++++++++++ antd-ruby/lib/antd/client.rb | 23 ++++++ antd-rust/src/client.rs | 36 +++++++++ .../Sources/AntdSdk/AntdClientProtocol.swift | 5 ++ .../Sources/AntdSdk/AntdRestClient.swift | 8 ++ antd-zig/src/antd.zig | 10 +++ antd/Cargo.lock | 2 +- antd/README.md | 1 + antd/src/rest/mod.rs | 1 + antd/src/rest/upload.rs | 74 +++++++++++++++++-- antd/src/types.rs | 18 ++++- llms-full.txt | 19 +++++ llms.txt | 3 +- 28 files changed, 546 insertions(+), 16 deletions(-) diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index aefe0c5..8105415 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -130,6 +130,10 @@ class Client { /// Prepare a file upload for external signing. PrepareUploadResult prepare_upload(std::string_view path); + /// Prepare a data upload for external signing. + /// Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + PrepareUploadResult prepare_data_upload(const std::vector& data); + /// Finalize an upload after an external signer has submitted payment transactions. FinalizeUploadResult finalize_upload(std::string_view upload_id, const std::map& tx_hashes); diff --git a/antd-cpp/src/client.cpp b/antd-cpp/src/client.cpp index 49baf71..036cd84 100644 --- a/antd-cpp/src/client.cpp +++ b/antd-cpp/src/client.cpp @@ -388,6 +388,33 @@ PrepareUploadResult Client::prepare_upload(std::string_view path) { return result; } +PrepareUploadResult Client::prepare_data_upload(const std::vector& data) { + auto j = impl_->do_json("POST", "/v1/data/prepare", json{ + {"data", detail::base64_encode(data)}, + }); + + PrepareUploadResult result; + result.upload_id = j.value("upload_id", ""); + result.total_amount = j.value("total_amount", ""); + result.data_payments_address = j.value("data_payments_address", ""); + result.payment_token_address = j.value("payment_token_address", ""); + result.rpc_url = j.value("rpc_url", ""); + + if (j.contains("payments") && j["payments"].is_array()) { + for (const auto& p : j["payments"]) { + if (p.is_object()) { + result.payments.push_back(PaymentInfo{ + .quote_hash = p.value("quote_hash", ""), + .rewards_address = p.value("rewards_address", ""), + .amount = p.value("amount", ""), + }); + } + } + } + + return result; +} + FinalizeUploadResult Client::finalize_upload(std::string_view upload_id, const std::map& tx_hashes) { json hashes = json::object(); diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index 2e26928..6fc05cc 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -267,6 +267,17 @@ public async Task PrepareUploadAsync(string path) return new PrepareUploadResult(resp.UploadId, payments, resp.TotalAmount, resp.DataPaymentsAddress, resp.PaymentTokenAddress, resp.RpcUrl); } + /// + /// Prepares a data upload for external signing. + /// Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + /// + public async Task PrepareDataUploadAsync(byte[] data) + { + var resp = await PostJsonAsync("/v1/data/prepare", new { data = Convert.ToBase64String(data) }); + var payments = resp.Payments?.Select(p => new PaymentInfo(p.QuoteHash, p.RewardsAddress, p.Amount)).ToList() ?? []; + return new PrepareUploadResult(resp.UploadId, payments, resp.TotalAmount, resp.DataPaymentsAddress, resp.PaymentTokenAddress, resp.RpcUrl); + } + /// /// Finalizes an upload after an external signer has submitted payment transactions. /// diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index 6ee945b..fccd373 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -38,5 +38,6 @@ public interface IAntdClient : IDisposable // External Signer (Two-Phase Upload) Task PrepareUploadAsync(string path); + Task PrepareDataUploadAsync(byte[] data); Task FinalizeUploadAsync(string uploadId, Dictionary txHashes); } diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index 1f23435..e634415 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -337,6 +337,13 @@ class AntdClient { return PrepareUploadResult.fromJson(json!); } + /// Prepares a data upload for external signing. + /// Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + Future prepareDataUpload(Uint8List data) async { + final json = await _doJson('POST', '/v1/data/prepare', {'data': _b64Encode(data)}); + return PrepareUploadResult.fromJson(json!); + } + /// Finalizes an upload after an external signer has submitted payment transactions. Future finalizeUpload( String uploadId, diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index 55b5fca..d6e93ab 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -534,6 +534,40 @@ defmodule Antd.Client do @spec prepare_upload!(t(), String.t()) :: Antd.PrepareUploadResult.t() def prepare_upload!(client, path), do: unwrap!(prepare_upload(client, path)) + @doc "Prepares a data upload for external signing." + @spec prepare_data_upload(t(), binary()) :: {:ok, Antd.PrepareUploadResult.t()} | {:error, Exception.t()} + def prepare_data_upload(%__MODULE__{} = client, data) when is_binary(data) do + case do_json(client, :post, "/v1/data/prepare", %{data: Base.encode64(data)}) do + {:ok, body} -> + payments = + (body["payments"] || []) + |> Enum.map(fn p -> + %Antd.PaymentInfo{ + quote_hash: p["quote_hash"], + rewards_address: p["rewards_address"], + amount: p["amount"] + } + end) + + {:ok, + %Antd.PrepareUploadResult{ + upload_id: body["upload_id"], + payments: payments, + total_amount: body["total_amount"], + data_payments_address: body["data_payments_address"], + payment_token_address: body["payment_token_address"], + rpc_url: body["rpc_url"] + }} + + {:error, _} = err -> + err + end + end + + @doc "Like `prepare_data_upload/2` but raises on error." + @spec prepare_data_upload!(t(), binary()) :: Antd.PrepareUploadResult.t() + def prepare_data_upload!(client, data), do: unwrap!(prepare_data_upload(client, data)) + @doc "Finalizes an upload after an external signer has submitted payment transactions." @spec finalize_upload(t(), String.t(), map()) :: {:ok, Antd.FinalizeUploadResult.t()} | {:error, Exception.t()} def finalize_upload(%__MODULE__{} = client, upload_id, tx_hashes) do diff --git a/antd-go/client.go b/antd-go/client.go index c6e813a..24accfd 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -520,17 +520,50 @@ func (c *Client) PrepareUpload(ctx context.Context, path string) (*PrepareUpload }, nil } +// PrepareDataUpload prepares a 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 FinalizeUpload. +func (c *Client) PrepareDataUpload(ctx context.Context, data []byte) (*PrepareUploadResult, error) { + j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/data/prepare", map[string]any{ + "data": b64Encode(data), + }) + if err != nil { + return nil, err + } + var payments []PaymentInfo + for _, p := range arrAt(j, "payments") { + if pm, ok := p.(map[string]any); ok { + payments = append(payments, PaymentInfo{ + QuoteHash: str(pm, "quote_hash"), + RewardsAddress: str(pm, "rewards_address"), + Amount: str(pm, "amount"), + }) + } + } + return &PrepareUploadResult{ + UploadID: str(j, "upload_id"), + Payments: payments, + TotalAmount: str(j, "total_amount"), + DataPaymentsAddress: str(j, "data_payments_address"), + PaymentTokenAddress: str(j, "payment_token_address"), + RPCUrl: str(j, "rpc_url"), + }, nil +} + // FinalizeUpload finalizes an upload after an external signer has submitted payment transactions. // txHashes maps quote_hash to tx_hash for each payment. -func (c *Client) FinalizeUpload(ctx context.Context, uploadID string, txHashes map[string]string) (*FinalizeUploadResult, error) { +// If storeDataMap is true, the DataMap is also stored on-network and Address is returned (requires a daemon wallet). +func (c *Client) FinalizeUpload(ctx context.Context, uploadID string, txHashes map[string]string, storeDataMap bool) (*FinalizeUploadResult, error) { j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/upload/finalize", map[string]any{ - "upload_id": uploadID, - "tx_hashes": txHashes, + "upload_id": uploadID, + "tx_hashes": txHashes, + "store_data_map": storeDataMap, }) if err != nil { return nil, err } return &FinalizeUploadResult{ + DataMap: str(j, "data_map"), Address: str(j, "address"), ChunksStored: num64(j, "chunks_stored"), }, nil diff --git a/antd-go/models.go b/antd-go/models.go index 11045f9..a719478 100644 --- a/antd-go/models.go +++ b/antd-go/models.go @@ -70,6 +70,7 @@ type PrepareUploadResult struct { // FinalizeUploadResult is the result of finalizing an externally-signed upload. type FinalizeUploadResult struct { - Address string `json:"address"` // hex address of stored data - ChunksStored int64 `json:"chunks_stored"` // number of chunks stored + DataMap string `json:"data_map"` // hex-encoded serialized DataMap (always returned) + Address string `json:"address,omitempty"` // network address (only when store_data_map=true) + ChunksStored int64 `json:"chunks_stored"` // number of chunks stored } diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index b5ad47c..8b2e287 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -408,6 +408,31 @@ public PrepareUploadResult prepareUpload(String path) { ); } + /** + * Prepares a 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 {@link #finalizeUpload}. + * + * @param data raw bytes to upload + * @return PrepareUploadResult with upload_id, payments, and contract details + */ + public PrepareUploadResult prepareDataUpload(byte[] data) { + String body = Json.object("data", b64Encode(data)); + Map j = doJson("POST", "/v1/data/prepare", body); + List payments = new ArrayList<>(); + for (Map pm : listOfMaps(j, "payments")) { + payments.add(new PaymentInfo(str(pm, "quote_hash"), str(pm, "rewards_address"), str(pm, "amount"))); + } + return new PrepareUploadResult( + str(j, "upload_id"), + Collections.unmodifiableList(payments), + str(j, "total_amount"), + str(j, "data_payments_address"), + str(j, "payment_token_address"), + str(j, "rpc_url") + ); + } + /** * Finalizes an upload after an external signer has submitted payment transactions. * diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index f0f4846..0e62b32 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -339,6 +339,30 @@ export class RestClient { }; } + /** Prepare a data upload for external signing. */ + async prepareDataUpload(data: Buffer): Promise { + const j = await this.postJson<{ + upload_id: string; + payments: { quote_hash: string; rewards_address: string; amount: string }[]; + total_amount: string; + data_payments_address: string; + payment_token_address: string; + rpc_url: string; + }>("/v1/data/prepare", { data: RestClient.b64(data) }); + return { + uploadId: j.upload_id, + payments: (j.payments ?? []).map((p) => ({ + quoteHash: p.quote_hash, + rewardsAddress: p.rewards_address, + amount: p.amount, + })), + totalAmount: j.total_amount, + dataPaymentsAddress: j.data_payments_address, + paymentTokenAddress: j.payment_token_address, + rpcUrl: j.rpc_url, + }; + } + /** Finalize an upload after an external signer has submitted payment transactions. */ async finalizeUpload( uploadId: string, diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index dbb1e66..0d831bf 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -288,6 +288,14 @@ class AntdRestClient( return PrepareUploadResult(resp.uploadId, payments, resp.totalAmount, resp.dataPaymentsAddress, resp.paymentTokenAddress, resp.rpcUrl) } + /** Prepares a data upload for external signing. */ + override suspend fun prepareDataUpload(data: ByteArray): PrepareUploadResult { + val body = buildJsonObject { put("data", b64(data)) }.toString() + val resp = postJson("/v1/data/prepare", body) + val payments = resp.payments?.map { PaymentInfo(it.quoteHash, it.rewardsAddress, it.amount) } ?: emptyList() + return PrepareUploadResult(resp.uploadId, payments, resp.totalAmount, resp.dataPaymentsAddress, resp.paymentTokenAddress, resp.rpcUrl) + } + /** Finalizes an upload after an external signer has submitted payment transactions. */ override suspend fun finalizeUpload(uploadId: String, txHashes: Map): FinalizeUploadResult { val body = buildJsonObject { diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt index 0cd5288..902b486 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt @@ -46,5 +46,6 @@ interface IAntdClient : Closeable { // External Signer (Two-Phase Upload) suspend fun prepareUpload(path: String): PrepareUploadResult + suspend fun prepareDataUpload(data: ByteArray): PrepareUploadResult suspend fun finalizeUpload(uploadId: String, txHashes: Map): FinalizeUploadResult } diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index 3af9ae7..e0f5595 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -482,6 +482,39 @@ function Client:prepare_upload(path) }, nil end +--- Prepare a data upload for external signing. +-- Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. +-- @param data string raw bytes to upload +-- @return table|nil PrepareUploadResult, error|nil +function Client:prepare_data_upload(data) + local j, _, err = self:_do_json("POST", "/v1/data/prepare", { + data = base64.encode(data), + }) + if err then return nil, err end + + local payments = {} + if j.payments and type(j.payments) == "table" then + for _, p in ipairs(j.payments) do + if type(p) == "table" then + payments[#payments + 1] = { + quote_hash = str(p, "quote_hash"), + rewards_address = str(p, "rewards_address"), + amount = str(p, "amount"), + } + end + end + end + + return { + upload_id = str(j, "upload_id"), + payments = payments, + total_amount = str(j, "total_amount"), + data_payments_address = str(j, "data_payments_address"), + payment_token_address = str(j, "payment_token_address"), + rpc_url = str(j, "rpc_url"), + }, nil +end + --- Finalize an upload after an external signer has submitted payment transactions. -- @param upload_id string the upload ID from prepare_upload -- @param tx_hashes table map of quote_hash to tx_hash diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index fbb8a4c..d464328 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -656,7 +656,54 @@ async def prepare_upload( # --------------------------------------------------------------------------- -# Tool 19: finalize_upload +# Tool 19: prepare_data_upload +# --------------------------------------------------------------------------- + + +@mcp.tool() +async def prepare_data_upload( + data: str, +) -> str: + """Prepare a data upload for external signing (two-phase upload). + + Takes base64-encoded data and returns payment details including contract + addresses, quote hashes, and amounts that an external signer must process + before calling finalize_upload. + + Args: + data: Base64-encoded bytes to upload. + + Returns: + JSON with upload_id, payments array (quote_hash, rewards_address, amount), + total_amount, data_payments_address, payment_token_address, and rpc_url. + """ + client, network = _get_ctx() + try: + raw = base64.b64decode(data) + result = await client.prepare_data_upload(raw) + return _ok({ + "upload_id": result.upload_id, + "payments": [ + { + "quote_hash": p.quote_hash, + "rewards_address": p.rewards_address, + "amount": p.amount, + } + for p in result.payments + ], + "total_amount": result.total_amount, + "data_payments_address": result.data_payments_address, + "payment_token_address": result.payment_token_address, + "rpc_url": result.rpc_url, + }, network) + except AntdError as exc: + return _err_antd(exc, network) + except Exception as exc: + return _err(exc, network) + + +# --------------------------------------------------------------------------- +# Tool 20: finalize_upload # --------------------------------------------------------------------------- diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index ba95d3b..6d4801e 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -932,6 +932,46 @@ public function prepareUploadAsync(string $path): PromiseInterface ); } + /** + * Prepare a data upload for external signing. + * Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + * + * @param string $data Raw bytes to upload. + * @return array{upload_id: string, payments: array, total_amount: string, data_payments_address: string, payment_token_address: string, rpc_url: string} + */ + public function prepareDataUpload(string $data): array + { + $json = $this->doJson('POST', '/v1/data/prepare', ['data' => $this->b64Encode($data)]); + return [ + 'upload_id' => $json['upload_id'] ?? '', + 'payments' => $json['payments'] ?? [], + 'total_amount' => $json['total_amount'] ?? '', + 'data_payments_address' => $json['data_payments_address'] ?? '', + 'payment_token_address' => $json['payment_token_address'] ?? '', + 'rpc_url' => $json['rpc_url'] ?? '', + ]; + } + + /** + * Async: Prepare a data upload for external signing. + * + * @param string $data Raw bytes to upload. + * @return PromiseInterface + */ + public function prepareDataUploadAsync(string $data): PromiseInterface + { + return $this->doJsonAsync('POST', '/v1/data/prepare', ['data' => $this->b64Encode($data)])->then( + fn(?array $json) => [ + 'upload_id' => $json['upload_id'] ?? '', + 'payments' => $json['payments'] ?? [], + 'total_amount' => $json['total_amount'] ?? '', + 'data_payments_address' => $json['data_payments_address'] ?? '', + 'payment_token_address' => $json['payment_token_address'] ?? '', + 'rpc_url' => $json['rpc_url'] ?? '', + ], + ); + } + /** * Finalize an upload after an external signer has submitted payment transactions. * diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 09c8155..59ac547 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -287,6 +287,33 @@ def prepare_upload(self, path: str) -> PrepareUploadResult: rpc_url=j.get("rpc_url", ""), ) + def prepare_data_upload(self, data: bytes) -> PrepareUploadResult: + """Prepare a 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. + """ + resp = self._http.post("/v1/data/prepare", json={"data": _b64(data)}) + _check(resp) + j = resp.json() + payments = [ + PaymentInfo( + quote_hash=p["quote_hash"], + rewards_address=p["rewards_address"], + amount=p["amount"], + ) + for p in j.get("payments", []) + ] + return PrepareUploadResult( + upload_id=j["upload_id"], + payments=payments, + total_amount=j.get("total_amount", ""), + data_payments_address=j.get("data_payments_address", ""), + payment_token_address=j.get("payment_token_address", ""), + rpc_url=j.get("rpc_url", ""), + ) + def finalize_upload(self, upload_id: str, tx_hashes: dict[str, str]) -> FinalizeUploadResult: """Finalize an upload after an external signer has submitted payment transactions. @@ -545,6 +572,33 @@ async def prepare_upload(self, path: str) -> PrepareUploadResult: rpc_url=j.get("rpc_url", ""), ) + async def prepare_data_upload(self, data: bytes) -> PrepareUploadResult: + """Prepare a 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. + """ + resp = await self._http.post("/v1/data/prepare", json={"data": _b64(data)}) + _check(resp) + j = resp.json() + payments = [ + PaymentInfo( + quote_hash=p["quote_hash"], + rewards_address=p["rewards_address"], + amount=p["amount"], + ) + for p in j.get("payments", []) + ] + return PrepareUploadResult( + upload_id=j["upload_id"], + payments=payments, + total_amount=j.get("total_amount", ""), + data_payments_address=j.get("data_payments_address", ""), + payment_token_address=j.get("payment_token_address", ""), + rpc_url=j.get("rpc_url", ""), + ) + async def finalize_upload(self, upload_id: str, tx_hashes: dict[str, str]) -> FinalizeUploadResult: """Finalize an upload after an external signer has submitted payment transactions. diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index 12e5f20..dae2fcb 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -287,6 +287,29 @@ def prepare_upload(path) ) end + # Prepare a data upload for external signing. + # Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + # @param data [String] raw bytes to upload + # @return [PrepareUploadResult] + def prepare_data_upload(data) + j = do_json(:post, "/v1/data/prepare", { data: b64_encode(data) }) + payments = (j["payments"] || []).map do |p| + PaymentInfo.new( + quote_hash: p["quote_hash"], + rewards_address: p["rewards_address"], + amount: p["amount"] + ) + end + PrepareUploadResult.new( + upload_id: j["upload_id"], + payments: payments, + total_amount: j["total_amount"], + data_payments_address: j["data_payments_address"], + payment_token_address: j["payment_token_address"], + rpc_url: j["rpc_url"] + ) + end + # Finalize an upload after an external signer has submitted payment transactions. # @param upload_id [String] the upload ID from prepare_upload # @param tx_hashes [Hash] map of quote_hash to tx_hash diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index 4a7d050..5994bc5 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -599,6 +599,42 @@ impl Client { }) } + /// Prepares a 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`](Self::finalize_upload). + pub async fn prepare_data_upload(&self, data: &[u8]) -> Result { + let (j, _) = self + .do_json( + reqwest::Method::POST, + "/v1/data/prepare", + Some(json!({ "data": Self::b64_encode(data) })), + ) + .await?; + let j = j.unwrap_or_default(); + let payments = j + .get("payments") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .map(|p| PaymentInfo { + quote_hash: Self::str_field(p, "quote_hash"), + rewards_address: Self::str_field(p, "rewards_address"), + amount: Self::str_field(p, "amount"), + }) + .collect() + }) + .unwrap_or_default(); + Ok(PrepareUploadResult { + upload_id: Self::str_field(&j, "upload_id"), + payments, + total_amount: Self::str_field(&j, "total_amount"), + data_payments_address: Self::str_field(&j, "data_payments_address"), + payment_token_address: Self::str_field(&j, "payment_token_address"), + rpc_url: Self::str_field(&j, "rpc_url"), + }) + } + /// Finalizes an upload after an external signer has submitted payment transactions. pub async fn finalize_upload( &self, diff --git a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift index 493638d..09f0df3 100644 --- a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift +++ b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift @@ -39,4 +39,9 @@ public protocol AntdClientProtocol: Sendable { func walletAddress() async throws -> WalletAddress func walletBalance() async throws -> WalletBalance func walletApprove() async throws -> Bool + + // External Signer (Two-Phase Upload) + func prepareUpload(path: String) async throws -> PrepareUploadResult + func prepareDataUpload(_ data: Data) async throws -> PrepareUploadResult + func finalizeUpload(uploadId: String, txHashes: [String: String]) async throws -> FinalizeUploadResult } diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index d101253..8fbe484 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -221,6 +221,14 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { return PrepareUploadResult(uploadId: resp.uploadId, payments: payments, totalAmount: resp.totalAmount, dataPaymentsAddress: resp.dataPaymentsAddress, paymentTokenAddress: resp.paymentTokenAddress, rpcUrl: resp.rpcUrl) } + /// Prepares a data upload for external signing. + /// Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + public func prepareDataUpload(_ data: Data) async throws -> PrepareUploadResult { + let resp: PrepareUploadDTO = try await postJSON("/v1/data/prepare", body: ["data": data.base64EncodedString()]) + let payments = (resp.payments ?? []).map { PaymentInfo(quoteHash: $0.quoteHash, rewardsAddress: $0.rewardsAddress, amount: $0.amount) } + return PrepareUploadResult(uploadId: resp.uploadId, payments: payments, totalAmount: resp.totalAmount, dataPaymentsAddress: resp.dataPaymentsAddress, paymentTokenAddress: resp.paymentTokenAddress, rpcUrl: resp.rpcUrl) + } + /// Finalizes an upload after an external signer has submitted payment transactions. public func finalizeUpload(uploadId: String, txHashes: [String: String]) async throws -> FinalizeUploadResult { let body: [String: Any] = ["upload_id": uploadId, "tx_hashes": txHashes] diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index 0ab5d0b..0602117 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -415,6 +415,16 @@ pub const Client = struct { return resp; } + /// Prepare a data upload for external signing. + /// Takes raw bytes, base64-encodes them, and POSTs to /v1/data/prepare. + /// Returns raw JSON response body that the caller must parse. + pub fn prepareDataUpload(self: *Client, data: []const u8) ![]const u8 { + const req_body = try json_helpers.buildDataBody(self.allocator, data); + defer self.allocator.free(req_body); + const resp = try self.doRequest(.POST, "/v1/data/prepare", req_body) orelse return error.JsonError; + return resp; + } + /// Finalize an upload after an external signer has submitted payment transactions. /// Returns raw JSON response body that the caller must parse. pub fn finalizeUpload(self: *Client, upload_id: []const u8, tx_hashes_json: []const u8) ![]const u8 { diff --git a/antd/Cargo.lock b/antd/Cargo.lock index 54ab4e1..da5f791 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "ant-core" version = "0.1.1" -source = "git+https://github.com/WithAutonomi/ant-client#b9c08d612a6d0f72c8a4466c1d6233028ca16e15" +source = "git+https://github.com/WithAutonomi/ant-client#7bcb6cbca9efb052843b22d912259acf26f47d13" dependencies = [ "ant-evm", "ant-node", diff --git a/antd/README.md b/antd/README.md index e9661d4..9f94548 100644 --- a/antd/README.md +++ b/antd/README.md @@ -95,6 +95,7 @@ On startup, antd writes a `daemon.port` file containing the actual REST port, gR | `POST` | `/v1/dirs/download/public` | Download a directory | | `POST` | `/v1/cost/file` | Estimate file upload cost | | **External Signer** | | | +| `POST` | `/v1/data/prepare` | Prepare data upload for external signing | | `POST` | `/v1/upload/prepare` | Prepare file upload for external signing | | `POST` | `/v1/upload/finalize` | Finalize upload with external tx hashes | | **Wallet** | | | diff --git a/antd/src/rest/mod.rs b/antd/src/rest/mod.rs index b872616..4bd1a42 100644 --- a/antd/src/rest/mod.rs +++ b/antd/src/rest/mod.rs @@ -47,6 +47,7 @@ pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router .route("/v1/cost/file", post(files::file_cost)) // External signer (two-phase upload) .route("/v1/upload/prepare", post(upload::prepare_upload)) + .route("/v1/data/prepare", post(upload::prepare_data_upload)) .route("/v1/upload/finalize", post(upload::finalize_upload)) // Wallet .route("/v1/wallet/address", get(wallet::wallet_address)) diff --git a/antd/src/rest/upload.rs b/antd/src/rest/upload.rs index e478319..9d985a6 100644 --- a/antd/src/rest/upload.rs +++ b/antd/src/rest/upload.rs @@ -65,6 +65,56 @@ pub async fn prepare_upload( })) } +/// Phase 1 (data): Prepare an in-memory data upload for external signing. +/// +/// Same as prepare_upload but takes base64-encoded data instead of a file path. +pub async fn prepare_data_upload( + State(state): State>, + Json(req): Json, +) -> Result, AntdError> { + use base64::Engine; + use base64::engine::general_purpose::STANDARD as BASE64; + use bytes::Bytes; + + let data = BASE64.decode(&req.data) + .map_err(|e| AntdError::BadRequest(format!("invalid base64: {e}")))?; + + let client = state.client.clone(); + let prepared = tokio::spawn(async move { + client.data_prepare_upload(Bytes::from(data)).await + .map_err(AntdError::from_core) + }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; + + let payments: Vec = prepared.payment_intent.payments.iter().map(|(quote_hash, rewards_addr, amount)| { + PaymentEntry { + quote_hash: format!("{:#x}", quote_hash), + rewards_address: format!("{:#x}", rewards_addr), + amount: amount.to_string(), + } + }).collect(); + + let total_amount = prepared.payment_intent.total_amount.to_string(); + + let upload_id = hex::encode(rand::random::<[u8; 16]>()); + state.pending_uploads.lock().await.insert(upload_id.clone(), prepared); + + let rpc_url = std::env::var("EVM_RPC_URL") + .unwrap_or_else(|_| "http://127.0.0.1:8545".to_string()); + let payment_token_address = std::env::var("EVM_PAYMENT_TOKEN_ADDRESS") + .unwrap_or_default(); + let data_payments_address = std::env::var("EVM_DATA_PAYMENTS_ADDRESS") + .unwrap_or_default(); + + Ok(Json(PrepareUploadResponse { + upload_id, + payments, + total_amount, + data_payments_address, + payment_token_address, + rpc_url, + })) +} + /// Phase 2: Finalize an upload after external payment. /// /// Takes the upload_id from prepare and a map of quote_hash → tx_hash @@ -98,17 +148,31 @@ pub async fn finalize_upload( }) .collect::>()?; + let store_on_network = req.store_data_map; let client = state.client.clone(); - let (address, chunks_stored) = tokio::spawn(async move { + let (data_map_hex, address, chunks_stored) = tokio::spawn(async move { let result = client.finalize_upload(prepared, &tx_hash_map).await .map_err(AntdError::from_core)?; - let address = client.data_map_store(&result.data_map).await - .map_err(AntdError::from_core)?; - Ok::<_, AntdError>((address, result.chunks_stored)) + + let data_map_bytes = rmp_serde::to_vec(&result.data_map) + .map_err(|e| AntdError::Internal(format!("serialize data map: {e}")))?; + let data_map_hex = hex::encode(data_map_bytes); + + // Optionally store the DataMap on-network (requires a wallet). + let address = if store_on_network { + let addr = client.data_map_store(&result.data_map).await + .map_err(AntdError::from_core)?; + Some(hex::encode(addr)) + } else { + None + }; + + Ok::<_, AntdError>((data_map_hex, address, result.chunks_stored)) }).await.map_err(|e| AntdError::Internal(format!("task failed: {e}")))??; Ok(Json(FinalizeUploadResponse { - address: hex::encode(address), + data_map: data_map_hex, + address, chunks_stored: chunks_stored as u64, })) } diff --git a/antd/src/types.rs b/antd/src/types.rs index f951eb3..3ea2a25 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -99,6 +99,11 @@ pub struct PrepareUploadRequest { pub path: String, } +#[derive(Deserialize)] +pub struct PrepareDataUploadRequest { + pub data: String, // base64 +} + #[derive(Serialize)] pub struct PrepareUploadResponse { /// Opaque token to pass back to finalize (hex-encoded serialized state). @@ -131,13 +136,20 @@ pub struct FinalizeUploadRequest { pub upload_id: String, /// Map of quote_hash (hex) → tx_hash (hex) from on-chain payment. pub tx_hashes: std::collections::HashMap, + /// If true, store the DataMap on-network and return its address. + /// If false (default), return the raw DataMap for caller-side storage. + #[serde(default)] + pub store_data_map: bool, } #[derive(Serialize)] pub struct FinalizeUploadResponse { - /// Hex-encoded address of the stored data map. - pub address: String, - /// Number of chunks stored. + /// Hex-encoded serialized DataMap. Always returned. + pub data_map: String, + /// Network address of the stored DataMap (only set when store_data_map=true). + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + /// Number of chunks stored on the network. pub chunks_stored: u64, } diff --git a/llms-full.txt b/llms-full.txt index 10cb8bc..7f27423 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -373,6 +373,25 @@ Request: `{}` (empty body) Response: `{"approved": true}` Returns 400 if no wallet is configured. +#### `POST /v1/data/prepare` +Prepare a data upload for external signing (two-phase upload). Instead of antd paying +from its built-in wallet, this returns payment details for an external signer. + +Request: `{"data": "base64-encoded-bytes"}` +Response: +```json +{ + "upload_id": "hex-string", + "payments": [ + { "quote_hash": "0x...", "rewards_address": "0x...", "amount": "123" } + ], + "total_amount": "456", + "data_payments_address": "0x...", + "payment_token_address": "0x...", + "rpc_url": "http://..." +} +``` + #### `POST /v1/upload/prepare` Prepare a file upload for external signing (two-phase upload). Instead of antd paying from its built-in wallet, this returns payment details for an external signer. diff --git a/llms.txt b/llms.txt index b7840ac..990280b 100644 --- a/llms.txt +++ b/llms.txt @@ -63,7 +63,8 @@ data = client.data_get_public(result.address) | GET | `/v1/wallet/address` | Get wallet public address | | GET | `/v1/wallet/balance` | Get wallet token and gas balances | | POST | `/v1/wallet/approve` | Approve wallet to spend tokens on payment contracts | -| POST | `/v1/upload/prepare` | Prepare upload for external signer (two-phase upload) | +| POST | `/v1/data/prepare` | Prepare data upload for external signer (two-phase upload) | +| POST | `/v1/upload/prepare` | Prepare file upload for external signer (two-phase upload) | | POST | `/v1/upload/finalize` | Finalize externally-signed upload | ## gRPC Services From 32b3c50ca07110e5f62722f85649452322a12b0b Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Mon, 30 Mar 2026 17:00:41 +0100 Subject: [PATCH 19/20] Add external signer support to FFI bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FFI now exposes the two-phase upload flow for Android/iOS apps that manage their own wallet keys: - prepare_data_upload(data) — encrypt + collect quotes, returns PaymentEntry list with quote hashes, amounts, addresses - prepare_file_upload(path) — same for files on disk - finalize_upload(upload_id, tx_hashes) — build proofs + store chunks PreparedUpload state held in-process via Mutex since it contains non-serializable P2P types (same pattern as antd). New types: PaymentEntry, PrepareUploadResult, FinalizeUploadResult. Updated ant-core to 7bcb6cbc. Added rand dependency for upload IDs. Co-Authored-By: Claude Opus 4.6 (1M context) --- ffi/rust/Cargo.lock | 3 +- ffi/rust/ant-ffi/Cargo.toml | 1 + ffi/rust/ant-ffi/src/client.rs | 108 +++++++++++++++++++++++++++++++-- ffi/rust/ant-ffi/src/lib.rs | 29 +++++++++ 4 files changed, 136 insertions(+), 5 deletions(-) diff --git a/ffi/rust/Cargo.lock b/ffi/rust/Cargo.lock index 1dc3da5..528bd53 100644 --- a/ffi/rust/Cargo.lock +++ b/ffi/rust/Cargo.lock @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "ant-core" version = "0.1.1" -source = "git+https://github.com/WithAutonomi/ant-client#c63f546af44ec4e1d749c5f5d93945eb6378d5be" +source = "git+https://github.com/WithAutonomi/ant-client#7bcb6cbca9efb052843b22d912259acf26f47d13" dependencies = [ "ant-evm", "ant-node", @@ -900,6 +900,7 @@ dependencies = [ "bytes", "evmlib", "hex", + "rand 0.8.5", "rmp-serde", "thiserror 2.0.18", "tokio", diff --git a/ffi/rust/ant-ffi/Cargo.toml b/ffi/rust/ant-ffi/Cargo.toml index 45e4a33..efcf590 100644 --- a/ffi/rust/ant-ffi/Cargo.toml +++ b/ffi/rust/ant-ffi/Cargo.toml @@ -11,6 +11,7 @@ ant-core = { git = "https://github.com/WithAutonomi/ant-client" } evmlib = "0.4.9" bytes = "1" hex = "0.4" +rand = "0.8" rmp-serde = "1" thiserror = "2" tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/ffi/rust/ant-ffi/src/client.rs b/ffi/rust/ant-ffi/src/client.rs index 8d34e6c..36be63f 100644 --- a/ffi/rust/ant-ffi/src/client.rs +++ b/ffi/rust/ant-ffi/src/client.rs @@ -1,16 +1,20 @@ +use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use bytes::Bytes; +use tokio::sync::Mutex; use ant_core::data::{ Client as CoreClient, ClientConfig, CoreNodeConfig, MultiAddr, NodeMode, P2PNode, + PreparedUpload, finalize_batch_payment, }; use crate::data::{format_payment_mode, parse_payment_mode}; use crate::wallet::Wallet; use crate::{ - ChunkPutResult, ClientError, DataPutPrivateResult, DataPutPublicResult, FilePutPublicResult, + ChunkPutResult, ClientError, DataPutPrivateResult, DataPutPublicResult, + FinalizeUploadResult, FilePutPublicResult, PaymentEntry, PrepareUploadResult, }; /// Autonomi network client (wraps ant-core Client). @@ -20,6 +24,8 @@ use crate::{ #[derive(uniffi::Object)] pub struct Client { inner: CoreClient, + /// Pending prepared uploads (external signer flow). + pending_uploads: Mutex>, } #[uniffi::export(async_runtime = "tokio")] @@ -54,7 +60,7 @@ impl Client { let client = CoreClient::from_node(Arc::new(node), ClientConfig::default()); - Ok(Arc::new(Self { inner: client })) + Ok(Arc::new(Self { inner: client, pending_uploads: Mutex::new(HashMap::new()) })) } /// Connect to the network using explicit bootstrap peers. @@ -101,7 +107,7 @@ impl Client { let client = CoreClient::from_node(Arc::new(node), ClientConfig::default()); - Ok(Arc::new(Self { inner: client })) + Ok(Arc::new(Self { inner: client, pending_uploads: Mutex::new(HashMap::new()) })) } /// Connect to the network with a wallet configured for write operations. @@ -168,7 +174,7 @@ impl Client { let client = CoreClient::from_node(Arc::new(node), ClientConfig::default()).with_wallet(wallet); - Ok(Arc::new(Self { inner: client })) + Ok(Arc::new(Self { inner: client, pending_uploads: Mutex::new(HashMap::new()) })) } // ===== Chunk Operations ===== @@ -319,6 +325,100 @@ impl Client { Ok(()) } + // ===== External Signer Operations ===== + + /// Prepare a data upload for external signing. + /// Encrypts data, collects quotes, returns payment details. + /// Call finalize_upload() with tx hashes after signing externally. + pub async fn prepare_data_upload( + &self, + data: Vec, + ) -> Result { + let prepared = self.inner.data_prepare_upload(Bytes::from(data)).await?; + + let payments: Vec = prepared.payment_intent.payments.iter().map(|(qh, ra, amt)| { + PaymentEntry { + quote_hash: format!("{:#x}", qh), + rewards_address: format!("{:#x}", ra), + amount: amt.to_string(), + } + }).collect(); + + let total_amount = prepared.payment_intent.total_amount.to_string(); + + let data_map_bytes = rmp_serde::to_vec(&prepared.data_map).map_err(|e| { + ClientError::InternalError { reason: format!("serialize data map: {e}") } + })?; + let data_map = hex::encode(data_map_bytes); + + let upload_id = hex::encode(rand::random::<[u8; 16]>()); + self.pending_uploads.lock().await.insert(upload_id.clone(), prepared); + + Ok(PrepareUploadResult { payments, total_amount, data_map }) + } + + /// Prepare a file upload for external signing. + pub async fn prepare_file_upload( + &self, + path: String, + ) -> Result { + let file_path = PathBuf::from(&path); + let prepared = self.inner.file_prepare_upload(&file_path).await?; + + let payments: Vec = prepared.payment_intent.payments.iter().map(|(qh, ra, amt)| { + PaymentEntry { + quote_hash: format!("{:#x}", qh), + rewards_address: format!("{:#x}", ra), + amount: amt.to_string(), + } + }).collect(); + + let total_amount = prepared.payment_intent.total_amount.to_string(); + + let data_map_bytes = rmp_serde::to_vec(&prepared.data_map).map_err(|e| { + ClientError::InternalError { reason: format!("serialize data map: {e}") } + })?; + let data_map = hex::encode(data_map_bytes); + + let upload_id = hex::encode(rand::random::<[u8; 16]>()); + self.pending_uploads.lock().await.insert(upload_id.clone(), prepared); + + Ok(PrepareUploadResult { payments, total_amount, data_map }) + } + + /// Finalize an upload after external payment. + /// Takes a map of quote_hash (hex) → tx_hash (hex). + pub async fn finalize_upload( + &self, + upload_id: String, + tx_hashes: HashMap, + ) -> Result { + let prepared = self.pending_uploads.lock().await + .remove(&upload_id) + .ok_or_else(|| ClientError::NotFound { + reason: format!("upload_id {upload_id} not found"), + })?; + + let tx_hash_map: HashMap = + tx_hashes.iter().map(|(qh, th)| { + let q: [u8; 32] = hex::decode(qh.trim_start_matches("0x")) + .map_err(|e| ClientError::InvalidInput { reason: format!("invalid quote_hash: {e}") })? + .try_into() + .map_err(|_| ClientError::InvalidInput { reason: "quote_hash must be 32 bytes".into() })?; + let t: [u8; 32] = hex::decode(th.trim_start_matches("0x")) + .map_err(|e| ClientError::InvalidInput { reason: format!("invalid tx_hash: {e}") })? + .try_into() + .map_err(|_| ClientError::InvalidInput { reason: "tx_hash must be 32 bytes".into() })?; + Ok((q.into(), t.into())) + }).collect::>()?; + + let result = self.inner.finalize_upload(prepared, &tx_hash_map).await?; + + Ok(FinalizeUploadResult { + chunks_stored: result.chunks_stored as u64, + }) + } + // ===== Wallet Operations ===== /// Approve token spend for storage payments (one-time). diff --git a/ffi/rust/ant-ffi/src/lib.rs b/ffi/rust/ant-ffi/src/lib.rs index 21b25de..3bef83d 100644 --- a/ffi/rust/ant-ffi/src/lib.rs +++ b/ffi/rust/ant-ffi/src/lib.rs @@ -46,6 +46,35 @@ pub struct FilePutPublicResult { pub address: String, } +/// Payment entry for external signing. +#[derive(uniffi::Record)] +pub struct PaymentEntry { + /// Quote hash (hex, 32 bytes). + pub quote_hash: String, + /// Rewards address (hex with 0x prefix). + pub rewards_address: String, + /// Amount to pay (atto tokens as decimal string). + pub amount: String, +} + +/// Result of preparing an upload for external signing. +#[derive(uniffi::Record)] +pub struct PrepareUploadResult { + /// Payment entries to sign externally. + pub payments: Vec, + /// Total amount across all payments (atto tokens). + pub total_amount: String, + /// Hex-encoded serialized DataMap for later retrieval. + pub data_map: String, +} + +/// Result of finalizing an externally-signed upload. +#[derive(uniffi::Record)] +pub struct FinalizeUploadResult { + /// Number of chunks stored on the network. + pub chunks_stored: u64, +} + // ===== Error types ===== /// Error type for client operations. From e923ff405fca818014dbb3e65bf363d9ad460f2e Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 31 Mar 2026 09:57:15 +0100 Subject: [PATCH 20/20] Remove graph entries from SDK (out of scope for launch) Graph entries (DAG nodes) are being removed from the initial launch scope. This removes all graph functionality across the entire SDK: - antd: REST routes, gRPC service, handler, types, proto file, build.rs - All 15 SDK clients: REST + gRPC methods, model types - MCP server: 4 graph tools removed - Tests: graph test cases removed across Go, Python, Rust, Java, C#, Dart, Ruby, Lua, PHP - Examples: graph example files deleted (Python, TypeScript, Java, C#, Dart, Kotlin, Lua) - Docs: graph removed from llms.txt, llms-full.txt, skill.md, architecture.md, all quickstart guides, data primitives table - Scripts: graph test steps removed from test-api.sh/.ps1 - Proto: graph.proto and generated Go bindings deleted Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 3 +- antd-cpp/README.md | 12 +- antd-cpp/include/antd/client.hpp | 17 - antd-cpp/include/antd/grpc_client.hpp | 19 +- antd-cpp/include/antd/models.hpp | 14 - antd-cpp/src/client.cpp | 72 --- antd-csharp/Antd.Sdk.Tests/TestRunner.cs | 62 +-- antd-csharp/Antd.Sdk/AntdGrpcClient.cs | 60 --- antd-csharp/Antd.Sdk/AntdRestClient.cs | 40 -- antd-csharp/Antd.Sdk/IAntdClient.cs | 6 - antd-csharp/Antd.Sdk/Models.cs | 6 - antd-csharp/Examples/Program.cs | 39 -- antd-csharp/README.md | 12 - antd-dart/README.md | 11 +- antd-dart/example/05_graph.dart | 38 -- antd-dart/lib/src/client.dart | 44 -- antd-dart/lib/src/grpc_client.dart | 90 +--- antd-dart/lib/src/models.dart | 69 --- antd-dart/test/client_test.dart | 51 -- antd-dart/test/grpc_client_test.dart | 70 +-- antd-elixir/README.md | 14 +- antd-elixir/lib/antd.ex | 8 - antd-elixir/lib/antd/client.ex | 92 ---- antd-elixir/lib/antd/grpc_client.ex | 100 +--- antd-elixir/lib/antd/models.ex | 26 - antd-go/README.md | 10 +- antd-go/client.go | 66 --- antd-go/client_test.go | 44 -- antd-go/grpc_client.go | 84 +-- antd-go/grpc_client_test.go | 81 +-- antd-go/models.go | 14 - antd-go/proto/antd/v1/graph.pb.go | 488 ------------------ antd-go/proto/antd/v1/graph_grpc.pb.go | 235 --------- antd-java/README.md | 14 +- antd-java/examples/Example04GraphEntries.java | 38 -- .../java/com/autonomi/antd/AntdClient.java | 41 -- .../com/autonomi/antd/AsyncAntdClient.java | 49 -- .../com/autonomi/antd/GrpcAntdClient.java | 95 +--- .../autonomi/antd/models/GraphDescendant.java | 9 - .../com/autonomi/antd/models/GraphEntry.java | 17 - .../com/autonomi/antd/AntdClientTest.java | 43 -- .../com/autonomi/antd/GrpcAntdClientTest.java | 99 +--- antd-js/README.md | 12 - antd-js/examples/05-graph.ts | 37 -- antd-js/src/index.ts | 2 - antd-js/src/models.ts | 14 - antd-js/src/rest-client.ts | 51 -- antd-kotlin/README.md | 1 - .../main/kotlin/com/autonomi/examples/Main.kt | 39 -- .../kotlin/com/autonomi/sdk/AntdGrpcClient.kt | 38 -- .../kotlin/com/autonomi/sdk/AntdRestClient.kt | 39 -- .../kotlin/com/autonomi/sdk/IAntdClient.kt | 6 - .../main/kotlin/com/autonomi/sdk/Models.kt | 20 - .../kotlin/com/autonomi/sdk/SmokeTests.kt | 5 - antd-lua/README.md | 11 - antd-lua/antd-scm-1.rockspec | 2 +- antd-lua/spec/client_spec.lua | 54 +- antd-lua/src/antd/client.lua | 75 --- antd-lua/src/antd/init.lua | 2 - antd-lua/src/antd/models.lua | 26 - antd-mcp/README.md | 23 +- antd-mcp/src/antd_mcp/server.py | 144 +----- antd-php/README.md | 9 - antd-php/src/AntdClient.php | 173 ------- antd-php/tests/AntdClientTest.php | 60 --- antd-py/README.md | 12 - antd-py/examples/05_graph.py | 43 -- antd-py/src/antd/__init__.py | 4 - antd-py/src/antd/_grpc.py | 106 ---- antd-py/src/antd/_rest.py | 78 --- antd-py/src/antd/models.py | 16 - antd-ruby/README.md | 13 +- antd-ruby/lib/antd/client.rb | 54 -- antd-ruby/lib/antd/grpc_client.rb | 63 +-- antd-ruby/lib/antd/models.rb | 6 - antd-ruby/test/test_client.rb | 51 -- antd-ruby/test/test_grpc_client.rb | 70 --- antd-rust/README.md | 11 +- antd-rust/src/client.rs | 100 ---- antd-rust/src/grpc_client.rs | 102 +--- antd-rust/src/grpc_tests.rs | 89 +--- antd-rust/src/models.rs | 18 - antd-rust/src/tests.rs | 87 +--- antd-swift/README.md | 1 - .../Sources/AntdSdk/AntdClientProtocol.swift | 6 - .../Sources/AntdSdk/AntdGrpcClient.swift | 4 - .../Sources/AntdSdk/AntdRestClient.swift | 40 -- antd-swift/Sources/AntdSdk/Models.swift | 26 - antd-zig/README.md | 15 +- antd-zig/src/antd.zig | 55 -- antd-zig/src/models.zig | 32 -- antd/README.md | 8 - antd/build.rs | 1 - antd/openapi.yaml | 156 ------ antd/proto/antd/v1/graph.proto | 49 -- antd/src/grpc/mod.rs | 3 - antd/src/grpc/service.rs | 22 - antd/src/rest/graph.rs | 41 -- antd/src/rest/mod.rs | 8 +- antd/src/types.rs | 35 -- docs/architecture.md | 37 +- docs/missing-features.md | 7 +- docs/quickstart-cpp.md | 39 -- docs/quickstart-csharp.md | 27 - docs/quickstart-dart.md | 33 -- docs/quickstart-elixir.md | 24 - docs/quickstart-java.md | 39 -- docs/quickstart-kotlin.md | 25 - docs/quickstart-lua.md | 35 -- docs/quickstart-php.md | 25 - docs/quickstart-python.md | 28 - docs/quickstart-ruby.md | 27 - docs/quickstart-rust.md | 30 -- docs/quickstart-swift.md | 28 - docs/quickstart-zig.md | 34 -- ffi/README.md | 2 - llms-full.txt | 144 +----- llms.txt | 5 - scripts/test-api.ps1 | 21 +- scripts/test-api.sh | 21 +- skill.md | 19 +- 121 files changed, 85 insertions(+), 5235 deletions(-) delete mode 100644 antd-dart/example/05_graph.dart delete mode 100644 antd-go/proto/antd/v1/graph.pb.go delete mode 100644 antd-go/proto/antd/v1/graph_grpc.pb.go delete mode 100644 antd-java/examples/Example04GraphEntries.java delete mode 100644 antd-java/src/main/java/com/autonomi/antd/models/GraphDescendant.java delete mode 100644 antd-java/src/main/java/com/autonomi/antd/models/GraphEntry.java delete mode 100644 antd-js/examples/05-graph.ts delete mode 100644 antd-py/examples/05_graph.py delete mode 100644 antd/proto/antd/v1/graph.proto delete mode 100644 antd/src/rest/graph.rs diff --git a/README.md b/README.md index 7eca23a..3b0a70d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ant-sdk -A developer-friendly SDK for the [Autonomi](https://autonomi.com) decentralized network. Store data, build DAGs, and more — from Go, JavaScript/TypeScript, Python, C#, Kotlin, Swift, Ruby, PHP, Dart, Lua, Elixir, Zig, Rust, C++, Java, or AI agents. +A developer-friendly SDK for the [Autonomi](https://autonomi.com) decentralized network. Store data permanently and more — from Go, JavaScript/TypeScript, Python, C#, Kotlin, Swift, Ruby, PHP, Dart, Lua, Elixir, Zig, Rust, C++, Java, or AI agents. ## Architecture @@ -421,7 +421,6 @@ The Autonomi network provides these core primitives, all accessible through the |-----------|-------------| | **Data** | Store/retrieve arbitrary byte blobs (public or private/encrypted) | | **Chunks** | Low-level content-addressed storage | -| **Graph Entries** | Append-only DAG nodes with parent/descendant links | | **Files** | File/directory upload with archive manifests | ## Developer CLI Reference diff --git a/antd-cpp/README.md b/antd-cpp/README.md index 2c8c51e..b12dcec 100644 --- a/antd-cpp/README.md +++ b/antd-cpp/README.md @@ -131,7 +131,7 @@ for (auto& f : futures) { ## gRPC Transport -The SDK includes a `GrpcClient` class that provides the same 19 methods as the +The SDK includes a `GrpcClient` class that provides the same methods as the REST `Client`, but communicates over gRPC. This can offer lower latency and better streaming support for large data transfers. @@ -234,15 +234,6 @@ All methods throw `antd::AntdError` (or a subclass) on failure. | `chunk_put(data)` | Store a raw chunk | | `chunk_get(address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) - -| Method | Description | -|--------|-------------| -| `graph_entry_put(secret_key, parents, content, descendants)` | Create entry | -| `graph_entry_get(address)` | Read entry | -| `graph_entry_exists(address)` | Check if exists | -| `graph_entry_cost(public_key)` | Estimate creation cost | - ### Files & Directories | Method | Description | @@ -303,5 +294,4 @@ See the [examples/](examples/) directory: - `02-data` — Public data storage and retrieval - `03-chunks` — Raw chunk operations - `04-files` — File and directory upload/download -- `05-graph` — Graph entry (DAG node) operations - `06-private-data` — Private encrypted data storage diff --git a/antd-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index 8105415..b8ae7fa 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -74,23 +74,6 @@ class Client { /// Retrieve a chunk by address. std::vector chunk_get(std::string_view address); - // --- Graph Entries (DAG Nodes) --- - - /// Create a new graph entry. - PutResult graph_entry_put(std::string_view owner_secret_key, - const std::vector& parents, - std::string_view content, - const std::vector& descendants); - - /// Retrieve a graph entry by address. - GraphEntry graph_entry_get(std::string_view address); - - /// Check if a graph entry exists at the given address. - bool graph_entry_exists(std::string_view address); - - /// Estimate the cost of creating a graph entry. - std::string graph_entry_cost(std::string_view public_key); - // --- Files & Directories --- /// Upload a local file to the network. diff --git a/antd-cpp/include/antd/grpc_client.hpp b/antd-cpp/include/antd/grpc_client.hpp index 98194a0..fdcc13c 100644 --- a/antd-cpp/include/antd/grpc_client.hpp +++ b/antd-cpp/include/antd/grpc_client.hpp @@ -17,7 +17,7 @@ inline constexpr const char* kDefaultGrpcTarget = "localhost:50051"; /// gRPC client for the antd daemon. /// -/// Provides the same 19 methods as the REST `Client`, but communicates over +/// Provides the same methods as the REST `Client`, but communicates over /// gRPC using the proto-generated stubs from `antd/v1/*.proto`. /// /// All methods throw antd::AntdError (or a subclass) on failure. @@ -77,23 +77,6 @@ class GrpcClient { /// Retrieve a chunk by address. std::vector chunk_get(std::string_view address); - // --- Graph Entries (DAG Nodes) --- - - /// Create a new graph entry. - PutResult graph_entry_put(std::string_view owner_secret_key, - const std::vector& parents, - std::string_view content, - const std::vector& descendants); - - /// Retrieve a graph entry by address. - GraphEntry graph_entry_get(std::string_view address); - - /// Check if a graph entry exists at the given address. - bool graph_entry_exists(std::string_view address); - - /// Estimate the cost of creating a graph entry. - std::string graph_entry_cost(std::string_view public_key); - // --- Files & Directories --- /// Upload a local file to the network. diff --git a/antd-cpp/include/antd/models.hpp b/antd-cpp/include/antd/models.hpp index 618f3e5..948783b 100644 --- a/antd-cpp/include/antd/models.hpp +++ b/antd-cpp/include/antd/models.hpp @@ -18,20 +18,6 @@ struct PutResult { std::string address; // hex }; -/// A descendant entry in a graph node. -struct GraphDescendant { - std::string public_key; // hex - std::string content; // hex, 32 bytes -}; - -/// A DAG node from the network. -struct GraphEntry { - std::string owner; - std::vector parents; - std::string content; - std::vector descendants; -}; - /// A single entry in a file archive. struct ArchiveEntry { std::string path; diff --git a/antd-cpp/src/client.cpp b/antd-cpp/src/client.cpp index 036cd84..3bd3510 100644 --- a/antd-cpp/src/client.cpp +++ b/antd-cpp/src/client.cpp @@ -168,78 +168,6 @@ std::vector Client::chunk_get(std::string_view address) { return detail::base64_decode(j.value("data", "")); } -// --------------------------------------------------------------------------- -// Graph Entries (DAG Nodes) -// --------------------------------------------------------------------------- - -PutResult Client::graph_entry_put(std::string_view owner_secret_key, - const std::vector& parents, - std::string_view content, - const std::vector& descendants) { - json descs = json::array(); - for (const auto& d : descendants) { - descs.push_back(json{{"public_key", d.public_key}, {"content", d.content}}); - } - - auto j = impl_->do_json("POST", "/v1/graph", json{ - {"owner_secret_key", std::string(owner_secret_key)}, - {"parents", parents}, - {"content", std::string(content)}, - {"descendants", descs}, - }); - return PutResult{ - .cost = j.value("cost", ""), - .address = j.value("address", ""), - }; -} - -GraphEntry Client::graph_entry_get(std::string_view address) { - auto j = impl_->do_json("GET", "/v1/graph/" + std::string(address)); - - GraphEntry entry; - entry.owner = j.value("owner", ""); - entry.content = j.value("content", ""); - - if (j.contains("parents") && j["parents"].is_array()) { - for (const auto& p : j["parents"]) { - if (p.is_string()) { - entry.parents.push_back(p.get()); - } - } - } - - if (j.contains("descendants") && j["descendants"].is_array()) { - for (const auto& d : j["descendants"]) { - if (d.is_object()) { - entry.descendants.push_back(GraphDescendant{ - .public_key = d.value("public_key", ""), - .content = d.value("content", ""), - }); - } - } - } - - return entry; -} - -bool Client::graph_entry_exists(std::string_view address) { - int code = impl_->do_head("/v1/graph/" + std::string(address)); - if (code == 404) { - return false; - } - if (code >= 300) { - error_for_status(code, "graph entry exists check failed"); - } - return true; -} - -std::string Client::graph_entry_cost(std::string_view public_key) { - auto j = impl_->do_json("POST", "/v1/graph/cost", json{ - {"public_key", std::string(public_key)}, - }); - return j.value("cost", ""); -} - // --------------------------------------------------------------------------- // Files & Directories // --------------------------------------------------------------------------- diff --git a/antd-csharp/Antd.Sdk.Tests/TestRunner.cs b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs index d566c9a..6aba129 100644 --- a/antd-csharp/Antd.Sdk.Tests/TestRunner.cs +++ b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs @@ -41,17 +41,10 @@ private static bool EnableAnsi() private const int PropagationDelay = 3; - // Unique keys per transport to avoid DHT collisions between REST and gRPC runs - private readonly string _keyGraph; - public TestRunner(IAntdClient client, string transport) { _client = client; _transport = transport; - - // Offset keys by transport: REST uses 01-04, gRPC uses 11-14 - var offset = transport == "grpc" ? "1" : "0"; - _keyGraph = new string('0', 62) + offset + "3"; } private void Pass(string name, string detail = "") @@ -82,7 +75,6 @@ public async Task RunAllAsync() var dataAddr = await TestDataPublic(); await TestDataCost(); var chunkAddr = await TestChunks(); - await TestGraph(); await TestLargeData(); // Summary @@ -203,59 +195,7 @@ private async Task TestDataCost() return chunkAddr; } - // 5. Graph entry put/exists/get/cost - private async Task TestGraph() - { - string? graphAddr = null; - try - { - var contentHex = string.Concat(Enumerable.Repeat("ab", 32)); // "abab...ab" 64 hex chars = 32 bytes - var result = await _client.GraphEntryPutAsync(_keyGraph, [], contentHex, []); - graphAddr = result.Address; - Pass("Graph entry put", $"addr={result.Address[..16]}... cost={result.Cost}"); - } - catch (AlreadyExistsException) - { - Pass("Graph entry put", "already exists (expected on re-run)"); - } - catch (Exception ex) { Fail("Graph entry put", ex.Message); } - - if (graphAddr != null) - { - Console.WriteLine($" ... waiting {PropagationDelay}s for DHT propagation"); - await Task.Delay(PropagationDelay * 1000); - - try - { - var exists = await _client.GraphEntryExistsAsync(graphAddr); - if (exists) Pass("Graph entry exists"); - else Fail("Graph entry exists", "returned false"); - } - catch (Exception ex) { Fail("Graph entry exists", ex.Message); } - - try - { - var entry = await _client.GraphEntryGetAsync(graphAddr); - Pass("Graph entry get", $"owner={entry.Owner[..16]}... content={entry.Content[..16]}..."); - } - catch (Exception ex) { Fail("Graph entry get", ex.Message); } - - try - { - var cost = await _client.GraphEntryCostAsync(graphAddr); - Pass("Graph entry cost", $"cost={cost}"); - } - catch (Exception ex) { Fail("Graph entry cost", ex.Message); } - } - else - { - Skip("Graph entry exists", "no graph address"); - Skip("Graph entry get", "no graph address"); - Skip("Graph entry cost", "no graph address"); - } - } - - // 6. Large data round-trip (10 KB) + // 5. Large data round-trip (10 KB) private async Task TestLargeData() { try diff --git a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs index 1f44f71..2b3cbca 100644 --- a/antd-csharp/Antd.Sdk/AntdGrpcClient.cs +++ b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs @@ -11,7 +11,6 @@ public sealed class AntdGrpcClient : IAntdClient private readonly HealthService.HealthServiceClient _health; private readonly DataService.DataServiceClient _data; private readonly ChunkService.ChunkServiceClient _chunks; - private readonly GraphService.GraphServiceClient _graph; private readonly FileService.FileServiceClient _files; public AntdGrpcClient(string target = "http://localhost:50051") @@ -20,7 +19,6 @@ public AntdGrpcClient(string target = "http://localhost:50051") _health = new HealthService.HealthServiceClient(_channel); _data = new DataService.DataServiceClient(_channel); _chunks = new ChunkService.ChunkServiceClient(_channel); - _graph = new GraphService.GraphServiceClient(_channel); _files = new FileService.FileServiceClient(_channel); } @@ -135,64 +133,6 @@ public async Task ChunkGetAsync(string address) catch (RpcException ex) { throw Wrap(ex); } } - // ── Graph ── - - public async Task GraphEntryPutAsync(string ownerSecretKey, List parents, string content, List descendants) - { - try - { - var req = new PutGraphEntryRequest - { - OwnerSecretKey = ownerSecretKey, - Content = content, - }; - req.Parents.AddRange(parents); - req.Descendants.AddRange(descendants.Select(d => new Antd.V1.GraphDescendant - { - PublicKey = d.PublicKey, - Content = d.Content, - })); - var resp = await _graph.PutAsync(req); - return new PutResult(resp.Cost.AttoTokens, resp.Address); - } - catch (RpcException ex) { throw Wrap(ex); } - } - - public async Task GraphEntryGetAsync(string address) - { - try - { - var resp = await _graph.GetAsync(new GetGraphEntryRequest { Address = address }); - var descendants = resp.Descendants.Select(d => new GraphDescendant(d.PublicKey, d.Content)).ToList(); - return new GraphEntry(resp.Owner, resp.Parents.ToList(), resp.Content, descendants); - } - catch (RpcException ex) { throw Wrap(ex); } - } - - public async Task GraphEntryExistsAsync(string address) - { - try - { - var resp = await _graph.CheckExistenceAsync(new CheckGraphEntryRequest { Address = address }); - return resp.Exists; - } - catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) - { - return false; - } - catch (RpcException ex) { throw Wrap(ex); } - } - - public async Task GraphEntryCostAsync(string publicKey) - { - try - { - var resp = await _graph.GetCostAsync(new GraphEntryCostRequest { PublicKey = publicKey }); - return resp.AttoTokens; - } - catch (RpcException ex) { throw Wrap(ex); } - } - // ── Files ── public async Task FileUploadPublicAsync(string path, string? paymentMode = null) diff --git a/antd-csharp/Antd.Sdk/AntdRestClient.cs b/antd-csharp/Antd.Sdk/AntdRestClient.cs index 6fc05cc..2e8e69e 100644 --- a/antd-csharp/Antd.Sdk/AntdRestClient.cs +++ b/antd-csharp/Antd.Sdk/AntdRestClient.cs @@ -141,36 +141,6 @@ public async Task ChunkGetAsync(string address) return Convert.FromBase64String(resp.Data); } - // ── Graph ── - - public async Task GraphEntryPutAsync(string ownerSecretKey, List parents, string content, List descendants) - { - var body = new - { - owner_secret_key = ownerSecretKey, - parents, - content, - descendants = descendants.Select(d => new { public_key = d.PublicKey, content = d.Content }).ToList(), - }; - var resp = await PostJsonAsync("/v1/graph", body); - return new PutResult(resp.Cost, resp.Address); - } - - public async Task GraphEntryGetAsync(string address) - { - var resp = await GetJsonAsync($"/v1/graph/{address}"); - var descendants = resp.Descendants?.Select(d => new GraphDescendant(d.PublicKey, d.Content)).ToList() ?? []; - return new GraphEntry(resp.Owner, resp.Parents ?? [], resp.Content, descendants); - } - - public Task GraphEntryExistsAsync(string address) => HeadExistsAsync($"/v1/graph/{address}"); - - public async Task GraphEntryCostAsync(string publicKey) - { - var resp = await PostJsonAsync("/v1/graph/cost", new { public_key = publicKey }); - return resp.Cost; - } - // ── Files ── public async Task FileUploadPublicAsync(string path, string? paymentMode = null) @@ -307,16 +277,6 @@ private sealed record DataGetDto( private sealed record CostDto( [property: JsonPropertyName("cost")] string Cost); - private sealed record GraphDescendantDto( - [property: JsonPropertyName("public_key")] string PublicKey, - [property: JsonPropertyName("content")] string Content); - - private sealed record GraphEntryDto( - [property: JsonPropertyName("owner")] string Owner, - [property: JsonPropertyName("parents")] List? Parents, - [property: JsonPropertyName("content")] string Content, - [property: JsonPropertyName("descendants")] List? Descendants); - private sealed record ArchiveEntryDto( [property: JsonPropertyName("path")] string Path, [property: JsonPropertyName("address")] string Address, diff --git a/antd-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index fccd373..b9f1871 100644 --- a/antd-csharp/Antd.Sdk/IAntdClient.cs +++ b/antd-csharp/Antd.Sdk/IAntdClient.cs @@ -16,12 +16,6 @@ public interface IAntdClient : IDisposable Task ChunkPutAsync(byte[] data); Task ChunkGetAsync(string address); - // Graph - Task GraphEntryPutAsync(string ownerSecretKey, List parents, string content, List descendants); - Task GraphEntryGetAsync(string address); - Task GraphEntryExistsAsync(string address); - Task GraphEntryCostAsync(string publicKey); - // Files Task FileUploadPublicAsync(string path, string? paymentMode = null); Task FileDownloadPublicAsync(string address, string destPath); diff --git a/antd-csharp/Antd.Sdk/Models.cs b/antd-csharp/Antd.Sdk/Models.cs index 17f9380..70c9be7 100644 --- a/antd-csharp/Antd.Sdk/Models.cs +++ b/antd-csharp/Antd.Sdk/Models.cs @@ -6,12 +6,6 @@ public sealed record HealthStatus(bool Ok, string Network); /// Result of a put/create operation that stores data on the network. public sealed record PutResult(string Cost, string Address); -/// A descendant entry in a graph node. -public sealed record GraphDescendant(string PublicKey, string Content); - -/// A graph entry retrieved from the network. -public sealed record GraphEntry(string Owner, List Parents, string Content, List Descendants); - /// A single entry in an archive manifest. public sealed record ArchiveEntry(string Path, string Address, ulong Created, ulong Modified, ulong Size); diff --git a/antd-csharp/Examples/Program.cs b/antd-csharp/Examples/Program.cs index d7f80bd..24e28e1 100644 --- a/antd-csharp/Examples/Program.cs +++ b/antd-csharp/Examples/Program.cs @@ -16,14 +16,12 @@ static async Task Main(string[] args) case "2": await Example02_Data(); break; case "3": await Example03_Chunks(); break; case "4": await Example04_Files(); break; - case "5": await Example05_Graph(); break; case "6": await Example06_PrivateData(); break; case "all": await Example01_Connect(); await Example02_Data(); await Example03_Chunks(); await Example04_Files(); - await Example05_Graph(); await Example06_PrivateData(); break; default: @@ -135,43 +133,6 @@ static async Task Example04_Files() Console.WriteLine("File upload/download OK!\n"); } - /// Example 05: Graph entry (DAG node) operations. - static async Task Example05_Graph() - { - Console.WriteLine("=== Example 05: Graph ==="); - using var client = AntdClient.CreateRest(); - - var secretKey = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLower(); - - // Create a root graph entry - var content = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLower(); - var result = await client.GraphEntryPutAsync( - secretKey, - new List(), - content, - new List() - ); - Console.WriteLine($"Graph entry created at: {result.Address}"); - Console.WriteLine($"Cost: {result.Cost} atto tokens"); - - // Read - var entry = await client.GraphEntryGetAsync(result.Address); - Console.WriteLine($"Owner: {entry.Owner}"); - Console.WriteLine($"Content: {entry.Content}"); - Console.WriteLine($"Parents: {entry.Parents.Count}"); - Console.WriteLine($"Descendants: {entry.Descendants.Count}"); - - // Check existence - var exists = await client.GraphEntryExistsAsync(result.Address); - Console.WriteLine($"Graph entry exists: {exists}"); - - // Estimate cost - var cost = await client.GraphEntryCostAsync(secretKey); - Console.WriteLine($"Cost estimate for new entry: {cost} atto tokens"); - - Console.WriteLine("Graph entry operations OK!\n"); - } - /// Example 06: Private (encrypted) data round-trip. static async Task Example06_PrivateData() { diff --git a/antd-csharp/README.md b/antd-csharp/README.md index a7fabc8..094ff60 100644 --- a/antd-csharp/README.md +++ b/antd-csharp/README.md @@ -88,15 +88,6 @@ All methods are async and return `Task`. The client implements `IDisposable`. | `ChunkPutAsync(byte[] data)` | `PutResult` | Store a raw chunk | | `ChunkGetAsync(string address)` | `byte[]` | Retrieve a chunk | -### Graph - -| Method | Returns | Description | -|--------|---------|-------------| -| `GraphEntryPutAsync(string key, List parents, string content, List desc)` | `PutResult` | Create | -| `GraphEntryGetAsync(string address)` | `GraphEntry` | Read | -| `GraphEntryExistsAsync(string address)` | `bool` | Check existence | -| `GraphEntryCostAsync(string publicKey)` | `string` | Estimate cost | - ### Files | Method | Returns | Description | @@ -117,8 +108,6 @@ All models are sealed records (immutable). |-------|--------|-------------| | `HealthStatus` | `Ok`, `Network` | Health check result | | `PutResult` | `Cost`, `Address` | Write operation result | -| `GraphDescendant` | `PublicKey`, `Content` | Graph descendant entry | -| `GraphEntry` | `Owner`, `Parents`, `Content`, `Descendants` | Graph DAG node | | `ArchiveEntry` | `Path`, `Address`, `Created`, `Modified`, `Size` | Archive file entry | | `Archive` | `Entries` | Archive manifest | @@ -167,7 +156,6 @@ dotnet run -- 1 # Connect dotnet run -- 2 # Public data dotnet run -- 3 # Chunks dotnet run -- 4 # Files -dotnet run -- 5 # Graph dotnet run -- 6 # Private data dotnet run -- all # Run all ``` diff --git a/antd-dart/README.md b/antd-dart/README.md index 20065ab..a329b57 100644 --- a/antd-dart/README.md +++ b/antd-dart/README.md @@ -52,7 +52,7 @@ void main() async { ## gRPC Transport -The SDK includes a `GrpcAntdClient` class that provides the same 19 async +The SDK includes a `GrpcAntdClient` class that provides the same 15 async methods as the REST `AntdClient`, but communicates over gRPC. ### Setup @@ -162,14 +162,6 @@ All methods return `Future` and can throw `AntdError` subclasses. | `chunkPut(data)` | Store a raw chunk | | `chunkGet(address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) -| Method | Description | -|--------|-------------| -| `graphEntryPut(secretKey, parents, content, descendants)` | Create entry | -| `graphEntryGet(address)` | Read entry | -| `graphEntryExists(address)` | Check if exists | -| `graphEntryCost(publicKey)` | Estimate creation cost | - ### Files & Directories | Method | Description | |--------|-------------| @@ -216,5 +208,4 @@ See the [example/](example/) directory: - `02_data` — Public data storage and retrieval - `03_chunks` — Raw chunk operations - `04_files` — File and directory upload/download -- `05_graph` — Graph entry (DAG node) operations - `06_private_data` — Private encrypted data storage diff --git a/antd-dart/example/05_graph.dart b/antd-dart/example/05_graph.dart deleted file mode 100644 index a5bd587..0000000 --- a/antd-dart/example/05_graph.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:antd/antd.dart'; - -/// Demonstrates graph entry (DAG node) operations. -void main() async { - final client = AntdClient(); - - try { - // Create a graph entry - final result = await client.graphEntryPut( - 'your_secret_key_hex', - [], // no parents (root node) - 'content_hash_hex', - [ - GraphDescendant(publicKey: 'descendant_pk_hex', content: 'desc_content_hex'), - ], - ); - print('Graph entry created at ${result.address} (cost: ${result.cost} atto)'); - - // Read the graph entry - final entry = await client.graphEntryGet(result.address); - print('Owner: ${entry.owner}'); - print('Parents: ${entry.parents}'); - print('Content: ${entry.content}'); - print('Descendants: ${entry.descendants.length}'); - - // Check existence - final exists = await client.graphEntryExists(result.address); - print('Exists: $exists'); - - // Estimate cost - final cost = await client.graphEntryCost('your_public_key_hex'); - print('Estimated cost: $cost atto'); - } on AntdError catch (e) { - print('Error: $e'); - } finally { - client.close(); - } -} diff --git a/antd-dart/lib/src/client.dart b/antd-dart/lib/src/client.dart index e634415..05c36e6 100644 --- a/antd-dart/lib/src/client.dart +++ b/antd-dart/lib/src/client.dart @@ -199,50 +199,6 @@ class AntdClient { return _b64Decode(json!['data'] as String); } - // --- Graph --- - - /// Creates a new graph entry (DAG node). - Future graphEntryPut( - String ownerSecretKey, - List parents, - String content, - List descendants, - ) async { - final json = await _doJson('POST', '/v1/graph', { - 'owner_secret_key': ownerSecretKey, - 'parents': parents, - 'content': content, - 'descendants': descendants.map((d) => d.toJson()).toList(), - }); - return PutResult.fromJson(json!); - } - - /// Retrieves a graph entry by address. - Future graphEntryGet(String address) async { - final json = await _doJson('GET', '/v1/graph/$address'); - return GraphEntry.fromJson(json!); - } - - /// Checks if a graph entry exists at the given address. - Future graphEntryExists(String address) async { - final code = await _doHead('/v1/graph/$address'); - if (code == 404) { - return false; - } - if (code >= 300) { - throw errorForStatus(code, 'graph entry exists check failed'); - } - return true; - } - - /// Estimates the cost of creating a graph entry. - Future graphEntryCost(String publicKey) async { - final json = await _doJson('POST', '/v1/graph/cost', { - 'public_key': publicKey, - }); - return json!['cost'] as String; - } - // --- Files --- /// Uploads a local file to the network. diff --git a/antd-dart/lib/src/grpc_client.dart b/antd-dart/lib/src/grpc_client.dart index 41228e4..9911618 100644 --- a/antd-dart/lib/src/grpc_client.dart +++ b/antd-dart/lib/src/grpc_client.dart @@ -10,9 +10,6 @@ import 'generated/antd/v1/data.pbgrpc.dart' as data_pb; import 'generated/antd/v1/data.pb.dart' as data_msg; import 'generated/antd/v1/chunks.pbgrpc.dart' as chunks_pb; import 'generated/antd/v1/chunks.pb.dart' as chunks_msg; -import 'generated/antd/v1/graph.pbgrpc.dart' as graph_pb; -import 'generated/antd/v1/graph.pb.dart' as graph_msg; -import 'generated/antd/v1/common.pb.dart' as common_pb; import 'generated/antd/v1/files.pbgrpc.dart' as files_pb; import 'generated/antd/v1/files.pb.dart' as files_msg; @@ -22,7 +19,7 @@ import 'models.dart'; /// gRPC client for the antd daemon. /// -/// Provides the same 19 async methods as [AntdClient] (REST), but communicates +/// Provides the same 15 async methods as [AntdClient] (REST), but communicates /// over gRPC using the proto-generated stubs from `antd/v1/*.proto`. /// /// **Proto compilation**: Run `protoc` with the Dart gRPC plugin to generate @@ -40,7 +37,6 @@ class GrpcAntdClient { final health_pb.HealthServiceClient _healthStub; final data_pb.DataServiceClient _dataStub; final chunks_pb.ChunkServiceClient _chunkStub; - final graph_pb.GraphServiceClient _graphStub; final files_pb.FileServiceClient _fileStub; /// Creates a new antd gRPC client. @@ -87,15 +83,6 @@ class GrpcAntdClient { credentials: ChannelCredentials.insecure()), ), ), - _graphStub = graph_pb.GraphServiceClient( - channel ?? - ClientChannel( - host, - port: port, - options: const ChannelOptions( - credentials: ChannelCredentials.insecure()), - ), - ), _fileStub = files_pb.FileServiceClient( channel ?? ClientChannel( @@ -279,81 +266,6 @@ class GrpcAntdClient { } } - // --------------------------------------------------------------------------- - // Graph Entries (DAG Nodes) - // --------------------------------------------------------------------------- - - /// Creates a new graph entry (DAG node). - Future graphEntryPut( - String ownerSecretKey, - List parents, - String content, - List descendants, - ) async { - try { - final req = graph_msg.PutGraphEntryRequest() - ..ownerSecretKey = ownerSecretKey - ..parents.addAll(parents) - ..content = content - ..descendants.addAll( - descendants.map( - (d) => common_pb.GraphDescendant() - ..publicKey = d.publicKey - ..content = d.content, - ), - ); - final resp = await _graphStub.put(req); - return PutResult( - cost: resp.cost.attoTokens, - address: resp.address, - ); - } on GrpcError catch (e) { - _handleError(e); - } - } - - /// Retrieves a graph entry by address. - Future graphEntryGet(String address) async { - try { - final req = graph_msg.GetGraphEntryRequest()..address = address; - final resp = await _graphStub.get(req); - return GraphEntry( - owner: resp.owner, - parents: List.unmodifiable(resp.parents), - content: resp.content, - descendants: List.unmodifiable( - resp.descendants.map( - (d) => GraphDescendant(publicKey: d.publicKey, content: d.content), - ), - ), - ); - } on GrpcError catch (e) { - _handleError(e); - } - } - - /// Checks if a graph entry exists at the given address. - Future graphEntryExists(String address) async { - try { - final req = graph_msg.CheckGraphEntryRequest()..address = address; - final resp = await _graphStub.checkExistence(req); - return resp.exists; - } on GrpcError catch (e) { - _handleError(e); - } - } - - /// Estimates the cost of creating a graph entry. - Future graphEntryCost(String publicKey) async { - try { - final req = graph_msg.GraphEntryCostRequest()..publicKey = publicKey; - final resp = await _graphStub.getCost(req); - return resp.attoTokens; - } on GrpcError catch (e) { - _handleError(e); - } - } - // --------------------------------------------------------------------------- // Files & Directories // --------------------------------------------------------------------------- diff --git a/antd-dart/lib/src/models.dart b/antd-dart/lib/src/models.dart index 425ae2c..530febf 100644 --- a/antd-dart/lib/src/models.dart +++ b/antd-dart/lib/src/models.dart @@ -41,75 +41,6 @@ class PutResult { String toString() => 'PutResult(cost: $cost, address: $address)'; } -/// GraphDescendant is a descendant entry in a graph node. -class GraphDescendant { - /// The public key in hex. - final String publicKey; - - /// The content in hex (32 bytes). - final String content; - - const GraphDescendant({required this.publicKey, required this.content}); - - factory GraphDescendant.fromJson(Map json) { - return GraphDescendant( - publicKey: json['public_key'] as String? ?? '', - content: json['content'] as String? ?? '', - ); - } - - Map toJson() => { - 'public_key': publicKey, - 'content': content, - }; - - @override - String toString() => - 'GraphDescendant(publicKey: $publicKey, content: $content)'; -} - -/// GraphEntry is a DAG node from the network. -class GraphEntry { - /// The owner public key. - final String owner; - - /// Parent addresses. - final List parents; - - /// The content hash. - final String content; - - /// Descendant entries. - final List descendants; - - const GraphEntry({ - required this.owner, - required this.parents, - required this.content, - required this.descendants, - }); - - factory GraphEntry.fromJson(Map json) { - return GraphEntry( - owner: json['owner'] as String? ?? '', - parents: (json['parents'] as List?) - ?.map((e) => e as String) - .toList() ?? - [], - content: json['content'] as String? ?? '', - descendants: (json['descendants'] as List?) - ?.map((e) => - GraphDescendant.fromJson(e as Map)) - .toList() ?? - [], - ); - } - - @override - String toString() => - 'GraphEntry(owner: $owner, parents: $parents, content: $content, descendants: $descendants)'; -} - /// ArchiveEntry is a single entry in a file archive. class ArchiveEntry { /// The file path within the archive. diff --git a/antd-dart/test/client_test.dart b/antd-dart/test/client_test.dart index b21631b..001f8c1 100644 --- a/antd-dart/test/client_test.dart +++ b/antd-dart/test/client_test.dart @@ -57,28 +57,6 @@ MockClient mockDaemon() { body = {'data': base64.encode(utf8.encode('chunkdata'))}; break; - // Graph - case 'POST /v1/graph': - body = {'cost': '500', 'address': 'ge1'}; - break; - case 'GET /v1/graph/ge1': - body = { - 'owner': 'owner1', - 'parents': [], - 'content': 'abc', - 'descendants': [ - {'public_key': 'pk1', 'content': 'desc1'} - ], - }; - break; - case 'HEAD /v1/graph/ge1': - return http.Response('', 200); - case 'HEAD /v1/graph/missing': - return http.Response('', 404); - case 'POST /v1/graph/cost': - body = {'cost': '500'}; - break; - // Files case 'POST /v1/files/upload/public': body = {'cost': '1000', 'address': 'file1'}; @@ -199,35 +177,6 @@ void main() { }); }); - group('Graph', () { - test('put, get, and check existence of graph entries', () async { - final client = AntdClient(httpClient: mockDaemon()); - - final put = await client.graphEntryPut('sk1', [], 'abc', []); - expect(put.address, equals('ge1')); - - final entry = await client.graphEntryGet('ge1'); - expect(entry.owner, equals('owner1')); - expect(entry.descendants.length, equals(1)); - expect(entry.descendants[0].publicKey, equals('pk1')); - - final exists = await client.graphEntryExists('ge1'); - expect(exists, isTrue); - - final missing = await client.graphEntryExists('missing'); - expect(missing, isFalse); - - client.close(); - }); - - test('estimates graph entry cost', () async { - final client = AntdClient(httpClient: mockDaemon()); - final cost = await client.graphEntryCost('pk1'); - expect(cost, equals('500')); - client.close(); - }); - }); - group('Files', () { test('upload and download files', () async { final client = AntdClient(httpClient: mockDaemon()); diff --git a/antd-dart/test/grpc_client_test.dart b/antd-dart/test/grpc_client_test.dart index 78a3e1a..9345560 100644 --- a/antd-dart/test/grpc_client_test.dart +++ b/antd-dart/test/grpc_client_test.dart @@ -8,7 +8,7 @@ import 'package:test/test.dart'; // Standalone fake gRPC client for testing. // // Does NOT import grpc_client.dart (which requires proto-generated stubs). -// Instead, defines a _FakeGrpcClient with the same 19-method API that returns +// Instead, defines a _FakeGrpcClient with the same 15-method API that returns // canned responses or throws fake gRPC-like errors for error mapping tests. // --------------------------------------------------------------------------- @@ -34,7 +34,7 @@ Exception mapGrpcError(FakeGrpcError e) { } } -/// Fake gRPC client returning canned responses for all 19 methods. +/// Fake gRPC client returning canned responses for all 15 methods. class _FakeGrpcClient { final FakeGrpcError? errorToThrow; @@ -69,27 +69,6 @@ Future chunkPut(Uint8List data) => Future chunkGet(String address) => _maybeThrow(Uint8List.fromList([99, 104, 117, 110, 107])); // "chunk" -Future graphEntryPut( - String ownerSecretKey, - List parents, - String content, - List descendants, - ) => - _maybeThrow(const PutResult(cost: '500', address: 'ge1')); - -Future graphEntryGet(String address) => - _maybeThrow(const GraphEntry( - owner: 'owner1', - parents: [], - content: 'abc', - descendants: [GraphDescendant(publicKey: 'pk1', content: 'desc1')], - )); - -Future graphEntryExists(String address) => - _maybeThrow(address == 'ge1'); - -Future graphEntryCost(String publicKey) => _maybeThrow('500'); - Future fileUploadPublic(String path) => _maybeThrow(const PutResult(cost: '1000', address: 'file1')); @@ -137,7 +116,7 @@ _FakeGrpcClient errorClient(int grpcCode, String message) { void main() { // ------------------------------------------------------------------------- - // Happy-path tests – all 19 methods + // Happy-path tests – all 15 methods // ------------------------------------------------------------------------- group('Health', () { @@ -202,41 +181,6 @@ void main() { }); }); - group('Graph', () { - test('put graph entry', () async { - final client = _FakeGrpcClient(); - final result = await client.graphEntryPut('sk1', [], 'abc', []); - expect(result.cost, equals('500')); - expect(result.address, equals('ge1')); - }); - - test('get graph entry', () async { - final client = _FakeGrpcClient(); - final entry = await client.graphEntryGet('ge1'); - expect(entry.owner, equals('owner1')); - expect(entry.parents, isEmpty); - expect(entry.content, equals('abc')); - expect(entry.descendants.length, equals(1)); - expect(entry.descendants[0].publicKey, equals('pk1')); - expect(entry.descendants[0].content, equals('desc1')); - }); - - test('graph entry exists returns true', () async { - final client = _FakeGrpcClient(); - expect(await client.graphEntryExists('ge1'), isTrue); - }); - - test('graph entry exists returns false', () async { - final client = _FakeGrpcClient(); - expect(await client.graphEntryExists('missing'), isFalse); - }); - - test('graph entry cost', () async { - final client = _FakeGrpcClient(); - expect(await client.graphEntryCost('pk1'), equals('500')); - }); - }); - group('Files', () { test('upload file', () async { final client = _FakeGrpcClient(); @@ -389,14 +333,6 @@ void main() { ); }); - test('error propagates from graphEntryPut', () async { - final client = errorClient(6, 'duplicate'); - expect( - () => client.graphEntryPut('sk', [], 'c', []), - throwsA(isA()), - ); - }); - test('error propagates from fileUploadPublic', () async { final client = errorClient(8, 'too large'); expect( diff --git a/antd-elixir/README.md b/antd-elixir/README.md index 4aaa7ee..b192e85 100644 --- a/antd-elixir/README.md +++ b/antd-elixir/README.md @@ -47,7 +47,7 @@ client = Antd.Client.new() ## gRPC Transport -The SDK includes an `Antd.GrpcClient` module that provides the same 19 +The SDK includes an `Antd.GrpcClient` module that provides the same functions as the REST `Antd.Client`, but communicates over gRPC. ### Setup @@ -65,7 +65,7 @@ Generate the Elixir protobuf/gRPC stubs from the proto definitions: protoc --elixir_out=plugins=grpc:lib \ -I../../antd/proto \ antd/v1/common.proto antd/v1/health.proto antd/v1/data.proto \ - antd/v1/chunks.proto antd/v1/graph.proto antd/v1/files.proto + antd/v1/chunks.proto antd/v1/files.proto ``` The generated modules are expected under `lib/antd/v1/`. @@ -144,15 +144,6 @@ All functions take a `%Antd.Client{}` as the first argument. Each returns `{:ok, | `chunk_put(client, data)` | Store a raw chunk | | `chunk_get(client, address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) - -| Function | Description | -|----------|-------------| -| `graph_entry_put(client, secret_key, parents, content, descendants)` | Create entry | -| `graph_entry_get(client, address)` | Read entry | -| `graph_entry_exists(client, address)` | Check if exists | -| `graph_entry_cost(client, public_key)` | Estimate creation cost | - ### Files & Directories | Function | Description | @@ -216,5 +207,4 @@ See the [examples/](examples/) directory: - `02_data.exs` — Public data storage and retrieval - `03_chunks.exs` — Raw chunk operations - `04_files.exs` — File and directory upload/download -- `05_graph.exs` — Graph entries (DAG nodes) - `06_private_data.exs` — Private encrypted data diff --git a/antd-elixir/lib/antd.ex b/antd-elixir/lib/antd.ex index 656f610..476e9da 100644 --- a/antd-elixir/lib/antd.ex +++ b/antd-elixir/lib/antd.ex @@ -41,14 +41,6 @@ defmodule Antd do defdelegate chunk_put!(client, data), to: Antd.Client defdelegate chunk_get(client, address), to: Antd.Client defdelegate chunk_get!(client, address), to: Antd.Client - defdelegate graph_entry_put(client, owner_secret_key, parents, content, descendants), to: Antd.Client - defdelegate graph_entry_put!(client, owner_secret_key, parents, content, descendants), to: Antd.Client - defdelegate graph_entry_get(client, address), to: Antd.Client - defdelegate graph_entry_get!(client, address), to: Antd.Client - defdelegate graph_entry_exists(client, address), to: Antd.Client - defdelegate graph_entry_exists!(client, address), to: Antd.Client - defdelegate graph_entry_cost(client, public_key), to: Antd.Client - defdelegate graph_entry_cost!(client, public_key), to: Antd.Client defdelegate file_upload_public(client, path), to: Antd.Client defdelegate file_upload_public!(client, path), to: Antd.Client defdelegate file_download_public(client, address, dest_path), to: Antd.Client diff --git a/antd-elixir/lib/antd/client.ex b/antd-elixir/lib/antd/client.ex index d6e93ab..d3b5b8e 100644 --- a/antd-elixir/lib/antd/client.ex +++ b/antd-elixir/lib/antd/client.ex @@ -204,98 +204,6 @@ defmodule Antd.Client do @spec chunk_get!(t(), String.t()) :: binary() def chunk_get!(client, address), do: unwrap!(chunk_get(client, address)) - # --------------------------------------------------------------------------- - # Graph - # --------------------------------------------------------------------------- - - @doc "Creates a new graph entry (DAG node)." - @spec graph_entry_put(t(), String.t(), [String.t()], String.t(), [Antd.GraphDescendant.t()]) :: - {:ok, Antd.PutResult.t()} | {:error, Exception.t()} - def graph_entry_put(%__MODULE__{} = client, owner_secret_key, parents, content, descendants) do - descs = - Enum.map(descendants, fn d -> - %{public_key: d.public_key, content: d.content} - end) - - payload = %{ - owner_secret_key: owner_secret_key, - parents: parents, - content: content, - descendants: descs - } - - case do_json(client, :post, "/v1/graph", payload) do - {:ok, body} -> - {:ok, %Antd.PutResult{cost: body["cost"], address: body["address"]}} - - {:error, _} = err -> - err - end - end - - @doc "Like `graph_entry_put/5` but raises on error." - @spec graph_entry_put!(t(), String.t(), [String.t()], String.t(), [Antd.GraphDescendant.t()]) :: - Antd.PutResult.t() - def graph_entry_put!(client, owner_secret_key, parents, content, descendants) do - unwrap!(graph_entry_put(client, owner_secret_key, parents, content, descendants)) - end - - @doc "Retrieves a graph entry by address." - @spec graph_entry_get(t(), String.t()) :: {:ok, Antd.GraphEntry.t()} | {:error, Exception.t()} - def graph_entry_get(%__MODULE__{} = client, address) do - case do_json(client, :get, "/v1/graph/#{address}", nil) do - {:ok, body} -> - descendants = - (body["descendants"] || []) - |> Enum.map(fn d -> - %Antd.GraphDescendant{public_key: d["public_key"], content: d["content"]} - end) - - {:ok, - %Antd.GraphEntry{ - owner: body["owner"], - parents: body["parents"] || [], - content: body["content"], - descendants: descendants - }} - - {:error, _} = err -> - err - end - end - - @doc "Like `graph_entry_get/2` but raises on error." - @spec graph_entry_get!(t(), String.t()) :: Antd.GraphEntry.t() - def graph_entry_get!(client, address), do: unwrap!(graph_entry_get(client, address)) - - @doc "Checks if a graph entry exists at the given address." - @spec graph_entry_exists(t(), String.t()) :: {:ok, boolean()} | {:error, Exception.t()} - def graph_entry_exists(%__MODULE__{} = client, address) do - case do_head(client, "/v1/graph/#{address}") do - {:ok, status} when status >= 200 and status < 300 -> {:ok, true} - {:ok, 404} -> {:ok, false} - {:ok, status} -> {:error, Antd.Errors.error_for_status(status, "graph entry exists check failed")} - {:error, _} = err -> err - end - end - - @doc "Like `graph_entry_exists/2` but raises on error." - @spec graph_entry_exists!(t(), String.t()) :: boolean() - def graph_entry_exists!(client, address), do: unwrap!(graph_entry_exists(client, address)) - - @doc "Estimates the cost of creating a graph entry." - @spec graph_entry_cost(t(), String.t()) :: {:ok, String.t()} | {:error, Exception.t()} - def graph_entry_cost(%__MODULE__{} = client, public_key) do - case do_json(client, :post, "/v1/graph/cost", %{public_key: public_key}) do - {:ok, body} -> {:ok, body["cost"]} - {:error, _} = err -> err - end - end - - @doc "Like `graph_entry_cost/2` but raises on error." - @spec graph_entry_cost!(t(), String.t()) :: String.t() - def graph_entry_cost!(client, public_key), do: unwrap!(graph_entry_cost(client, public_key)) - # --------------------------------------------------------------------------- # Files & Directories # --------------------------------------------------------------------------- diff --git a/antd-elixir/lib/antd/grpc_client.ex b/antd-elixir/lib/antd/grpc_client.ex index 4b1074f..a995984 100644 --- a/antd-elixir/lib/antd/grpc_client.ex +++ b/antd-elixir/lib/antd/grpc_client.ex @@ -2,7 +2,7 @@ defmodule Antd.GrpcClient do @moduledoc """ gRPC client for the antd daemon. - Provides the same 19 functions as `Antd.Client` (REST), but communicates over + Provides the same functions as `Antd.Client` (REST), but communicates over gRPC using the proto-generated modules from `antd/v1/*.proto`. All public functions return `{:ok, result}` or `{:error, exception}`. @@ -15,7 +15,7 @@ defmodule Antd.GrpcClient do protoc --elixir_out=plugins=grpc:lib \\ -I../../antd/proto \\ antd/v1/common.proto antd/v1/health.proto antd/v1/data.proto \\ - antd/v1/chunks.proto antd/v1/graph.proto antd/v1/files.proto + antd/v1/chunks.proto antd/v1/files.proto The generated modules are expected under `lib/antd/v1/`. """ @@ -225,102 +225,6 @@ defmodule Antd.GrpcClient do @spec chunk_get!(t(), String.t()) :: binary() def chunk_get!(client, address), do: unwrap!(chunk_get(client, address)) - # --------------------------------------------------------------------------- - # Graph - # --------------------------------------------------------------------------- - - @doc "Creates a new graph entry (DAG node)." - @spec graph_entry_put(t(), String.t(), [String.t()], String.t(), [Antd.GraphDescendant.t()]) :: - {:ok, Antd.PutResult.t()} | {:error, Exception.t()} - def graph_entry_put(%__MODULE__{channel: channel}, owner_secret_key, parents, content, descendants) do - descs = - Enum.map(descendants, fn d -> - Antd.V1.GraphDescendant.new(public_key: d.public_key, content: d.content) - end) - - req = - Antd.V1.PutGraphEntryRequest.new( - owner_secret_key: owner_secret_key, - parents: parents, - content: content, - descendants: descs - ) - - case Antd.V1.GraphService.Stub.put(channel, req) do - {:ok, resp} -> - {:ok, %Antd.PutResult{cost: resp.cost.atto_tokens, address: resp.address}} - - {:error, rpc_error} -> - {:error, translate_error(rpc_error)} - end - end - - @doc "Like `graph_entry_put/5` but raises on error." - @spec graph_entry_put!(t(), String.t(), [String.t()], String.t(), [Antd.GraphDescendant.t()]) :: - Antd.PutResult.t() - def graph_entry_put!(client, owner_secret_key, parents, content, descendants) do - unwrap!(graph_entry_put(client, owner_secret_key, parents, content, descendants)) - end - - @doc "Retrieves a graph entry by address." - @spec graph_entry_get(t(), String.t()) :: {:ok, Antd.GraphEntry.t()} | {:error, Exception.t()} - def graph_entry_get(%__MODULE__{channel: channel}, address) do - req = Antd.V1.GetGraphEntryRequest.new(address: address) - - case Antd.V1.GraphService.Stub.get(channel, req) do - {:ok, resp} -> - descendants = - Enum.map(resp.descendants, fn d -> - %Antd.GraphDescendant{public_key: d.public_key, content: d.content} - end) - - {:ok, - %Antd.GraphEntry{ - owner: resp.owner, - parents: Enum.to_list(resp.parents), - content: resp.content, - descendants: descendants - }} - - {:error, rpc_error} -> - {:error, translate_error(rpc_error)} - end - end - - @doc "Like `graph_entry_get/2` but raises on error." - @spec graph_entry_get!(t(), String.t()) :: Antd.GraphEntry.t() - def graph_entry_get!(client, address), do: unwrap!(graph_entry_get(client, address)) - - @doc "Checks if a graph entry exists at the given address." - @spec graph_entry_exists(t(), String.t()) :: {:ok, boolean()} | {:error, Exception.t()} - def graph_entry_exists(%__MODULE__{channel: channel}, address) do - req = Antd.V1.CheckGraphEntryRequest.new(address: address) - - case Antd.V1.GraphService.Stub.check_existence(channel, req) do - {:ok, resp} -> {:ok, resp.exists} - {:error, rpc_error} -> {:error, translate_error(rpc_error)} - end - end - - @doc "Like `graph_entry_exists/2` but raises on error." - @spec graph_entry_exists!(t(), String.t()) :: boolean() - def graph_entry_exists!(client, address), do: unwrap!(graph_entry_exists(client, address)) - - @doc "Estimates the cost of creating a graph entry." - @spec graph_entry_cost(t(), String.t()) :: {:ok, String.t()} | {:error, Exception.t()} - def graph_entry_cost(%__MODULE__{channel: channel}, public_key) do - req = Antd.V1.GraphEntryCostRequest.new(public_key: public_key) - - case Antd.V1.GraphService.Stub.get_cost(channel, req) do - {:ok, resp} -> {:ok, resp.atto_tokens} - {:error, rpc_error} -> {:error, translate_error(rpc_error)} - end - end - - @doc "Like `graph_entry_cost/2` but raises on error." - @spec graph_entry_cost!(t(), String.t()) :: String.t() - def graph_entry_cost!(client, public_key), do: unwrap!(graph_entry_cost(client, public_key)) - # --------------------------------------------------------------------------- # Files & Directories # --------------------------------------------------------------------------- diff --git a/antd-elixir/lib/antd/models.ex b/antd-elixir/lib/antd/models.ex index bc1cf7d..7158519 100644 --- a/antd-elixir/lib/antd/models.ex +++ b/antd-elixir/lib/antd/models.ex @@ -22,32 +22,6 @@ defmodule Antd.PutResult do } end -defmodule Antd.GraphDescendant do - @moduledoc "A descendant entry in a graph node." - - @enforce_keys [:public_key, :content] - defstruct [:public_key, :content] - - @type t :: %__MODULE__{ - public_key: String.t(), - content: String.t() - } -end - -defmodule Antd.GraphEntry do - @moduledoc "A DAG node from the network." - - @enforce_keys [:owner, :parents, :content, :descendants] - defstruct [:owner, :parents, :content, :descendants] - - @type t :: %__MODULE__{ - owner: String.t(), - parents: [String.t()], - content: String.t(), - descendants: [Antd.GraphDescendant.t()] - } -end - defmodule Antd.ArchiveEntry do @moduledoc "A single entry in a file archive." diff --git a/antd-go/README.md b/antd-go/README.md index ac5526b..dacb785 100644 --- a/antd-go/README.md +++ b/antd-go/README.md @@ -105,14 +105,6 @@ All methods take a `context.Context` as the first parameter for cancellation and | `ChunkPut(ctx, data)` | Store a raw chunk | | `ChunkGet(ctx, address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) -| Method | Description | -|--------|-------------| -| `GraphEntryPut(ctx, secretKey, parents, content, descendants)` | Create entry | -| `GraphEntryGet(ctx, address)` | Read entry | -| `GraphEntryExists(ctx, address)` | Check if exists | -| `GraphEntryCost(ctx, publicKey)` | Estimate creation cost | - ### Files & Directories | Method | Description | |--------|-------------| @@ -127,7 +119,7 @@ All methods take a `context.Context` as the first parameter for cancellation and ## gRPC Transport The SDK also provides a `GrpcClient` that connects to the antd daemon over gRPC. -It exposes the same 19 methods with identical signatures and error types as the REST client. +It exposes the same methods with identical signatures and error types as the REST client. ### Generating Proto Stubs diff --git a/antd-go/client.go b/antd-go/client.go index 24accfd..641fd83 100644 --- a/antd-go/client.go +++ b/antd-go/client.go @@ -287,72 +287,6 @@ func (c *Client) ChunkGet(ctx context.Context, address string) ([]byte, error) { return b64Decode(str(j, "data")) } -// --- Graph --- - -// GraphEntryPut creates a new graph entry (DAG node). -func (c *Client) GraphEntryPut(ctx context.Context, ownerSecretKey string, parents []string, content string, descendants []GraphDescendant) (*PutResult, error) { - descs := make([]map[string]any, len(descendants)) - for i, d := range descendants { - descs[i] = map[string]any{"public_key": d.PublicKey, "content": d.Content} - } - j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/graph", map[string]any{ - "owner_secret_key": ownerSecretKey, - "parents": parents, - "content": content, - "descendants": descs, - }) - if err != nil { - return nil, err - } - return &PutResult{Cost: str(j, "cost"), Address: str(j, "address")}, nil -} - -// GraphEntryGet retrieves a graph entry by address. -func (c *Client) GraphEntryGet(ctx context.Context, address string) (*GraphEntry, error) { - j, _, err := c.doJSON(ctx, http.MethodGet, "/v1/graph/"+address, nil) - if err != nil { - return nil, err - } - var descs []GraphDescendant - for _, d := range arrAt(j, "descendants") { - if dm, ok := d.(map[string]any); ok { - descs = append(descs, GraphDescendant{PublicKey: str(dm, "public_key"), Content: str(dm, "content")}) - } - } - return &GraphEntry{ - Owner: str(j, "owner"), - Parents: strSlice(j, "parents"), - Content: str(j, "content"), - Descendants: descs, - }, nil -} - -// GraphEntryExists checks if a graph entry exists at the given address. -func (c *Client) GraphEntryExists(ctx context.Context, address string) (bool, error) { - code, err := c.doHead(ctx, "/v1/graph/"+address) - if err != nil { - return false, err - } - if code == 404 { - return false, nil - } - if code >= 300 { - return false, errorForStatus(code, "graph entry exists check failed") - } - return true, nil -} - -// GraphEntryCost estimates the cost of creating a graph entry. -func (c *Client) GraphEntryCost(ctx context.Context, publicKey string) (string, error) { - j, _, err := c.doJSON(ctx, http.MethodPost, "/v1/graph/cost", map[string]any{ - "public_key": publicKey, - }) - if err != nil { - return "", err - } - return str(j, "cost"), nil -} - // --- Files --- // FileUploadPublic uploads a local file to the network. diff --git a/antd-go/client_test.go b/antd-go/client_test.go index 1d610ae..5792d32 100644 --- a/antd-go/client_test.go +++ b/antd-go/client_test.go @@ -47,19 +47,6 @@ func mockDaemon(t *testing.T) *httptest.Server { case r.Method == "GET" && r.URL.Path == "/v1/chunks/chunk1": json.NewEncoder(w).Encode(map[string]any{"data": base64.StdEncoding.EncodeToString([]byte("chunkdata"))}) - // Graph - case r.Method == "POST" && r.URL.Path == "/v1/graph": - json.NewEncoder(w).Encode(map[string]any{"cost": "500", "address": "ge1"}) - case r.Method == "GET" && r.URL.Path == "/v1/graph/ge1": - json.NewEncoder(w).Encode(map[string]any{ - "owner": "owner1", "parents": []any{}, "content": "abc", - "descendants": []any{map[string]any{"public_key": "pk1", "content": "desc1"}}, - }) - case r.Method == "HEAD" && r.URL.Path == "/v1/graph/ge1": - w.WriteHeader(200) - case r.Method == "POST" && r.URL.Path == "/v1/graph/cost": - json.NewEncoder(w).Encode(map[string]any{"cost": "500"}) - // Files case r.Method == "POST" && r.URL.Path == "/v1/files/upload/public": json.NewEncoder(w).Encode(map[string]any{"cost": "1000", "address": "file1"}) @@ -183,37 +170,6 @@ func TestChunks(t *testing.T) { } } -func TestGraph(t *testing.T) { - srv := mockDaemon(t) - defer srv.Close() - c := NewClient(srv.URL) - ctx := context.Background() - - put, err := c.GraphEntryPut(ctx, "sk1", []string{}, "abc", []GraphDescendant{}) - if err != nil { - t.Fatal(err) - } - if put.Address != "ge1" { - t.Fatalf("unexpected graph put: %+v", put) - } - - ge, err := c.GraphEntryGet(ctx, "ge1") - if err != nil { - t.Fatal(err) - } - if ge.Owner != "owner1" || len(ge.Descendants) != 1 { - t.Fatalf("unexpected graph entry: %+v", ge) - } - - exists, err := c.GraphEntryExists(ctx, "ge1") - if err != nil { - t.Fatal(err) - } - if !exists { - t.Fatal("expected graph entry to exist") - } -} - func TestFiles(t *testing.T) { srv := mockDaemon(t) defer srv.Close() diff --git a/antd-go/grpc_client.go b/antd-go/grpc_client.go index b5aa136..4400eba 100644 --- a/antd-go/grpc_client.go +++ b/antd-go/grpc_client.go @@ -32,7 +32,7 @@ func WithDialOptions(opts ...grpc.DialOption) GrpcOption { return func(c *GrpcClient) { c.dialOpts = append(c.dialOpts, opts...) } } -// GrpcClient is a gRPC client for the antd daemon. It exposes the same 19 +// GrpcClient is a gRPC client for the antd daemon. It exposes the same // methods as the REST Client, using the same model types and error types. type GrpcClient struct { conn *grpc.ClientConn @@ -43,8 +43,7 @@ type GrpcClient struct { health pb.HealthServiceClient data pb.DataServiceClient chunk pb.ChunkServiceClient - graph pb.GraphServiceClient - file pb.FileServiceClient + file pb.FileServiceClient } // NewGrpcClientAutoDiscover creates a gRPC client that discovers the daemon target @@ -83,7 +82,6 @@ func NewGrpcClient(target string, opts ...GrpcOption) (*GrpcClient, error) { c.health = pb.NewHealthServiceClient(conn) c.data = pb.NewDataServiceClient(conn) c.chunk = pb.NewChunkServiceClient(conn) - c.graph = pb.NewGraphServiceClient(conn) c.file = pb.NewFileServiceClient(conn) return c, nil @@ -259,84 +257,6 @@ func (c *GrpcClient) ChunkGet(ctx context.Context, address string) ([]byte, erro return resp.GetData(), nil } -// --- Graph (4 methods) --- - -// GraphEntryPut creates a new graph entry (DAG node). -func (c *GrpcClient) GraphEntryPut(ctx context.Context, ownerSecretKey string, parents []string, content string, descendants []GraphDescendant) (*PutResult, error) { - ctx, cancel := c.ctx(ctx) - defer cancel() - - pbDescs := make([]*pb.GraphDescendant, len(descendants)) - for i, d := range descendants { - pbDescs[i] = &pb.GraphDescendant{ - PublicKey: d.PublicKey, - Content: d.Content, - } - } - - resp, err := c.graph.Put(ctx, &pb.PutGraphEntryRequest{ - OwnerSecretKey: ownerSecretKey, - Parents: parents, - Content: content, - Descendants: pbDescs, - }) - if err != nil { - return nil, errorFromGrpc(err) - } - return &PutResult{ - Cost: resp.GetCost().GetAttoTokens(), - Address: resp.GetAddress(), - }, nil -} - -// GraphEntryGet retrieves a graph entry by address. -func (c *GrpcClient) GraphEntryGet(ctx context.Context, address string) (*GraphEntry, error) { - ctx, cancel := c.ctx(ctx) - defer cancel() - - resp, err := c.graph.Get(ctx, &pb.GetGraphEntryRequest{Address: address}) - if err != nil { - return nil, errorFromGrpc(err) - } - descs := make([]GraphDescendant, len(resp.GetDescendants())) - for i, d := range resp.GetDescendants() { - descs[i] = GraphDescendant{ - PublicKey: d.GetPublicKey(), - Content: d.GetContent(), - } - } - return &GraphEntry{ - Owner: resp.GetOwner(), - Parents: resp.GetParents(), - Content: resp.GetContent(), - Descendants: descs, - }, nil -} - -// GraphEntryExists checks if a graph entry exists at the given address. -func (c *GrpcClient) GraphEntryExists(ctx context.Context, address string) (bool, error) { - ctx, cancel := c.ctx(ctx) - defer cancel() - - resp, err := c.graph.CheckExistence(ctx, &pb.CheckGraphEntryRequest{Address: address}) - if err != nil { - return false, errorFromGrpc(err) - } - return resp.GetExists(), nil -} - -// GraphEntryCost estimates the cost of creating a graph entry. -func (c *GrpcClient) GraphEntryCost(ctx context.Context, publicKey string) (string, error) { - ctx, cancel := c.ctx(ctx) - defer cancel() - - resp, err := c.graph.GetCost(ctx, &pb.GraphEntryCostRequest{PublicKey: publicKey}) - if err != nil { - return "", errorFromGrpc(err) - } - return resp.GetAttoTokens(), nil -} - // --- Files (7 methods) --- // FileUploadPublic uploads a local file to the network. diff --git a/antd-go/grpc_client_test.go b/antd-go/grpc_client_test.go index 0323ce0..e851c04 100644 --- a/antd-go/grpc_client_test.go +++ b/antd-go/grpc_client_test.go @@ -78,37 +78,6 @@ func (m *mockChunkService) Get(_ context.Context, _ *pb.GetChunkRequest) (*pb.Ge return &pb.GetChunkResponse{Data: []byte("chunkdata")}, nil } -// mockGraphService implements pb.GraphServiceServer. -type mockGraphService struct { - pb.UnimplementedGraphServiceServer -} - -func (m *mockGraphService) Put(_ context.Context, _ *pb.PutGraphEntryRequest) (*pb.PutGraphEntryResponse, error) { - return &pb.PutGraphEntryResponse{ - Cost: &pb.Cost{AttoTokens: "500"}, - Address: "ge1", - }, nil -} - -func (m *mockGraphService) Get(_ context.Context, _ *pb.GetGraphEntryRequest) (*pb.GetGraphEntryResponse, error) { - return &pb.GetGraphEntryResponse{ - Owner: "owner1", - Parents: []string{}, - Content: "abc", - Descendants: []*pb.GraphDescendant{ - {PublicKey: "pk1", Content: "desc1"}, - }, - }, nil -} - -func (m *mockGraphService) CheckExistence(_ context.Context, _ *pb.CheckGraphEntryRequest) (*pb.GraphExistsResponse, error) { - return &pb.GraphExistsResponse{Exists: true}, nil -} - -func (m *mockGraphService) GetCost(_ context.Context, _ *pb.GraphEntryCostRequest) (*pb.Cost, error) { - return &pb.Cost{AttoTokens: "500"}, nil -} - // mockFileService implements pb.FileServiceServer. type mockFileService struct { pb.UnimplementedFileServiceServer @@ -180,7 +149,6 @@ func startMockServer(t *testing.T) *GrpcClient { pb.RegisterHealthServiceServer(s, &mockHealthService{}) pb.RegisterDataServiceServer(s, &mockDataService{}) pb.RegisterChunkServiceServer(s, &mockChunkService{}) - pb.RegisterGraphServiceServer(s, &mockGraphService{}) pb.RegisterFileServiceServer(s, &mockFileService{}) go func() { @@ -240,7 +208,7 @@ func startErrorServer(t *testing.T, code codes.Code, msg string) *GrpcClient { return c } -// --- Tests for all 19 gRPC methods --- +// --- Tests for all gRPC methods --- func TestGrpcHealth(t *testing.T) { c := startMockServer(t) @@ -330,53 +298,6 @@ func TestGrpcChunkGet(t *testing.T) { } } -func TestGrpcGraphEntryPut(t *testing.T) { - c := startMockServer(t) - put, err := c.GraphEntryPut(context.Background(), "sk1", []string{}, "abc", []GraphDescendant{}) - if err != nil { - t.Fatal(err) - } - if put.Address != "ge1" || put.Cost != "500" { - t.Fatalf("unexpected graph put: %+v", put) - } -} - -func TestGrpcGraphEntryGet(t *testing.T) { - c := startMockServer(t) - ge, err := c.GraphEntryGet(context.Background(), "ge1") - if err != nil { - t.Fatal(err) - } - if ge.Owner != "owner1" || len(ge.Descendants) != 1 { - t.Fatalf("unexpected graph entry: %+v", ge) - } - if ge.Descendants[0].PublicKey != "pk1" || ge.Descendants[0].Content != "desc1" { - t.Fatalf("unexpected descendant: %+v", ge.Descendants[0]) - } -} - -func TestGrpcGraphEntryExists(t *testing.T) { - c := startMockServer(t) - exists, err := c.GraphEntryExists(context.Background(), "ge1") - if err != nil { - t.Fatal(err) - } - if !exists { - t.Fatal("expected graph entry to exist") - } -} - -func TestGrpcGraphEntryCost(t *testing.T) { - c := startMockServer(t) - cost, err := c.GraphEntryCost(context.Background(), "pk1") - if err != nil { - t.Fatal(err) - } - if cost != "500" { - t.Fatalf("unexpected cost: %s", cost) - } -} - func TestGrpcFileUploadPublic(t *testing.T) { c := startMockServer(t) put, err := c.FileUploadPublic(context.Background(), "/tmp/test.txt") diff --git a/antd-go/models.go b/antd-go/models.go index a719478..56f83eb 100644 --- a/antd-go/models.go +++ b/antd-go/models.go @@ -12,20 +12,6 @@ type PutResult struct { Address string `json:"address"` // hex } -// GraphDescendant is a descendant entry in a graph node. -type GraphDescendant struct { - PublicKey string `json:"public_key"` // hex - Content string `json:"content"` // hex, 32 bytes -} - -// GraphEntry is a DAG node from the network. -type GraphEntry struct { - Owner string `json:"owner"` - Parents []string `json:"parents"` - Content string `json:"content"` - Descendants []GraphDescendant `json:"descendants"` -} - // ArchiveEntry is a single entry in a file archive. type ArchiveEntry struct { Path string `json:"path"` diff --git a/antd-go/proto/antd/v1/graph.pb.go b/antd-go/proto/antd/v1/graph.pb.go deleted file mode 100644 index 2ae91ad..0000000 --- a/antd-go/proto/antd/v1/graph.pb.go +++ /dev/null @@ -1,488 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.11 -// protoc v7.34.0 -// source: antd/v1/graph.proto - -package v1 - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type GetGraphEntryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // hex - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetGraphEntryRequest) Reset() { - *x = GetGraphEntryRequest{} - mi := &file_antd_v1_graph_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetGraphEntryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetGraphEntryRequest) ProtoMessage() {} - -func (x *GetGraphEntryRequest) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetGraphEntryRequest.ProtoReflect.Descriptor instead. -func (*GetGraphEntryRequest) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{0} -} - -func (x *GetGraphEntryRequest) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - -type GetGraphEntryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` - Parents []string `protobuf:"bytes,2,rep,name=parents,proto3" json:"parents,omitempty"` - Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` // hex, 32 bytes - Descendants []*GraphDescendant `protobuf:"bytes,4,rep,name=descendants,proto3" json:"descendants,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GetGraphEntryResponse) Reset() { - *x = GetGraphEntryResponse{} - mi := &file_antd_v1_graph_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GetGraphEntryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetGraphEntryResponse) ProtoMessage() {} - -func (x *GetGraphEntryResponse) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetGraphEntryResponse.ProtoReflect.Descriptor instead. -func (*GetGraphEntryResponse) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{1} -} - -func (x *GetGraphEntryResponse) GetOwner() string { - if x != nil { - return x.Owner - } - return "" -} - -func (x *GetGraphEntryResponse) GetParents() []string { - if x != nil { - return x.Parents - } - return nil -} - -func (x *GetGraphEntryResponse) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -func (x *GetGraphEntryResponse) GetDescendants() []*GraphDescendant { - if x != nil { - return x.Descendants - } - return nil -} - -type CheckGraphEntryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // hex - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CheckGraphEntryRequest) Reset() { - *x = CheckGraphEntryRequest{} - mi := &file_antd_v1_graph_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CheckGraphEntryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CheckGraphEntryRequest) ProtoMessage() {} - -func (x *CheckGraphEntryRequest) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[2] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CheckGraphEntryRequest.ProtoReflect.Descriptor instead. -func (*CheckGraphEntryRequest) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{2} -} - -func (x *CheckGraphEntryRequest) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - -type GraphExistsResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Exists bool `protobuf:"varint,1,opt,name=exists,proto3" json:"exists,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GraphExistsResponse) Reset() { - *x = GraphExistsResponse{} - mi := &file_antd_v1_graph_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GraphExistsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GraphExistsResponse) ProtoMessage() {} - -func (x *GraphExistsResponse) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[3] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GraphExistsResponse.ProtoReflect.Descriptor instead. -func (*GraphExistsResponse) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{3} -} - -func (x *GraphExistsResponse) GetExists() bool { - if x != nil { - return x.Exists - } - return false -} - -type PutGraphEntryRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - OwnerSecretKey string `protobuf:"bytes,1,opt,name=owner_secret_key,json=ownerSecretKey,proto3" json:"owner_secret_key,omitempty"` // hex - Parents []string `protobuf:"bytes,2,rep,name=parents,proto3" json:"parents,omitempty"` // hex public keys - Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` // hex, 32 bytes - Descendants []*GraphDescendant `protobuf:"bytes,4,rep,name=descendants,proto3" json:"descendants,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PutGraphEntryRequest) Reset() { - *x = PutGraphEntryRequest{} - mi := &file_antd_v1_graph_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PutGraphEntryRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PutGraphEntryRequest) ProtoMessage() {} - -func (x *PutGraphEntryRequest) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[4] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PutGraphEntryRequest.ProtoReflect.Descriptor instead. -func (*PutGraphEntryRequest) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{4} -} - -func (x *PutGraphEntryRequest) GetOwnerSecretKey() string { - if x != nil { - return x.OwnerSecretKey - } - return "" -} - -func (x *PutGraphEntryRequest) GetParents() []string { - if x != nil { - return x.Parents - } - return nil -} - -func (x *PutGraphEntryRequest) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -func (x *PutGraphEntryRequest) GetDescendants() []*GraphDescendant { - if x != nil { - return x.Descendants - } - return nil -} - -type PutGraphEntryResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Cost *Cost `protobuf:"bytes,1,opt,name=cost,proto3" json:"cost,omitempty"` - Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` // hex - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *PutGraphEntryResponse) Reset() { - *x = PutGraphEntryResponse{} - mi := &file_antd_v1_graph_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *PutGraphEntryResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PutGraphEntryResponse) ProtoMessage() {} - -func (x *PutGraphEntryResponse) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[5] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PutGraphEntryResponse.ProtoReflect.Descriptor instead. -func (*PutGraphEntryResponse) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{5} -} - -func (x *PutGraphEntryResponse) GetCost() *Cost { - if x != nil { - return x.Cost - } - return nil -} - -func (x *PutGraphEntryResponse) GetAddress() string { - if x != nil { - return x.Address - } - return "" -} - -type GraphEntryCostRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` // hex - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *GraphEntryCostRequest) Reset() { - *x = GraphEntryCostRequest{} - mi := &file_antd_v1_graph_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *GraphEntryCostRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GraphEntryCostRequest) ProtoMessage() {} - -func (x *GraphEntryCostRequest) ProtoReflect() protoreflect.Message { - mi := &file_antd_v1_graph_proto_msgTypes[6] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GraphEntryCostRequest.ProtoReflect.Descriptor instead. -func (*GraphEntryCostRequest) Descriptor() ([]byte, []int) { - return file_antd_v1_graph_proto_rawDescGZIP(), []int{6} -} - -func (x *GraphEntryCostRequest) GetPublicKey() string { - if x != nil { - return x.PublicKey - } - return "" -} - -var File_antd_v1_graph_proto protoreflect.FileDescriptor - -const file_antd_v1_graph_proto_rawDesc = "" + - "\n" + - "\x13antd/v1/graph.proto\x12\aantd.v1\x1a\x14antd/v1/common.proto\"0\n" + - "\x14GetGraphEntryRequest\x12\x18\n" + - "\aaddress\x18\x01 \x01(\tR\aaddress\"\x9d\x01\n" + - "\x15GetGraphEntryResponse\x12\x14\n" + - "\x05owner\x18\x01 \x01(\tR\x05owner\x12\x18\n" + - "\aparents\x18\x02 \x03(\tR\aparents\x12\x18\n" + - "\acontent\x18\x03 \x01(\tR\acontent\x12:\n" + - "\vdescendants\x18\x04 \x03(\v2\x18.antd.v1.GraphDescendantR\vdescendants\"2\n" + - "\x16CheckGraphEntryRequest\x12\x18\n" + - "\aaddress\x18\x01 \x01(\tR\aaddress\"-\n" + - "\x13GraphExistsResponse\x12\x16\n" + - "\x06exists\x18\x01 \x01(\bR\x06exists\"\xb0\x01\n" + - "\x14PutGraphEntryRequest\x12(\n" + - "\x10owner_secret_key\x18\x01 \x01(\tR\x0eownerSecretKey\x12\x18\n" + - "\aparents\x18\x02 \x03(\tR\aparents\x12\x18\n" + - "\acontent\x18\x03 \x01(\tR\acontent\x12:\n" + - "\vdescendants\x18\x04 \x03(\v2\x18.antd.v1.GraphDescendantR\vdescendants\"T\n" + - "\x15PutGraphEntryResponse\x12!\n" + - "\x04cost\x18\x01 \x01(\v2\r.antd.v1.CostR\x04cost\x12\x18\n" + - "\aaddress\x18\x02 \x01(\tR\aaddress\"6\n" + - "\x15GraphEntryCostRequest\x12\x1d\n" + - "\n" + - "public_key\x18\x01 \x01(\tR\tpublicKey2\xa5\x02\n" + - "\fGraphService\x12D\n" + - "\x03Get\x12\x1d.antd.v1.GetGraphEntryRequest\x1a\x1e.antd.v1.GetGraphEntryResponse\x12O\n" + - "\x0eCheckExistence\x12\x1f.antd.v1.CheckGraphEntryRequest\x1a\x1c.antd.v1.GraphExistsResponse\x12D\n" + - "\x03Put\x12\x1d.antd.v1.PutGraphEntryRequest\x1a\x1e.antd.v1.PutGraphEntryResponse\x128\n" + - "\aGetCost\x12\x1e.antd.v1.GraphEntryCostRequest\x1a\r.antd.v1.CostB\n" + - "\xaa\x02\aAntd.V1b\x06proto3" - -var ( - file_antd_v1_graph_proto_rawDescOnce sync.Once - file_antd_v1_graph_proto_rawDescData []byte -) - -func file_antd_v1_graph_proto_rawDescGZIP() []byte { - file_antd_v1_graph_proto_rawDescOnce.Do(func() { - file_antd_v1_graph_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_antd_v1_graph_proto_rawDesc), len(file_antd_v1_graph_proto_rawDesc))) - }) - return file_antd_v1_graph_proto_rawDescData -} - -var file_antd_v1_graph_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_antd_v1_graph_proto_goTypes = []any{ - (*GetGraphEntryRequest)(nil), // 0: antd.v1.GetGraphEntryRequest - (*GetGraphEntryResponse)(nil), // 1: antd.v1.GetGraphEntryResponse - (*CheckGraphEntryRequest)(nil), // 2: antd.v1.CheckGraphEntryRequest - (*GraphExistsResponse)(nil), // 3: antd.v1.GraphExistsResponse - (*PutGraphEntryRequest)(nil), // 4: antd.v1.PutGraphEntryRequest - (*PutGraphEntryResponse)(nil), // 5: antd.v1.PutGraphEntryResponse - (*GraphEntryCostRequest)(nil), // 6: antd.v1.GraphEntryCostRequest - (*GraphDescendant)(nil), // 7: antd.v1.GraphDescendant - (*Cost)(nil), // 8: antd.v1.Cost -} -var file_antd_v1_graph_proto_depIdxs = []int32{ - 7, // 0: antd.v1.GetGraphEntryResponse.descendants:type_name -> antd.v1.GraphDescendant - 7, // 1: antd.v1.PutGraphEntryRequest.descendants:type_name -> antd.v1.GraphDescendant - 8, // 2: antd.v1.PutGraphEntryResponse.cost:type_name -> antd.v1.Cost - 0, // 3: antd.v1.GraphService.Get:input_type -> antd.v1.GetGraphEntryRequest - 2, // 4: antd.v1.GraphService.CheckExistence:input_type -> antd.v1.CheckGraphEntryRequest - 4, // 5: antd.v1.GraphService.Put:input_type -> antd.v1.PutGraphEntryRequest - 6, // 6: antd.v1.GraphService.GetCost:input_type -> antd.v1.GraphEntryCostRequest - 1, // 7: antd.v1.GraphService.Get:output_type -> antd.v1.GetGraphEntryResponse - 3, // 8: antd.v1.GraphService.CheckExistence:output_type -> antd.v1.GraphExistsResponse - 5, // 9: antd.v1.GraphService.Put:output_type -> antd.v1.PutGraphEntryResponse - 8, // 10: antd.v1.GraphService.GetCost:output_type -> antd.v1.Cost - 7, // [7:11] is the sub-list for method output_type - 3, // [3:7] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name -} - -func init() { file_antd_v1_graph_proto_init() } -func file_antd_v1_graph_proto_init() { - if File_antd_v1_graph_proto != nil { - return - } - file_antd_v1_common_proto_init() - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_antd_v1_graph_proto_rawDesc), len(file_antd_v1_graph_proto_rawDesc)), - NumEnums: 0, - NumMessages: 7, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_antd_v1_graph_proto_goTypes, - DependencyIndexes: file_antd_v1_graph_proto_depIdxs, - MessageInfos: file_antd_v1_graph_proto_msgTypes, - }.Build() - File_antd_v1_graph_proto = out.File - file_antd_v1_graph_proto_goTypes = nil - file_antd_v1_graph_proto_depIdxs = nil -} diff --git a/antd-go/proto/antd/v1/graph_grpc.pb.go b/antd-go/proto/antd/v1/graph_grpc.pb.go deleted file mode 100644 index e18d0a0..0000000 --- a/antd-go/proto/antd/v1/graph_grpc.pb.go +++ /dev/null @@ -1,235 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.6.1 -// - protoc v7.34.0 -// source: antd/v1/graph.proto - -package v1 - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.64.0 or later. -const _ = grpc.SupportPackageIsVersion9 - -const ( - GraphService_Get_FullMethodName = "/antd.v1.GraphService/Get" - GraphService_CheckExistence_FullMethodName = "/antd.v1.GraphService/CheckExistence" - GraphService_Put_FullMethodName = "/antd.v1.GraphService/Put" - GraphService_GetCost_FullMethodName = "/antd.v1.GraphService/GetCost" -) - -// GraphServiceClient is the client API for GraphService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type GraphServiceClient interface { - Get(ctx context.Context, in *GetGraphEntryRequest, opts ...grpc.CallOption) (*GetGraphEntryResponse, error) - CheckExistence(ctx context.Context, in *CheckGraphEntryRequest, opts ...grpc.CallOption) (*GraphExistsResponse, error) - Put(ctx context.Context, in *PutGraphEntryRequest, opts ...grpc.CallOption) (*PutGraphEntryResponse, error) - GetCost(ctx context.Context, in *GraphEntryCostRequest, opts ...grpc.CallOption) (*Cost, error) -} - -type graphServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewGraphServiceClient(cc grpc.ClientConnInterface) GraphServiceClient { - return &graphServiceClient{cc} -} - -func (c *graphServiceClient) Get(ctx context.Context, in *GetGraphEntryRequest, opts ...grpc.CallOption) (*GetGraphEntryResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetGraphEntryResponse) - err := c.cc.Invoke(ctx, GraphService_Get_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *graphServiceClient) CheckExistence(ctx context.Context, in *CheckGraphEntryRequest, opts ...grpc.CallOption) (*GraphExistsResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GraphExistsResponse) - err := c.cc.Invoke(ctx, GraphService_CheckExistence_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *graphServiceClient) Put(ctx context.Context, in *PutGraphEntryRequest, opts ...grpc.CallOption) (*PutGraphEntryResponse, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(PutGraphEntryResponse) - err := c.cc.Invoke(ctx, GraphService_Put_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *graphServiceClient) GetCost(ctx context.Context, in *GraphEntryCostRequest, opts ...grpc.CallOption) (*Cost, error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Cost) - err := c.cc.Invoke(ctx, GraphService_GetCost_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -// GraphServiceServer is the server API for GraphService service. -// All implementations must embed UnimplementedGraphServiceServer -// for forward compatibility. -type GraphServiceServer interface { - Get(context.Context, *GetGraphEntryRequest) (*GetGraphEntryResponse, error) - CheckExistence(context.Context, *CheckGraphEntryRequest) (*GraphExistsResponse, error) - Put(context.Context, *PutGraphEntryRequest) (*PutGraphEntryResponse, error) - GetCost(context.Context, *GraphEntryCostRequest) (*Cost, error) - mustEmbedUnimplementedGraphServiceServer() -} - -// UnimplementedGraphServiceServer must be embedded to have -// forward compatible implementations. -// -// NOTE: this should be embedded by value instead of pointer to avoid a nil -// pointer dereference when methods are called. -type UnimplementedGraphServiceServer struct{} - -func (UnimplementedGraphServiceServer) Get(context.Context, *GetGraphEntryRequest) (*GetGraphEntryResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Get not implemented") -} -func (UnimplementedGraphServiceServer) CheckExistence(context.Context, *CheckGraphEntryRequest) (*GraphExistsResponse, error) { - return nil, status.Error(codes.Unimplemented, "method CheckExistence not implemented") -} -func (UnimplementedGraphServiceServer) Put(context.Context, *PutGraphEntryRequest) (*PutGraphEntryResponse, error) { - return nil, status.Error(codes.Unimplemented, "method Put not implemented") -} -func (UnimplementedGraphServiceServer) GetCost(context.Context, *GraphEntryCostRequest) (*Cost, error) { - return nil, status.Error(codes.Unimplemented, "method GetCost not implemented") -} -func (UnimplementedGraphServiceServer) mustEmbedUnimplementedGraphServiceServer() {} -func (UnimplementedGraphServiceServer) testEmbeddedByValue() {} - -// UnsafeGraphServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to GraphServiceServer will -// result in compilation errors. -type UnsafeGraphServiceServer interface { - mustEmbedUnimplementedGraphServiceServer() -} - -func RegisterGraphServiceServer(s grpc.ServiceRegistrar, srv GraphServiceServer) { - // If the following call panics, it indicates UnimplementedGraphServiceServer was - // embedded by pointer and is nil. This will cause panics if an - // unimplemented method is ever invoked, so we test this at initialization - // time to prevent it from happening at runtime later due to I/O. - if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { - t.testEmbeddedByValue() - } - s.RegisterService(&GraphService_ServiceDesc, srv) -} - -func _GraphService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetGraphEntryRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GraphServiceServer).Get(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: GraphService_Get_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GraphServiceServer).Get(ctx, req.(*GetGraphEntryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _GraphService_CheckExistence_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(CheckGraphEntryRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GraphServiceServer).CheckExistence(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: GraphService_CheckExistence_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GraphServiceServer).CheckExistence(ctx, req.(*CheckGraphEntryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _GraphService_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(PutGraphEntryRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GraphServiceServer).Put(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: GraphService_Put_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GraphServiceServer).Put(ctx, req.(*PutGraphEntryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _GraphService_GetCost_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GraphEntryCostRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GraphServiceServer).GetCost(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: GraphService_GetCost_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GraphServiceServer).GetCost(ctx, req.(*GraphEntryCostRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// GraphService_ServiceDesc is the grpc.ServiceDesc for GraphService service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var GraphService_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "antd.v1.GraphService", - HandlerType: (*GraphServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Get", - Handler: _GraphService_Get_Handler, - }, - { - MethodName: "CheckExistence", - Handler: _GraphService_CheckExistence_Handler, - }, - { - MethodName: "Put", - Handler: _GraphService_Put_Handler, - }, - { - MethodName: "GetCost", - Handler: _GraphService_GetCost_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "antd/v1/graph.proto", -} diff --git a/antd-java/README.md b/antd-java/README.md index 8ac5e90..58f09d2 100644 --- a/antd-java/README.md +++ b/antd-java/README.md @@ -109,15 +109,6 @@ All methods throw `AntdException` (or a typed subclass) on failure. | `chunkPut(data)` | Store a raw chunk | | `chunkGet(address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) - -| Method | Description | -|--------|-------------| -| `graphEntryPut(secretKey, parents, content, descendants)` | Create entry | -| `graphEntryGet(address)` | Read entry | -| `graphEntryExists(address)` | Check if exists | -| `graphEntryCost(publicKey)` | Estimate creation cost | - ### Files & Directories | Method | Description | @@ -174,11 +165,11 @@ var client = new AsyncAntdClient("http://custom:9090"); // var client = new AsyncAntdClient("http://localhost:8082", Duration.ofSeconds(30)); // custom timeout ``` -All 19 methods follow the naming convention `methodNameAsync()` and return `CompletableFuture` where `T` matches the sync return type. Void methods return `CompletableFuture`. +All 15 methods follow the naming convention `methodNameAsync()` and return `CompletableFuture` where `T` matches the sync return type. Void methods return `CompletableFuture`. ## gRPC Transport -The `GrpcAntdClient` provides an alternative transport using gRPC instead of REST. It implements the same 19 methods with identical signatures, so switching transports requires only changing the constructor. +The `GrpcAntdClient` provides an alternative transport using gRPC instead of REST. It implements the same 15 methods with identical signatures, so switching transports requires only changing the constructor. ```java import com.autonomi.antd.GrpcAntdClient; @@ -265,7 +256,6 @@ See the [examples/](examples/) directory: - `Example01Connect` — Health check - `Example02PublicData` — Public data storage and retrieval - `Example03Files` — File upload and download -- `Example04GraphEntries` — Graph entry (DAG node) operations - `Example05ErrorHandling` — Typed exception handling - `Example06PrivateData` — Private (encrypted) data storage diff --git a/antd-java/examples/Example04GraphEntries.java b/antd-java/examples/Example04GraphEntries.java deleted file mode 100644 index 4442f41..0000000 --- a/antd-java/examples/Example04GraphEntries.java +++ /dev/null @@ -1,38 +0,0 @@ -import com.autonomi.antd.AntdClient; -import com.autonomi.antd.models.GraphDescendant; -import com.autonomi.antd.models.GraphEntry; -import com.autonomi.antd.models.PutResult; - -import java.util.Collections; -import java.util.List; - -/** - * Example 04 — Create and read graph entries (DAG nodes). - */ -public class Example04GraphEntries { - public static void main(String[] args) { - try (var client = new AntdClient()) { - // Create a graph entry - PutResult result = client.graphEntryPut( - "your-secret-key-hex", - Collections.emptyList(), - "content-hex-32-bytes", - List.of(new GraphDescendant("descendant-public-key", "descendant-content"))); - System.out.println("Created at: " + result.address()); - - // Read it back - GraphEntry entry = client.graphEntryGet(result.address()); - System.out.println("Owner: " + entry.owner()); - System.out.println("Content: " + entry.content()); - System.out.println("Descendants: " + entry.descendants().size()); - - // Check existence - boolean exists = client.graphEntryExists(result.address()); - System.out.println("Exists: " + exists); - - // Estimate cost - String cost = client.graphEntryCost("your-public-key-hex"); - System.out.println("Cost estimate: " + cost + " atto"); - } - } -} diff --git a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java index 8b2e287..8d1b9ee 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AntdClient.java @@ -251,47 +251,6 @@ public byte[] chunkGet(String address) { return b64Decode(str(j, "data")); } - // ── Graph Entries (DAG Nodes) ── - - public PutResult graphEntryPut(String ownerSecretKey, List parents, - String content, List descendants) { - List> descs = new ArrayList<>(); - for (GraphDescendant d : descendants) { - descs.add(Map.of("public_key", d.publicKey(), "content", d.content())); - } - String body = Json.object( - "owner_secret_key", ownerSecretKey, - "parents", parents, - "content", content, - "descendants", descs - ); - Map j = doJson("POST", "/v1/graph", body); - return new PutResult(str(j, "cost"), str(j, "address")); - } - - public GraphEntry graphEntryGet(String address) { - Map j = doJson("GET", "/v1/graph/" + address, null); - List descs = new ArrayList<>(); - for (Map dm : listOfMaps(j, "descendants")) { - descs.add(new GraphDescendant(str(dm, "public_key"), str(dm, "content"))); - } - return new GraphEntry(str(j, "owner"), strList(j, "parents"), str(j, "content"), - Collections.unmodifiableList(descs)); - } - - public boolean graphEntryExists(String address) { - int code = doHead("/v1/graph/" + address); - if (code == 404) return false; - if (code >= 300) throw ExceptionFactory.fromHttpStatus(code, "graph entry exists check failed"); - return true; - } - - public String graphEntryCost(String publicKey) { - String body = Json.object("public_key", publicKey); - Map j = doJson("POST", "/v1/graph/cost", body); - return str(j, "cost"); - } - // ── Files & Directories ── public PutResult fileUploadPublic(String path) { diff --git a/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java index 0543f90..3a24ff5 100644 --- a/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java @@ -228,55 +228,6 @@ public CompletableFuture chunkGetAsync(String address) { .thenApply(j -> b64Decode(str(j, "data"))); } - // ── Graph Entries (DAG Nodes) ── - - /** Async variant of {@link AntdClient#graphEntryPut(String, List, String, List)}. */ - public CompletableFuture graphEntryPutAsync(String ownerSecretKey, List parents, - String content, List descendants) { - List> descs = new ArrayList<>(); - for (GraphDescendant d : descendants) { - descs.add(Map.of("public_key", d.publicKey(), "content", d.content())); - } - String body = Json.object( - "owner_secret_key", ownerSecretKey, - "parents", parents, - "content", content, - "descendants", descs - ); - return doJsonAsync("POST", "/v1/graph", body) - .thenApply(j -> new PutResult(str(j, "cost"), str(j, "address"))); - } - - /** Async variant of {@link AntdClient#graphEntryGet(String)}. */ - public CompletableFuture graphEntryGetAsync(String address) { - return doJsonAsync("GET", "/v1/graph/" + address, null) - .thenApply(j -> { - List descs = new ArrayList<>(); - for (Map dm : listOfMaps(j, "descendants")) { - descs.add(new GraphDescendant(str(dm, "public_key"), str(dm, "content"))); - } - return new GraphEntry(str(j, "owner"), strList(j, "parents"), str(j, "content"), - Collections.unmodifiableList(descs)); - }); - } - - /** Async variant of {@link AntdClient#graphEntryExists(String)}. */ - public CompletableFuture graphEntryExistsAsync(String address) { - return doHeadAsync("/v1/graph/" + address) - .thenApply(code -> { - if (code == 404) return false; - if (code >= 300) throw ExceptionFactory.fromHttpStatus(code, "graph entry exists check failed"); - return true; - }); - } - - /** Async variant of {@link AntdClient#graphEntryCost(String)}. */ - public CompletableFuture graphEntryCostAsync(String publicKey) { - String body = Json.object("public_key", publicKey); - return doJsonAsync("POST", "/v1/graph/cost", body) - .thenApply(j -> str(j, "cost")); - } - // ── Files & Directories ── /** Async variant of {@link AntdClient#fileUploadPublic(String)}. */ diff --git a/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java b/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java index 37d2c2a..745d50b 100644 --- a/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java +++ b/antd-java/src/main/java/com/autonomi/antd/GrpcAntdClient.java @@ -28,15 +28,6 @@ import antd.v1.Chunks.PutChunkRequest; import antd.v1.Chunks.PutChunkResponse; -import antd.v1.GraphServiceGrpc; -import antd.v1.Graph.GetGraphEntryRequest; -import antd.v1.Graph.GetGraphEntryResponse; -import antd.v1.Graph.CheckGraphEntryRequest; -import antd.v1.Graph.GraphExistsResponse; -import antd.v1.Graph.PutGraphEntryRequest; -import antd.v1.Graph.PutGraphEntryResponse; -import antd.v1.Graph.GraphEntryCostRequest; - import antd.v1.FileServiceGrpc; import antd.v1.Files.UploadFileRequest; import antd.v1.Files.UploadPublicResponse; @@ -60,7 +51,7 @@ * gRPC client for the antd daemon — the gateway to the Autonomi decentralized network. * *

    Uses {@code io.grpc} blocking stubs for synchronous calls. Implements the same - * 19 methods as {@link AntdClient} but communicates over gRPC instead of REST. + * 15 methods as {@link AntdClient} but communicates over gRPC instead of REST. * *

    Implements {@link AutoCloseable} so it can be used in try-with-resources blocks. * @@ -83,7 +74,6 @@ public class GrpcAntdClient implements AutoCloseable { private final HealthServiceGrpc.HealthServiceBlockingStub healthStub; private final DataServiceGrpc.DataServiceBlockingStub dataStub; private final ChunkServiceGrpc.ChunkServiceBlockingStub chunkStub; - private final GraphServiceGrpc.GraphServiceBlockingStub graphStub; private final FileServiceGrpc.FileServiceBlockingStub fileStub; /** @@ -127,7 +117,6 @@ public GrpcAntdClient(ManagedChannel channel) { this.healthStub = HealthServiceGrpc.newBlockingStub(channel); this.dataStub = DataServiceGrpc.newBlockingStub(channel); this.chunkStub = ChunkServiceGrpc.newBlockingStub(channel); - this.graphStub = GraphServiceGrpc.newBlockingStub(channel); this.fileStub = FileServiceGrpc.newBlockingStub(channel); } @@ -288,88 +277,6 @@ public byte[] chunkGet(String address) { } } - // ── Graph Entries (DAG Nodes) ── - - /** - * Create a graph entry (DAG node). - */ - public PutResult graphEntryPut(String ownerSecretKey, List parents, - String content, List descendants) { - try { - PutGraphEntryRequest.Builder builder = PutGraphEntryRequest.newBuilder() - .setOwnerSecretKey(ownerSecretKey) - .addAllParents(parents) - .setContent(content); - for (GraphDescendant d : descendants) { - builder.addDescendants(antd.v1.Common.GraphDescendant.newBuilder() - .setPublicKey(d.publicKey()) - .setContent(d.content()) - .build()); - } - PutGraphEntryResponse resp = graphStub.put(builder.build()); - return new PutResult(resp.getCost().getAttoTokens(), resp.getAddress()); - } catch (StatusRuntimeException e) { - throw mapException(e); - } - } - - /** - * Read a graph entry by address. - */ - public GraphEntry graphEntryGet(String address) { - try { - GetGraphEntryRequest req = GetGraphEntryRequest.newBuilder() - .setAddress(address) - .build(); - GetGraphEntryResponse resp = graphStub.get(req); - List descs = new ArrayList<>(); - for (antd.v1.Common.GraphDescendant d : resp.getDescendantsList()) { - descs.add(new GraphDescendant(d.getPublicKey(), d.getContent())); - } - return new GraphEntry( - resp.getOwner(), - Collections.unmodifiableList(resp.getParentsList()), - resp.getContent(), - Collections.unmodifiableList(descs) - ); - } catch (StatusRuntimeException e) { - throw mapException(e); - } - } - - /** - * Check if a graph entry exists. - */ - public boolean graphEntryExists(String address) { - try { - CheckGraphEntryRequest req = CheckGraphEntryRequest.newBuilder() - .setAddress(address) - .build(); - GraphExistsResponse resp = graphStub.checkExistence(req); - return resp.getExists(); - } catch (StatusRuntimeException e) { - if (e.getStatus().getCode() == io.grpc.Status.Code.NOT_FOUND) { - return false; - } - throw mapException(e); - } - } - - /** - * Estimate the cost of creating a graph entry. - */ - public String graphEntryCost(String publicKey) { - try { - GraphEntryCostRequest req = GraphEntryCostRequest.newBuilder() - .setPublicKey(publicKey) - .build(); - Cost resp = graphStub.getCost(req); - return resp.getAttoTokens(); - } catch (StatusRuntimeException e) { - throw mapException(e); - } - } - // ── Files & Directories ── /** diff --git a/antd-java/src/main/java/com/autonomi/antd/models/GraphDescendant.java b/antd-java/src/main/java/com/autonomi/antd/models/GraphDescendant.java deleted file mode 100644 index 2050161..0000000 --- a/antd-java/src/main/java/com/autonomi/antd/models/GraphDescendant.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.autonomi.antd.models; - -/** - * A descendant entry in a graph node. - * - * @param publicKey hex-encoded public key - * @param content hex-encoded content (32 bytes) - */ -public record GraphDescendant(String publicKey, String content) {} diff --git a/antd-java/src/main/java/com/autonomi/antd/models/GraphEntry.java b/antd-java/src/main/java/com/autonomi/antd/models/GraphEntry.java deleted file mode 100644 index 48b0c1a..0000000 --- a/antd-java/src/main/java/com/autonomi/antd/models/GraphEntry.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.autonomi.antd.models; - -import java.util.List; - -/** - * A DAG node from the network. - * - * @param owner the owner public key - * @param parents list of parent addresses - * @param content hex-encoded content - * @param descendants list of descendant entries - */ -public record GraphEntry( - String owner, - List parents, - String content, - List descendants) {} diff --git a/antd-java/src/test/java/com/autonomi/antd/AntdClientTest.java b/antd-java/src/test/java/com/autonomi/antd/AntdClientTest.java index 5562c99..80b0764 100644 --- a/antd-java/src/test/java/com/autonomi/antd/AntdClientTest.java +++ b/antd-java/src/test/java/com/autonomi/antd/AntdClientTest.java @@ -82,21 +82,6 @@ public MockResponse dispatch(RecordedRequest request) { return json("{\"data\":\"" + b64("chunkdata") + "\"}"); } - // Graph - if ("POST".equals(method) && "/v1/graph".equals(path)) { - return json("{\"cost\":\"500\",\"address\":\"ge1\"}"); - } - if ("GET".equals(method) && "/v1/graph/ge1".equals(path)) { - return json("{\"owner\":\"owner1\",\"parents\":[],\"content\":\"abc\"," - + "\"descendants\":[{\"public_key\":\"pk1\",\"content\":\"desc1\"}]}"); - } - if ("HEAD".equals(method) && "/v1/graph/ge1".equals(path)) { - return new MockResponse().setResponseCode(200); - } - if ("POST".equals(method) && "/v1/graph/cost".equals(path)) { - return json("{\"cost\":\"500\"}"); - } - // Files if ("POST".equals(method) && "/v1/files/upload/public".equals(path)) { return json("{\"cost\":\"1000\",\"address\":\"file1\"}"); @@ -185,34 +170,6 @@ void testChunks() { assertEquals("chunkdata", new String(data)); } - @Test - void testGraphEntryPut() { - PutResult put = client.graphEntryPut("sk1", Collections.emptyList(), "abc", - Collections.emptyList()); - assertEquals("ge1", put.address()); - assertEquals("500", put.cost()); - } - - @Test - void testGraphEntryGet() { - GraphEntry ge = client.graphEntryGet("ge1"); - assertEquals("owner1", ge.owner()); - assertEquals(1, ge.descendants().size()); - assertEquals("pk1", ge.descendants().get(0).publicKey()); - assertEquals("desc1", ge.descendants().get(0).content()); - } - - @Test - void testGraphEntryExists() { - assertTrue(client.graphEntryExists("ge1")); - } - - @Test - void testGraphEntryCost() { - String cost = client.graphEntryCost("pk1"); - assertEquals("500", cost); - } - @Test void testFileUploadPublic() { PutResult put = client.fileUploadPublic("/tmp/test.txt"); diff --git a/antd-java/src/test/java/com/autonomi/antd/GrpcAntdClientTest.java b/antd-java/src/test/java/com/autonomi/antd/GrpcAntdClientTest.java index ef86706..72b706e 100644 --- a/antd-java/src/test/java/com/autonomi/antd/GrpcAntdClientTest.java +++ b/antd-java/src/test/java/com/autonomi/antd/GrpcAntdClientTest.java @@ -30,15 +30,6 @@ import antd.v1.Chunks.PutChunkRequest; import antd.v1.Chunks.PutChunkResponse; -import antd.v1.GraphServiceGrpc; -import antd.v1.Graph.GetGraphEntryRequest; -import antd.v1.Graph.GetGraphEntryResponse; -import antd.v1.Graph.CheckGraphEntryRequest; -import antd.v1.Graph.GraphExistsResponse; -import antd.v1.Graph.PutGraphEntryRequest; -import antd.v1.Graph.PutGraphEntryResponse; -import antd.v1.Graph.GraphEntryCostRequest; - import antd.v1.FileServiceGrpc; import antd.v1.Files.UploadFileRequest; import antd.v1.Files.UploadPublicResponse; @@ -84,7 +75,6 @@ void setUp() throws Exception { .addService(new MockHealthService()) .addService(new MockDataService()) .addService(new MockChunkService()) - .addService(new MockGraphService()) .addService(new MockFileService()) .build() .start(); @@ -200,57 +190,6 @@ public void get(GetChunkRequest request, } } - static class MockGraphService extends GraphServiceGrpc.GraphServiceImplBase { - @Override - public void put(PutGraphEntryRequest request, - StreamObserver responseObserver) { - responseObserver.onNext( - PutGraphEntryResponse.newBuilder() - .setCost(Cost.newBuilder().setAttoTokens("500").build()) - .setAddress("ge1") - .build()); - responseObserver.onCompleted(); - } - - @Override - public void get(GetGraphEntryRequest request, - StreamObserver responseObserver) { - responseObserver.onNext( - GetGraphEntryResponse.newBuilder() - .setOwner("owner1") - .setContent("abc") - .addDescendants(antd.v1.Common.GraphDescendant.newBuilder() - .setPublicKey("pk1") - .setContent("desc1") - .build()) - .build()); - responseObserver.onCompleted(); - } - - @Override - public void checkExistence(CheckGraphEntryRequest request, - StreamObserver responseObserver) { - if ("ge1".equals(request.getAddress())) { - responseObserver.onNext( - GraphExistsResponse.newBuilder() - .setExists(true) - .build()); - responseObserver.onCompleted(); - } else { - responseObserver.onError( - Status.NOT_FOUND.withDescription("not found").asRuntimeException()); - } - } - - @Override - public void getCost(GraphEntryCostRequest request, - StreamObserver responseObserver) { - responseObserver.onNext( - Cost.newBuilder().setAttoTokens("500").build()); - responseObserver.onCompleted(); - } - } - static class MockFileService extends FileServiceGrpc.FileServiceImplBase { @Override public void uploadPublic(UploadFileRequest request, @@ -325,7 +264,7 @@ public void getFileCost(FileCostRequest request, } // ========================================================================= - // Tests — 19 methods + // Tests — 15 methods // ========================================================================= // --- Health --- @@ -386,42 +325,6 @@ void testChunkGet() { assertEquals("chunkdata", new String(data)); } - // --- Graph Entries --- - - @Test - void testGraphEntryPut() { - PutResult put = client.graphEntryPut("sk1", Collections.emptyList(), "abc", - Collections.emptyList()); - assertEquals("ge1", put.address()); - assertEquals("500", put.cost()); - } - - @Test - void testGraphEntryGet() { - GraphEntry ge = client.graphEntryGet("ge1"); - assertEquals("owner1", ge.owner()); - assertEquals("abc", ge.content()); - assertEquals(1, ge.descendants().size()); - assertEquals("pk1", ge.descendants().get(0).publicKey()); - assertEquals("desc1", ge.descendants().get(0).content()); - } - - @Test - void testGraphEntryExists() { - assertTrue(client.graphEntryExists("ge1")); - } - - @Test - void testGraphEntryExistsNotFound() { - assertFalse(client.graphEntryExists("nonexistent")); - } - - @Test - void testGraphEntryCost() { - String cost = client.graphEntryCost("pk1"); - assertEquals("500", cost); - } - // --- Files & Directories --- @Test diff --git a/antd-js/README.md b/antd-js/README.md index f92f632..3da7996 100644 --- a/antd-js/README.md +++ b/antd-js/README.md @@ -75,15 +75,6 @@ All methods are `async` and return Promises. | `chunkPut(data)` | `PutResult` | Store raw chunk | | `chunkGet(address)` | `Buffer` | Retrieve chunk by address | -### Graph - -| Method | Returns | Description | -|--------|---------|-------------| -| `graphEntryPut(ownerSecretKey, parents, content, descendants)` | `PutResult` | Create graph entry | -| `graphEntryGet(address)` | `GraphEntry` | Read graph entry | -| `graphEntryExists(address)` | `boolean` | Check existence | -| `graphEntryCost(publicKey)` | `string` | Estimate creation cost | - ### Files | Method | Returns | Description | @@ -101,8 +92,6 @@ All methods are `async` and return Promises. ```typescript interface HealthStatus { ok: boolean; network: string } interface PutResult { cost: string; address: string } -interface GraphDescendant { publicKey: string; content: string } -interface GraphEntry { owner: string; parents: string[]; content: string; descendants: GraphDescendant[] } interface ArchiveEntry { path: string; address: string; created: number; modified: number; size: number } interface Archive { entries: ArchiveEntry[] } ``` @@ -146,7 +135,6 @@ The `examples/` directory contains 6 runnable scripts covering all major feature | `02-data.ts` | Public data store/retrieve | | `03-chunks.ts` | Raw chunk operations | | `04-files.ts` | File upload/download | -| `05-graph.ts` | Graph entry operations | | `06-private-data.ts` | Private encrypted data | Run examples with [tsx](https://github.com/privatenumber/tsx): diff --git a/antd-js/examples/05-graph.ts b/antd-js/examples/05-graph.ts deleted file mode 100644 index 00e3c94..0000000 --- a/antd-js/examples/05-graph.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Example 05: Graph entry (DAG node) operations. - * - * Graph entries form a directed acyclic graph (DAG) on the network. - * Each entry has an owner, content, parent links, and descendant links. - */ - -import { randomBytes } from "node:crypto"; -import { createClient } from "../src/index.js"; - -const client = createClient(); - -// Generate a random secret key -const secretKey = randomBytes(32).toString("hex"); - -// Create a root graph entry (no parents) -const content = randomBytes(32).toString("hex"); // 32 bytes of content -const result = await client.graphEntryPut(secretKey, [], content, []); -console.log(`Graph entry created at: ${result.address}`); -console.log(`Cost: ${result.cost} atto tokens`); - -// Read the graph entry -const entry = await client.graphEntryGet(result.address); -console.log(`Owner: ${entry.owner}`); -console.log(`Content: ${entry.content}`); -console.log(`Parents: ${JSON.stringify(entry.parents)}`); -console.log(`Descendants: ${entry.descendants.length}`); - -// Check existence -const exists = await client.graphEntryExists(result.address); -console.log(`Graph entry exists: ${exists}`); - -// Estimate cost for another entry -const cost = await client.graphEntryCost(secretKey); -console.log(`Cost estimate for new entry: ${cost} atto tokens`); - -console.log("Graph entry operations OK!"); diff --git a/antd-js/src/index.ts b/antd-js/src/index.ts index bd1b53c..7e1cd8c 100644 --- a/antd-js/src/index.ts +++ b/antd-js/src/index.ts @@ -1,8 +1,6 @@ export type { HealthStatus, PutResult, - GraphDescendant, - GraphEntry, ArchiveEntry, Archive, WalletAddress, diff --git a/antd-js/src/models.ts b/antd-js/src/models.ts index e39048d..efdd318 100644 --- a/antd-js/src/models.ts +++ b/antd-js/src/models.ts @@ -10,20 +10,6 @@ export interface PutResult { address: string; // hex } -/** A descendant entry in a graph node. */ -export interface GraphDescendant { - publicKey: string; // hex - content: string; // hex, 32 bytes -} - -/** A graph entry from the network. */ -export interface GraphEntry { - owner: string; - parents: string[]; - content: string; - descendants: GraphDescendant[]; -} - /** An entry in a file archive. */ export interface ArchiveEntry { path: string; diff --git a/antd-js/src/rest-client.ts b/antd-js/src/rest-client.ts index 0e62b32..032839f 100644 --- a/antd-js/src/rest-client.ts +++ b/antd-js/src/rest-client.ts @@ -4,8 +4,6 @@ import type { Archive, ArchiveEntry, FinalizeUploadResult, - GraphDescendant, - GraphEntry, HealthStatus, PaymentInfo, PrepareUploadResult, @@ -176,55 +174,6 @@ export class RestClient { return RestClient.unb64(j.data); } - // ---- Graph ---- - - async graphEntryPut( - ownerSecretKey: string, - parents: string[], - content: string, - descendants: GraphDescendant[], - ): Promise { - const j = await this.postJson<{ cost: string; address: string }>("/v1/graph", { - owner_secret_key: ownerSecretKey, - parents, - content, - descendants: descendants.map((d) => ({ - public_key: d.publicKey, - content: d.content, - })), - }); - return { cost: j.cost, address: j.address }; - } - - async graphEntryGet(address: string): Promise { - const j = await this.getJson<{ - owner: string; - parents?: string[]; - content: string; - descendants?: { public_key: string; content: string }[]; - }>(`/v1/graph/${address}`); - return { - owner: j.owner, - parents: j.parents ?? [], - content: j.content, - descendants: (j.descendants ?? []).map((d) => ({ - publicKey: d.public_key, - content: d.content, - })), - }; - } - - async graphEntryExists(address: string): Promise { - return this.headExists(`/v1/graph/${address}`); - } - - async graphEntryCost(publicKey: string): Promise { - const j = await this.postJson<{ cost: string }>("/v1/graph/cost", { - public_key: publicKey, - }); - return j.cost; - } - // ---- Files ---- async fileUploadPublic(path: string, options?: { paymentMode?: string }): Promise { diff --git a/antd-kotlin/README.md b/antd-kotlin/README.md index e861593..0f31569 100644 --- a/antd-kotlin/README.md +++ b/antd-kotlin/README.md @@ -67,7 +67,6 @@ All methods are `suspend` functions for use with Kotlin coroutines. | **Health** | `health()` | | **Data** | `dataPutPublic`, `dataGetPublic`, `dataPutPrivate`, `dataGetPrivate`, `dataCost` | | **Chunks** | `chunkPut`, `chunkGet` | -| **Graph** | `graphEntryPut`, `graphEntryGet`, `graphEntryExists`, `graphEntryCost` | | **Files** | `fileUploadPublic`, `fileDownloadPublic`, `dirUploadPublic`, `dirDownloadPublic`, `archiveGetPublic`, `archivePutPublic`, `fileCost` | ## Error Handling diff --git a/antd-kotlin/examples/src/main/kotlin/com/autonomi/examples/Main.kt b/antd-kotlin/examples/src/main/kotlin/com/autonomi/examples/Main.kt index d35b11b..203a854 100644 --- a/antd-kotlin/examples/src/main/kotlin/com/autonomi/examples/Main.kt +++ b/antd-kotlin/examples/src/main/kotlin/com/autonomi/examples/Main.kt @@ -13,14 +13,12 @@ fun main(args: Array) = runBlocking { "2" -> example02Data() "3" -> example03Chunks() "4" -> example04Files() - "5" -> example05Graph() "6" -> example06PrivateData() "all" -> { example01Connect() example02Data() example03Chunks() example04Files() - example05Graph() example06PrivateData() } else -> println("Unknown example: $example. Use 1-6 or 'all'.") @@ -129,43 +127,6 @@ suspend fun example04Files() { client.close() } -/** Example 05: Graph entry (DAG node) operations. */ -suspend fun example05Graph() { - println("=== Example 05: Graph ===") - val client = AntdClient.createRest() - - val secretKey = randomHex() - - // Create a root graph entry - val content = randomHex() - val result = client.graphEntryPut( - secretKey, - emptyList(), - content, - emptyList(), - ) - println("Graph entry created at: ${result.address}") - println("Cost: ${result.cost} atto tokens") - - // Read - val entry = client.graphEntryGet(result.address) - println("Owner: ${entry.owner}") - println("Content: ${entry.content}") - println("Parents: ${entry.parents.size}") - println("Descendants: ${entry.descendants.size}") - - // Check existence - val exists = client.graphEntryExists(result.address) - println("Graph entry exists: $exists") - - // Estimate cost - val cost = client.graphEntryCost(secretKey) - println("Cost estimate for new entry: $cost atto tokens") - - println("Graph entry operations OK!\n") - client.close() -} - /** Example 06: Private (encrypted) data round-trip. */ suspend fun example06PrivateData() { println("=== Example 06: Private Data ===") diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt index b3c86d3..0ec5682 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdGrpcClient.kt @@ -23,7 +23,6 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { private val healthStub = HealthServiceGrpcKt.HealthServiceCoroutineStub(channel) private val dataStub = DataServiceGrpcKt.DataServiceCoroutineStub(channel) private val chunkStub = ChunkServiceGrpcKt.ChunkServiceCoroutineStub(channel) - private val graphStub = GraphServiceGrpcKt.GraphServiceCoroutineStub(channel) private val fileStub = FileServiceGrpcKt.FileServiceCoroutineStub(channel) override fun close() { @@ -86,43 +85,6 @@ class AntdGrpcClient(target: String = "localhost:50051") : IAntdClient { resp.data.toByteArray() } catch (ex: StatusRuntimeException) { throw wrap(ex) } - // ── Graph ── - - override suspend fun graphEntryPut( - ownerSecretKey: String, - parents: List, - content: String, - descendants: List, - ): PutResult = try { - val resp = graphStub.put(putGraphEntryRequest { - this.ownerSecretKey = ownerSecretKey - this.content = content - this.parents.addAll(parents) - this.descendants.addAll(descendants.map { d -> - graphDescendant { publicKey = d.publicKey; this.content = d.content } - }) - }) - PutResult(resp.cost.attoTokens, resp.address) - } catch (ex: StatusRuntimeException) { throw wrap(ex) } - - override suspend fun graphEntryGet(address: String): GraphEntry = try { - val resp = graphStub.get(getGraphEntryRequest { this.address = address }) - val desc = resp.descendantsList.map { GraphDescendant(it.publicKey, it.content) } - GraphEntry(resp.owner, resp.parentsList, resp.content, desc) - } catch (ex: StatusRuntimeException) { throw wrap(ex) } - - override suspend fun graphEntryExists(address: String): Boolean = try { - val resp = graphStub.checkExistence(checkGraphEntryRequest { this.address = address }) - resp.exists - } catch (ex: StatusRuntimeException) { - if (ex.status.code == Status.Code.NOT_FOUND) false else throw wrap(ex) - } - - override suspend fun graphEntryCost(publicKey: String): String = try { - val resp = graphStub.getCost(graphEntryCostRequest { this.publicKey = publicKey }) - resp.attoTokens - } catch (ex: StatusRuntimeException) { throw wrap(ex) } - // ── Files ── override suspend fun fileUploadPublic(path: String, paymentMode: String?): PutResult = try { diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt index 0d831bf..ee6c369 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/AntdRestClient.kt @@ -150,45 +150,6 @@ class AntdRestClient( return fromB64(resp.data) } - // ── Graph ── - - override suspend fun graphEntryPut( - ownerSecretKey: String, - parents: List, - content: String, - descendants: List, - ): PutResult { - val body = buildJsonObject { - put("owner_secret_key", ownerSecretKey) - putJsonArray("parents") { parents.forEach { add(JsonPrimitive(it)) } } - put("content", content) - putJsonArray("descendants") { - descendants.forEach { d -> - add(buildJsonObject { - put("public_key", d.publicKey) - put("content", d.content) - }) - } - } - }.toString() - val resp = postJson("/v1/graph", body) - return PutResult(resp.cost, resp.address) - } - - override suspend fun graphEntryGet(address: String): GraphEntry { - val resp = getJson("/v1/graph/$address") - val descendants = resp.descendants?.map { GraphDescendant(it.publicKey, it.content) } ?: emptyList() - return GraphEntry(resp.owner, resp.parents ?: emptyList(), resp.content, descendants) - } - - override suspend fun graphEntryExists(address: String): Boolean = headExists("/v1/graph/$address") - - override suspend fun graphEntryCost(publicKey: String): String { - val body = buildJsonObject { put("public_key", publicKey) }.toString() - val resp = postJson("/v1/graph/cost", body) - return resp.cost - } - // ── Files ── override suspend fun fileUploadPublic(path: String, paymentMode: String?): PutResult { diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt index 902b486..e1b58c4 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/IAntdClient.kt @@ -24,12 +24,6 @@ interface IAntdClient : Closeable { suspend fun chunkPut(data: ByteArray): PutResult suspend fun chunkGet(address: String): ByteArray - // Graph - suspend fun graphEntryPut(ownerSecretKey: String, parents: List, content: String, descendants: List): PutResult - suspend fun graphEntryGet(address: String): GraphEntry - suspend fun graphEntryExists(address: String): Boolean - suspend fun graphEntryCost(publicKey: String): String - // Files suspend fun fileUploadPublic(path: String, paymentMode: String? = null): PutResult suspend fun fileDownloadPublic(address: String, destPath: String) diff --git a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt index bf74161..d3355e7 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt @@ -9,12 +9,6 @@ data class HealthStatus(val ok: Boolean, val network: String) /** Result of a put/create operation that stores data on the network. */ data class PutResult(val cost: String, val address: String) -/** A descendant entry in a graph node. */ -data class GraphDescendant(val publicKey: String, val content: String) - -/** A graph entry retrieved from the network. */ -data class GraphEntry(val owner: String, val parents: List, val content: String, val descendants: List) - /** A single entry in an archive manifest. */ data class ArchiveEntry(val path: String, val address: String, val created: ULong, val modified: ULong, val size: ULong) @@ -73,20 +67,6 @@ internal data class CostDto( val cost: String, ) -@Serializable -internal data class GraphDescendantDto( - @SerialName("public_key") val publicKey: String, - val content: String, -) - -@Serializable -internal data class GraphEntryDto( - val owner: String, - val parents: List? = null, - val content: String, - val descendants: List? = null, -) - @Serializable internal data class ArchiveEntryDto( val path: String, diff --git a/antd-kotlin/lib/src/test/kotlin/com/autonomi/sdk/SmokeTests.kt b/antd-kotlin/lib/src/test/kotlin/com/autonomi/sdk/SmokeTests.kt index 73f44fe..7ec1fb9 100644 --- a/antd-kotlin/lib/src/test/kotlin/com/autonomi/sdk/SmokeTests.kt +++ b/antd-kotlin/lib/src/test/kotlin/com/autonomi/sdk/SmokeTests.kt @@ -52,11 +52,6 @@ class SmokeTests { assertEquals("100", put.cost) assertEquals("abc123", put.address) - val desc = GraphDescendant("pk", "content") - val entry = GraphEntry("owner", listOf("p1"), "c", listOf(desc)) - assertEquals(1, entry.parents.size) - assertEquals(1, entry.descendants.size) - val archiveEntry = ArchiveEntry("/file.txt", "addr", 1000UL, 2000UL, 512UL) val archive = Archive(listOf(archiveEntry)) assertEquals(1, archive.entries.size) diff --git a/antd-lua/README.md b/antd-lua/README.md index 189f7a7..ba66ed1 100644 --- a/antd-lua/README.md +++ b/antd-lua/README.md @@ -121,15 +121,6 @@ All methods return `value, err` following Lua convention. On success `err` is `n | `client:chunk_put(data)` | Store a raw chunk | | `client:chunk_get(address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) - -| Method | Description | -|--------|-------------| -| `client:graph_entry_put(secret_key, parents, content, descendants)` | Create entry | -| `client:graph_entry_get(address)` | Read entry | -| `client:graph_entry_exists(address)` | Check if exists | -| `client:graph_entry_cost(public_key)` | Estimate creation cost | - ### Files & Directories | Method | Description | @@ -182,7 +173,6 @@ local antd = require("antd") local entry = antd.new_archive_entry("file.txt", "abc123", 1000, 2000, 42) local archive = antd.new_archive({ entry }) -local descendant = antd.new_graph_descendant("public_key_hex", "content_hex") ``` ## Examples @@ -193,7 +183,6 @@ See the [examples/](examples/) directory: - `02-data` — Public data storage and retrieval - `03-chunks` — Raw chunk operations - `04-files` — File and directory upload/download -- `05-graph` — Graph entry (DAG node) operations - `06-private-data` — Private encrypted data storage ## Testing diff --git a/antd-lua/antd-scm-1.rockspec b/antd-lua/antd-scm-1.rockspec index 8970f28..ca8473f 100644 --- a/antd-lua/antd-scm-1.rockspec +++ b/antd-lua/antd-scm-1.rockspec @@ -9,7 +9,7 @@ description = { detailed = [[ A Lua client library for the antd daemon REST API. Provides access to the Autonomi decentralized network for storing - and retrieving immutable data, chunks, graph entries, files, and archives. + and retrieving immutable data, chunks, files, and archives. ]], homepage = "https://github.com/WithAutonomi/ant-sdk/tree/main/antd-lua", license = "MIT", diff --git a/antd-lua/spec/client_spec.lua b/antd-lua/spec/client_spec.lua index f9a70bf..0e2ed4f 100644 --- a/antd-lua/spec/client_spec.lua +++ b/antd-lua/spec/client_spec.lua @@ -109,20 +109,6 @@ local function setup_daemon() register_route("GET", "/v1/chunks/chunk1", 200, cjson.encode({ data = base64.encode("chunkdata") })) - -- Graph - register_route("POST", "/v1/graph", 200, - cjson.encode({ cost = "500", address = "ge1" })) - register_route("GET", "/v1/graph/ge1", 200, - cjson.encode({ - owner = "owner1", - parents = {}, - content = "abc", - descendants = { { public_key = "pk1", content = "desc1" } }, - })) - register_route("HEAD", "/v1/graph/ge1", 200, "") - register_route("POST", "/v1/graph/cost", 200, - cjson.encode({ cost = "500" })) - -- Files register_route("POST", "/v1/files/upload/public", 200, cjson.encode({ cost = "1000", address = "file1" })) @@ -233,42 +219,6 @@ describe("antd client", function() end) end) - describe("graph_entry_put", function() - it("creates a graph entry", function() - local result, err = client:graph_entry_put("sk1", {}, "abc", {}) - assert.is_nil(err) - assert.are.equal("ge1", result.address) - assert.are.equal("500", result.cost) - end) - end) - - describe("graph_entry_get", function() - it("retrieves a graph entry", function() - local ge, err = client:graph_entry_get("ge1") - assert.is_nil(err) - assert.are.equal("owner1", ge.owner) - assert.are.equal(1, #ge.descendants) - assert.are.equal("pk1", ge.descendants[1].public_key) - assert.are.equal("desc1", ge.descendants[1].content) - end) - end) - - describe("graph_entry_exists", function() - it("returns true when entry exists", function() - local exists, err = client:graph_entry_exists("ge1") - assert.is_nil(err) - assert.is_true(exists) - end) - end) - - describe("graph_entry_cost", function() - it("estimates graph entry cost", function() - local cost, err = client:graph_entry_cost("pk1") - assert.is_nil(err) - assert.are.equal("500", cost) - end) - end) - describe("file_upload_public", function() it("uploads a file", function() local result, err = client:file_upload_public("/tmp/test.txt") @@ -364,9 +314,9 @@ describe("antd client", function() end) it("maps 409 to already_exists error", function() - register_route("POST", "/v1/graph", 409, + register_route("POST", "/v1/data/public", 409, cjson.encode({ error = "already exists" })) - local _, err = client:graph_entry_put("sk1", {}, "abc", {}) + local _, err = client:data_put_public("test") assert.is_not_nil(err) assert.are.equal("already_exists", err.type) assert.are.equal(409, err.status_code) diff --git a/antd-lua/src/antd/client.lua b/antd-lua/src/antd/client.lua index e0f5595..9a8ea5d 100644 --- a/antd-lua/src/antd/client.lua +++ b/antd-lua/src/antd/client.lua @@ -228,81 +228,6 @@ function Client:chunk_get(address) return base64.decode(str(j, "data")), nil end --- ── Graph ── - ---- Create a graph entry (DAG node). --- @param owner_secret_key string secret key --- @param parents table list of parent addresses --- @param content string hex content --- @param descendants table list of {public_key=, content=} tables --- @return PutResult|nil, error|nil -function Client:graph_entry_put(owner_secret_key, parents, content, descendants) - local descs = {} - for i, d in ipairs(descendants) do - descs[i] = { public_key = d.public_key, content = d.content } - end - local j, _, err = self:_do_json("POST", "/v1/graph", { - owner_secret_key = owner_secret_key, - parents = parents, - content = content, - descendants = descs, - }) - if err then return nil, err end - return models.new_put_result(str(j, "cost"), str(j, "address")), nil -end - ---- Retrieve a graph entry by address. --- @param address string hex address --- @return GraphEntry|nil, error|nil -function Client:graph_entry_get(address) - local j, _, err = self:_do_json("GET", "/v1/graph/" .. address, nil) - if err then return nil, err end - - local descs = {} - if j.descendants and type(j.descendants) == "table" then - for _, d in ipairs(j.descendants) do - if type(d) == "table" then - descs[#descs + 1] = models.new_graph_descendant(str(d, "public_key"), str(d, "content")) - end - end - end - - local parents = {} - if j.parents and type(j.parents) == "table" then - for _, p in ipairs(j.parents) do - if type(p) == "string" then - parents[#parents + 1] = p - end - end - end - - return models.new_graph_entry(str(j, "owner"), parents, str(j, "content"), descs), nil -end - ---- Check if a graph entry exists. --- @param address string hex address --- @return boolean|nil, error|nil -function Client:graph_entry_exists(address) - local code, err = self:_do_head("/v1/graph/" .. address) - if err then return nil, err end - if code == 404 then return false, nil end - if code >= 300 then - return nil, errors.error_for_status(code, "graph entry exists check failed") - end - return true, nil -end - ---- Estimate cost of creating a graph entry. --- @param public_key string hex public key --- @return string|nil cost in atto tokens, error|nil -function Client:graph_entry_cost(public_key) - local j, _, err = self:_do_json("POST", "/v1/graph/cost", { - public_key = public_key, - }) - if err then return nil, err end - return str(j, "cost"), nil -end - -- ── Files ── --- Upload a file to the network. diff --git a/antd-lua/src/antd/init.lua b/antd-lua/src/antd/init.lua index 1e8370c..662dc20 100644 --- a/antd-lua/src/antd/init.lua +++ b/antd-lua/src/antd/init.lua @@ -42,8 +42,6 @@ end -- Re-export models M.new_health_status = models.new_health_status M.new_put_result = models.new_put_result -M.new_graph_descendant = models.new_graph_descendant -M.new_graph_entry = models.new_graph_entry M.new_archive_entry = models.new_archive_entry M.new_archive = models.new_archive diff --git a/antd-lua/src/antd/models.lua b/antd-lua/src/antd/models.lua index 06a21ec..c5543f1 100644 --- a/antd-lua/src/antd/models.lua +++ b/antd-lua/src/antd/models.lua @@ -26,32 +26,6 @@ function M.new_put_result(cost, address) } end ---- Create a GraphDescendant table. --- @param public_key string hex public key --- @param content string hex content (32 bytes) --- @return table -function M.new_graph_descendant(public_key, content) - return { - public_key = public_key, - content = content, - } -end - ---- Create a GraphEntry table. --- @param owner string owner public key --- @param parents table list of parent addresses --- @param content string hex content --- @param descendants table list of GraphDescendant tables --- @return table -function M.new_graph_entry(owner, parents, content, descendants) - return { - owner = owner, - parents = parents or {}, - content = content, - descendants = descendants or {}, - } -end - --- Create an ArchiveEntry table. -- @param path string file path -- @param address string hex address diff --git a/antd-mcp/README.md b/antd-mcp/README.md index 0b6b7e2..0f3e200 100644 --- a/antd-mcp/README.md +++ b/antd-mcp/README.md @@ -1,6 +1,6 @@ # antd-mcp — MCP Server for Autonomi -An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the Autonomi network as 17 tools for AI agents. Works with Claude Desktop, Claude Code, and any MCP-compatible client. +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the Autonomi network as 13 tools for AI agents. Works with Claude Desktop, Claude Code, and any MCP-compatible client. ## Installation @@ -63,30 +63,21 @@ The server will auto-discover the daemon via the port file. Add `"env": {"ANTD_B |---|------|-------------| | 7 | `wallet_address()` | Get wallet public address | | 8 | `wallet_balance()` | Get wallet token and gas balances | -| 16 | `wallet_approve()` | Approve wallet to spend tokens on payment contracts (one-time) | +| 9 | `wallet_approve()` | Approve wallet to spend tokens on payment contracts (one-time) | ### Chunk Operations | # | Tool | Description | |---|------|-------------| -| 9 | `chunk_put(data)` | Store a raw chunk (base64 input) | -| 10 | `chunk_get(address)` | Retrieve a chunk (base64 output) | - -### Graph Operations - -| # | Tool | Description | -|---|------|-------------| -| 11 | `create_graph_entry(owner_secret_key, content, parents?, descendants?)` | Create DAG node | -| 12 | `get_graph_entry(address)` | Read graph entry | -| 13 | `graph_entry_exists(address)` | Check if entry exists | -| 14 | `graph_entry_cost(public_key)` | Estimate creation cost | +| 10 | `chunk_put(data)` | Store a raw chunk (base64 input) | +| 11 | `chunk_get(address)` | Retrieve a chunk (base64 output) | ### Archive Operations | # | Tool | Description | |---|------|-------------| -| 15 | `archive_get(address)` | List files in an archive | -| 17 | `archive_put(entries)` | Create an archive manifest | +| 12 | `archive_get(address)` | List files in an archive | +| 13 | `archive_put(entries)` | Create an archive manifest | ### Payment Modes @@ -128,7 +119,7 @@ antd-mcp/ ├── pyproject.toml └── src/antd_mcp/ ├── __init__.py - ├── server.py # 17 MCP tool definitions + ├── server.py # 13 MCP tool definitions ├── discover.py # Daemon port-file discovery └── errors.py # Error formatting ``` diff --git a/antd-mcp/src/antd_mcp/server.py b/antd-mcp/src/antd_mcp/server.py index d464328..855b3c7 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -12,8 +12,6 @@ from antd import AsyncAntdClient from antd.exceptions import AntdError -from antd.models import GraphDescendant - from .discover import discover_daemon_url from .errors import format_error, format_unexpected_error @@ -378,137 +376,7 @@ async def chunk_get( # --------------------------------------------------------------------------- -# Tool 11: create_graph_entry -# --------------------------------------------------------------------------- - - -@mcp.tool() -async def create_graph_entry( - owner_secret_key: str, - content: str, - parents: list[str] | None = None, - descendants: list[dict] | None = None, -) -> str: - """Create a graph entry (DAG node) on the Autonomi network. - - Args: - owner_secret_key: Hex-encoded secret key of the graph entry owner. - content: Hex-encoded content (32 bytes). - parents: List of parent graph entry addresses. Default: empty. - descendants: List of descendant objects, each with "public_key" and "content" (hex). - Default: empty. - - Returns: - JSON with graph entry address and cost, or error details. - """ - client, network = _get_ctx() - try: - desc = [ - GraphDescendant(public_key=d["public_key"], content=d["content"]) - for d in (descendants or []) - ] - result = await client.graph_entry_put( - owner_secret_key, parents or [], content, desc, - ) - return _ok({"address": result.address, "cost": result.cost}, network) - except AntdError as exc: - return _err_antd(exc, network) - except Exception as exc: - return _err(exc, network) - - -# --------------------------------------------------------------------------- -# Tool 12: get_graph_entry -# --------------------------------------------------------------------------- - - -@mcp.tool() -async def get_graph_entry( - address: str, -) -> str: - """Read a graph entry from the Autonomi network. - - Args: - address: Hex address of the graph entry. - - Returns: - JSON with graph entry details (owner, parents, content, descendants), - or error details. - """ - client, network = _get_ctx() - try: - g = await client.graph_entry_get(address) - return _ok({ - "owner": g.owner, - "parents": g.parents, - "content": g.content, - "descendants": [ - {"public_key": d.public_key, "content": d.content} - for d in g.descendants - ], - }, network) - except AntdError as exc: - return _err_antd(exc, network) - except Exception as exc: - return _err(exc, network) - - -# --------------------------------------------------------------------------- -# Tool 13: graph_entry_exists -# --------------------------------------------------------------------------- - - -@mcp.tool() -async def graph_entry_exists( - address: str, -) -> str: - """Check if a graph entry exists on the Autonomi network. - - Args: - address: Hex address of the graph entry. - - Returns: - JSON with exists boolean, or error details. - """ - client, network = _get_ctx() - try: - exists = await client.graph_entry_exists(address) - return _ok({"exists": exists}, network) - except AntdError as exc: - return _err_antd(exc, network) - except Exception as exc: - return _err(exc, network) - - -# --------------------------------------------------------------------------- -# Tool 14: graph_entry_cost -# --------------------------------------------------------------------------- - - -@mcp.tool() -async def graph_entry_cost( - public_key: str, -) -> str: - """Estimate the cost to create a graph entry. - - Args: - public_key: Hex-encoded public key of the graph entry owner. - - Returns: - JSON with cost in atto tokens, or error details. - """ - client, network = _get_ctx() - try: - cost = await client.graph_entry_cost(public_key) - return _ok({"cost": cost}, network) - except AntdError as exc: - return _err_antd(exc, network) - except Exception as exc: - return _err(exc, network) - - -# --------------------------------------------------------------------------- -# Tool 15: archive_get +# Tool 11: archive_get # --------------------------------------------------------------------------- @@ -547,7 +415,7 @@ async def archive_get( # --------------------------------------------------------------------------- -# Tool 16: wallet_approve +# Tool 12: wallet_approve # --------------------------------------------------------------------------- @@ -572,7 +440,7 @@ async def wallet_approve() -> str: # --------------------------------------------------------------------------- -# Tool 17: archive_put +# Tool 13: archive_put # --------------------------------------------------------------------------- @@ -611,7 +479,7 @@ async def archive_put( # --------------------------------------------------------------------------- -# Tool 18: prepare_upload +# Tool 14: prepare_upload # --------------------------------------------------------------------------- @@ -656,7 +524,7 @@ async def prepare_upload( # --------------------------------------------------------------------------- -# Tool 19: prepare_data_upload +# Tool 15: prepare_data_upload # --------------------------------------------------------------------------- @@ -703,7 +571,7 @@ async def prepare_data_upload( # --------------------------------------------------------------------------- -# Tool 20: finalize_upload +# Tool 16: finalize_upload # --------------------------------------------------------------------------- diff --git a/antd-php/README.md b/antd-php/README.md index 0a0d35c..ef39da4 100644 --- a/antd-php/README.md +++ b/antd-php/README.md @@ -78,14 +78,6 @@ $client = new AntdClient('http://localhost:8082', 300.0, $myGuzzleClient); | `chunkPut(string $data)` | Store a raw chunk | | `chunkGet(string $address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) -| Method | Description | -|--------|-------------| -| `graphEntryPut(string $ownerSecretKey, array $parents, string $content, array $descendants)` | Create entry | -| `graphEntryGet(string $address)` | Read entry | -| `graphEntryExists(string $address)` | Check if exists | -| `graphEntryCost(string $publicKey)` | Estimate creation cost | - ### Files & Directories | Method | Description | |--------|-------------| @@ -202,5 +194,4 @@ See the [examples/](examples/) directory: - `02-data.php` — Public data storage and retrieval - `03-chunks.php` — Raw chunk operations - `04-files.php` — File and directory upload/download -- `05-graph.php` — Graph entry (DAG node) operations - `06-private-data.php` — Private encrypted data diff --git a/antd-php/src/AntdClient.php b/antd-php/src/AntdClient.php index 6d4801e..478bee1 100644 --- a/antd-php/src/AntdClient.php +++ b/antd-php/src/AntdClient.php @@ -11,8 +11,6 @@ use Autonomi\Antd\Errors\ErrorFactory; use Autonomi\Antd\Models\Archive; use Autonomi\Antd\Models\ArchiveEntry; -use Autonomi\Antd\Models\GraphDescendant; -use Autonomi\Antd\Models\GraphEntry; use Autonomi\Antd\Models\HealthStatus; use Autonomi\Antd\Models\PutResult; @@ -399,177 +397,6 @@ public function chunkGetAsync(string $address): PromiseInterface ); } - // --- Graph --- - - /** - * Create a new graph entry (DAG node). - * - * @param string $ownerSecretKey - * @param string[] $parents - * @param string $content - * @param GraphDescendant[] $descendants - */ - public function graphEntryPut( - string $ownerSecretKey, - array $parents, - string $content, - array $descendants, - ): PutResult { - $descs = array_map( - fn(GraphDescendant $d) => ['public_key' => $d->publicKey, 'content' => $d->content], - $descendants, - ); - $json = $this->doJson('POST', '/v1/graph', [ - 'owner_secret_key' => $ownerSecretKey, - 'parents' => $parents, - 'content' => $content, - 'descendants' => $descs, - ]); - return new PutResult( - cost: $json['cost'] ?? '', - address: $json['address'] ?? '', - ); - } - - /** - * Async: Create a new graph entry (DAG node). - * - * @param string $ownerSecretKey - * @param string[] $parents - * @param string $content - * @param GraphDescendant[] $descendants - * @return PromiseInterface - */ - public function graphEntryPutAsync( - string $ownerSecretKey, - array $parents, - string $content, - array $descendants, - ): PromiseInterface { - $descs = array_map( - fn(GraphDescendant $d) => ['public_key' => $d->publicKey, 'content' => $d->content], - $descendants, - ); - return $this->doJsonAsync('POST', '/v1/graph', [ - 'owner_secret_key' => $ownerSecretKey, - 'parents' => $parents, - 'content' => $content, - 'descendants' => $descs, - ])->then( - fn(?array $json) => new PutResult( - cost: $json['cost'] ?? '', - address: $json['address'] ?? '', - ), - ); - } - - /** - * Retrieve a graph entry by address. - */ - public function graphEntryGet(string $address): GraphEntry - { - $json = $this->doJson('GET', '/v1/graph/' . $address); - $descendants = []; - foreach ($json['descendants'] ?? [] as $d) { - $descendants[] = new GraphDescendant( - publicKey: $d['public_key'] ?? '', - content: $d['content'] ?? '', - ); - } - return new GraphEntry( - owner: $json['owner'] ?? '', - parents: $json['parents'] ?? [], - content: $json['content'] ?? '', - descendants: $descendants, - ); - } - - /** - * Async: Retrieve a graph entry by address. - * - * @return PromiseInterface - */ - public function graphEntryGetAsync(string $address): PromiseInterface - { - return $this->doJsonAsync('GET', '/v1/graph/' . $address)->then( - function (?array $json) { - $descendants = []; - foreach ($json['descendants'] ?? [] as $d) { - $descendants[] = new GraphDescendant( - publicKey: $d['public_key'] ?? '', - content: $d['content'] ?? '', - ); - } - return new GraphEntry( - owner: $json['owner'] ?? '', - parents: $json['parents'] ?? [], - content: $json['content'] ?? '', - descendants: $descendants, - ); - }, - ); - } - - /** - * Check if a graph entry exists at the given address. - */ - public function graphEntryExists(string $address): bool - { - $code = $this->doHead('/v1/graph/' . $address); - if ($code === 404) { - return false; - } - if ($code >= 300) { - throw ErrorFactory::fromHttpStatus($code, 'graph entry exists check failed'); - } - return true; - } - - /** - * Async: Check if a graph entry exists at the given address. - * - * @return PromiseInterface - */ - public function graphEntryExistsAsync(string $address): PromiseInterface - { - return $this->doHeadAsync('/v1/graph/' . $address)->then( - function (int $code) { - if ($code === 404) { - return false; - } - if ($code >= 300) { - throw ErrorFactory::fromHttpStatus($code, 'graph entry exists check failed'); - } - return true; - }, - ); - } - - /** - * Estimate the cost of creating a graph entry. - */ - public function graphEntryCost(string $publicKey): string - { - $json = $this->doJson('POST', '/v1/graph/cost', [ - 'public_key' => $publicKey, - ]); - return $json['cost'] ?? ''; - } - - /** - * Async: Estimate the cost of creating a graph entry. - * - * @return PromiseInterface - */ - public function graphEntryCostAsync(string $publicKey): PromiseInterface - { - return $this->doJsonAsync('POST', '/v1/graph/cost', [ - 'public_key' => $publicKey, - ])->then( - fn(?array $json) => $json['cost'] ?? '', - ); - } - // --- Files --- /** diff --git a/antd-php/tests/AntdClientTest.php b/antd-php/tests/AntdClientTest.php index 93ab6da..9211e25 100644 --- a/antd-php/tests/AntdClientTest.php +++ b/antd-php/tests/AntdClientTest.php @@ -14,7 +14,6 @@ use Autonomi\Antd\Errors\AlreadyExistsError; use Autonomi\Antd\Models\Archive; use Autonomi\Antd\Models\ArchiveEntry; -use Autonomi\Antd\Models\GraphDescendant; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; @@ -129,65 +128,6 @@ public function testChunkGet(): void $this->assertSame('chunkdata', $data); } - // --- Graph --- - - public function testGraphEntryPut(): void - { - $mock = new MockHandler([ - $this->jsonResponse(200, ['cost' => '500', 'address' => 'ge1']), - ]); - $client = $this->createClient($mock); - $result = $client->graphEntryPut('sk1', [], 'abc', []); - $this->assertSame('ge1', $result->address); - $this->assertSame('500', $result->cost); - } - - public function testGraphEntryGet(): void - { - $mock = new MockHandler([ - $this->jsonResponse(200, [ - 'owner' => 'owner1', - 'parents' => [], - 'content' => 'abc', - 'descendants' => [['public_key' => 'pk1', 'content' => 'desc1']], - ]), - ]); - $client = $this->createClient($mock); - $entry = $client->graphEntryGet('ge1'); - $this->assertSame('owner1', $entry->owner); - $this->assertCount(1, $entry->descendants); - $this->assertSame('pk1', $entry->descendants[0]->publicKey); - $this->assertSame('desc1', $entry->descendants[0]->content); - } - - public function testGraphEntryExists(): void - { - $mock = new MockHandler([ - new Response(200), - ]); - $client = $this->createClient($mock); - $this->assertTrue($client->graphEntryExists('ge1')); - } - - public function testGraphEntryExistsNotFound(): void - { - $mock = new MockHandler([ - new Response(404), - ]); - $client = $this->createClient($mock); - $this->assertFalse($client->graphEntryExists('missing')); - } - - public function testGraphEntryCost(): void - { - $mock = new MockHandler([ - $this->jsonResponse(200, ['cost' => '500']), - ]); - $client = $this->createClient($mock); - $cost = $client->graphEntryCost('pk1'); - $this->assertSame('500', $cost); - } - // --- Files --- public function testFileUploadPublic(): void diff --git a/antd-py/README.md b/antd-py/README.md index 34ba18b..bd970c7 100644 --- a/antd-py/README.md +++ b/antd-py/README.md @@ -88,15 +88,6 @@ await aclient.close() | `chunk_put(data: bytes)` | `PutResult` | Store a raw chunk | | `chunk_get(address: str)` | `bytes` | Retrieve a chunk | -#### Graph - -| Method | Returns | Description | -|--------|---------|-------------| -| `graph_entry_put(owner_key: str, parents: list[str], content: str, descendants: list[GraphDescendant])` | `PutResult` | Create a graph entry | -| `graph_entry_get(address: str)` | `GraphEntry` | Read a graph entry | -| `graph_entry_exists(address: str)` | `bool` | Check if a graph entry exists | -| `graph_entry_cost(public_key: str)` | `str` | Estimate graph entry cost | - #### Files | Method | Returns | Description | @@ -117,8 +108,6 @@ All models are frozen dataclasses (immutable). |-------|--------|-------------| | `HealthStatus` | `ok: bool`, `network: str` | Health check result | | `PutResult` | `cost: str`, `address: str` | Write operation result | -| `GraphDescendant` | `public_key: str`, `content: str` | Graph descendant entry | -| `GraphEntry` | `owner`, `parents`, `content`, `descendants` | Graph DAG node | | `ArchiveEntry` | `path`, `address`, `created`, `modified`, `size` | Archive file entry | | `Archive` | `entries: list[ArchiveEntry]` | Archive manifest | @@ -162,7 +151,6 @@ python examples/01_connect.py # Health check python examples/02_data.py # Store/retrieve data python examples/03_chunks.py # Raw chunks python examples/04_files.py # File upload/download -python examples/05_graph.py # Graph entries (DAG) python examples/06_private_data.py # Private data with data maps ``` diff --git a/antd-py/examples/05_graph.py b/antd-py/examples/05_graph.py deleted file mode 100644 index dfd9d99..0000000 --- a/antd-py/examples/05_graph.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Example 05: Graph entry (DAG node) operations. - -Graph entries form a directed acyclic graph (DAG) on the network. -Each entry has an owner, content, parent links, and descendant links. -""" - -import os - -from antd import AntdClient -from antd.models import GraphDescendant - -client = AntdClient() - -# Generate a random secret key -secret_key = os.urandom(32).hex() - -# Create a root graph entry (no parents) -content = os.urandom(32).hex() # 32 bytes of content -result = client.graph_entry_put( - owner_secret_key=secret_key, - parents=[], - content=content, - descendants=[], -) -print(f"Graph entry created at: {result.address}") -print(f"Cost: {result.cost} atto tokens") - -# Read the graph entry -entry = client.graph_entry_get(result.address) -print(f"Owner: {entry.owner}") -print(f"Content: {entry.content}") -print(f"Parents: {entry.parents}") -print(f"Descendants: {len(entry.descendants)}") - -# Check existence -exists = client.graph_entry_exists(result.address) -print(f"Graph entry exists: {exists}") - -# Estimate cost for another entry -cost = client.graph_entry_cost(secret_key) -print(f"Cost estimate for new entry: {cost} atto tokens") - -print("Graph entry operations OK!") diff --git a/antd-py/src/antd/__init__.py b/antd-py/src/antd/__init__.py index bb4f57a..925db70 100644 --- a/antd-py/src/antd/__init__.py +++ b/antd-py/src/antd/__init__.py @@ -15,8 +15,6 @@ Archive, ArchiveEntry, FinalizeUploadResult, - GraphDescendant, - GraphEntry, HealthStatus, PaymentInfo, PrepareUploadResult, @@ -48,8 +46,6 @@ "HealthStatus", "Archive", "ArchiveEntry", - "GraphDescendant", - "GraphEntry", "PutResult", "WalletAddress", "WalletBalance", diff --git a/antd-py/src/antd/_grpc.py b/antd-py/src/antd/_grpc.py index 8f366a0..30ca92b 100644 --- a/antd-py/src/antd/_grpc.py +++ b/antd-py/src/antd/_grpc.py @@ -19,16 +19,12 @@ from .models import ( Archive, ArchiveEntry, - GraphDescendant, - GraphEntry, HealthStatus, PutResult, ) -from antd._proto.antd.v1 import common_pb2 from antd._proto.antd.v1 import data_pb2, data_pb2_grpc from antd._proto.antd.v1 import chunks_pb2, chunks_pb2_grpc -from antd._proto.antd.v1 import graph_pb2, graph_pb2_grpc from antd._proto.antd.v1 import files_pb2, files_pb2_grpc from antd._proto.antd.v1 import health_pb2, health_pb2_grpc @@ -76,7 +72,6 @@ def __init__(self, target: str = "localhost:50051"): self._health = health_pb2_grpc.HealthServiceStub(self._channel) self._data = data_pb2_grpc.DataServiceStub(self._channel) self._chunks = chunks_pb2_grpc.ChunkServiceStub(self._channel) - self._graph = graph_pb2_grpc.GraphServiceStub(self._channel) self._files = files_pb2_grpc.FileServiceStub(self._channel) def close(self) -> None: @@ -152,55 +147,6 @@ def chunk_get(self, address: str) -> bytes: except grpc.RpcError as e: _handle_rpc_error(e) - # --- Graph --- - - def graph_entry_put(self, owner_secret_key: str, parents: list[str], content: str, - descendants: list[GraphDescendant]) -> PutResult: - try: - resp = self._graph.Put(graph_pb2.PutGraphEntryRequest( - owner_secret_key=owner_secret_key, - parents=parents, - content=content, - descendants=[ - common_pb2.GraphDescendant(public_key=d.public_key, content=d.content) - for d in descendants - ], - )) - return PutResult(cost=resp.cost.atto_tokens, address=resp.address) - except grpc.RpcError as e: - _handle_rpc_error(e) - - def graph_entry_get(self, address: str) -> GraphEntry: - try: - resp = self._graph.Get(graph_pb2.GetGraphEntryRequest(address=address)) - return GraphEntry( - owner=resp.owner, - parents=list(resp.parents), - content=resp.content, - descendants=[ - GraphDescendant(public_key=d.public_key, content=d.content) - for d in resp.descendants - ], - ) - except grpc.RpcError as e: - _handle_rpc_error(e) - - def graph_entry_exists(self, address: str) -> bool: - try: - resp = self._graph.CheckExistence(graph_pb2.CheckGraphEntryRequest(address=address)) - return resp.exists - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.NOT_FOUND: - return False - _handle_rpc_error(e) - - def graph_entry_cost(self, public_key: str) -> str: - try: - resp = self._graph.GetCost(graph_pb2.GraphEntryCostRequest(public_key=public_key)) - return resp.atto_tokens - except grpc.RpcError as e: - _handle_rpc_error(e) - # --- Files --- def file_upload_public(self, path: str) -> PutResult: @@ -291,7 +237,6 @@ def __init__(self, target: str = "localhost:50051"): self._health = health_pb2_grpc.HealthServiceStub(self._channel) self._data = data_pb2_grpc.DataServiceStub(self._channel) self._chunks = chunks_pb2_grpc.ChunkServiceStub(self._channel) - self._graph = graph_pb2_grpc.GraphServiceStub(self._channel) self._files = files_pb2_grpc.FileServiceStub(self._channel) async def close(self) -> None: @@ -367,57 +312,6 @@ async def chunk_get(self, address: str) -> bytes: except grpc.RpcError as e: _handle_rpc_error(e) - # --- Graph --- - - async def graph_entry_put(self, owner_secret_key: str, parents: list[str], content: str, - descendants: list[GraphDescendant]) -> PutResult: - try: - resp = await self._graph.Put(graph_pb2.PutGraphEntryRequest( - owner_secret_key=owner_secret_key, - parents=parents, - content=content, - descendants=[ - common_pb2.GraphDescendant(public_key=d.public_key, content=d.content) - for d in descendants - ], - )) - return PutResult(cost=resp.cost.atto_tokens, address=resp.address) - except grpc.RpcError as e: - _handle_rpc_error(e) - - async def graph_entry_get(self, address: str) -> GraphEntry: - try: - resp = await self._graph.Get(graph_pb2.GetGraphEntryRequest(address=address)) - return GraphEntry( - owner=resp.owner, - parents=list(resp.parents), - content=resp.content, - descendants=[ - GraphDescendant(public_key=d.public_key, content=d.content) - for d in resp.descendants - ], - ) - except grpc.RpcError as e: - _handle_rpc_error(e) - - async def graph_entry_exists(self, address: str) -> bool: - try: - resp = await self._graph.CheckExistence( - graph_pb2.CheckGraphEntryRequest(address=address)) - return resp.exists - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.NOT_FOUND: - return False - _handle_rpc_error(e) - - async def graph_entry_cost(self, public_key: str) -> str: - try: - resp = await self._graph.GetCost( - graph_pb2.GraphEntryCostRequest(public_key=public_key)) - return resp.atto_tokens - except grpc.RpcError as e: - _handle_rpc_error(e) - # --- Files --- async def file_upload_public(self, path: str) -> PutResult: diff --git a/antd-py/src/antd/_rest.py b/antd-py/src/antd/_rest.py index 59ac547..1ac7d3f 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -12,8 +12,6 @@ Archive, ArchiveEntry, FinalizeUploadResult, - GraphDescendant, - GraphEntry, HealthStatus, PaymentInfo, PrepareUploadResult, @@ -132,44 +130,6 @@ def chunk_get(self, address: str) -> bytes: _check(resp) return _unb64(resp.json()["data"]) - # --- Graph --- - - def graph_entry_put(self, owner_secret_key: str, parents: list[str], content: str, - descendants: list[GraphDescendant]) -> PutResult: - resp = self._http.post("/v1/graph", json={ - "owner_secret_key": owner_secret_key, - "parents": parents, - "content": content, - "descendants": [{"public_key": d.public_key, "content": d.content} for d in descendants], - }) - _check(resp) - j = resp.json() - return PutResult(cost=j["cost"], address=j["address"]) - - def graph_entry_get(self, address: str) -> GraphEntry: - resp = self._http.get(f"/v1/graph/{address}") - _check(resp) - j = resp.json() - return GraphEntry( - owner=j["owner"], - parents=j.get("parents", []), - content=j["content"], - descendants=[GraphDescendant(public_key=d["public_key"], content=d["content"]) - for d in j.get("descendants", [])], - ) - - def graph_entry_exists(self, address: str) -> bool: - resp = self._http.head(f"/v1/graph/{address}") - if resp.status_code == 404: - return False - _check(resp) - return True - - def graph_entry_cost(self, public_key: str) -> str: - resp = self._http.post("/v1/graph/cost", json={"public_key": public_key}) - _check(resp) - return resp.json()["cost"] - # --- Files --- def file_upload_public(self, path: str, payment_mode: str | None = None) -> PutResult: @@ -417,44 +377,6 @@ async def chunk_get(self, address: str) -> bytes: _check(resp) return _unb64(resp.json()["data"]) - # --- Graph --- - - async def graph_entry_put(self, owner_secret_key: str, parents: list[str], content: str, - descendants: list[GraphDescendant]) -> PutResult: - resp = await self._http.post("/v1/graph", json={ - "owner_secret_key": owner_secret_key, - "parents": parents, - "content": content, - "descendants": [{"public_key": d.public_key, "content": d.content} for d in descendants], - }) - _check(resp) - j = resp.json() - return PutResult(cost=j["cost"], address=j["address"]) - - async def graph_entry_get(self, address: str) -> GraphEntry: - resp = await self._http.get(f"/v1/graph/{address}") - _check(resp) - j = resp.json() - return GraphEntry( - owner=j["owner"], - parents=j.get("parents", []), - content=j["content"], - descendants=[GraphDescendant(public_key=d["public_key"], content=d["content"]) - for d in j.get("descendants", [])], - ) - - async def graph_entry_exists(self, address: str) -> bool: - resp = await self._http.head(f"/v1/graph/{address}") - if resp.status_code == 404: - return False - _check(resp) - return True - - async def graph_entry_cost(self, public_key: str) -> str: - resp = await self._http.post("/v1/graph/cost", json={"public_key": public_key}) - _check(resp) - return resp.json()["cost"] - # --- Files --- async def file_upload_public(self, path: str, payment_mode: str | None = None) -> PutResult: diff --git a/antd-py/src/antd/models.py b/antd-py/src/antd/models.py index 081cedd..b21dfd2 100644 --- a/antd-py/src/antd/models.py +++ b/antd-py/src/antd/models.py @@ -18,22 +18,6 @@ class PutResult: address: str # hex -@dataclass(frozen=True) -class GraphDescendant: - """A descendant entry in a graph node.""" - public_key: str # hex - content: str # hex, 32 bytes - - -@dataclass(frozen=True) -class GraphEntry: - """A graph entry from the network.""" - owner: str - parents: list[str] = field(default_factory=list) - content: str = "" - descendants: list[GraphDescendant] = field(default_factory=list) - - @dataclass(frozen=True) class ArchiveEntry: """An entry in a file archive.""" diff --git a/antd-ruby/README.md b/antd-ruby/README.md index f484b00..ce62199 100644 --- a/antd-ruby/README.md +++ b/antd-ruby/README.md @@ -38,7 +38,7 @@ puts "Retrieved: #{data}" ## gRPC Transport -The SDK includes an `Antd::GrpcClient` class that provides the same 19 methods +The SDK includes an `Antd::GrpcClient` class that provides the same methods as the REST `Antd::Client`, but communicates over gRPC. ### Setup @@ -56,7 +56,7 @@ grpc_tools_ruby_protoc \ -I../../antd/proto \ --ruby_out=lib --grpc_out=lib \ antd/v1/common.proto antd/v1/health.proto antd/v1/data.proto \ - antd/v1/chunks.proto antd/v1/graph.proto antd/v1/files.proto + antd/v1/chunks.proto antd/v1/files.proto ``` The generated files are expected under `lib/antd/v1/`. @@ -131,14 +131,6 @@ client = Antd::Client.new(base_url: "http://custom-host:9090", timeout: 30) | `chunk_put(data)` | Store a raw chunk | | `chunk_get(address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) -| Method | Description | -|--------|-------------| -| `graph_entry_put(secret_key, parents, content, descendants)` | Create entry | -| `graph_entry_get(address)` | Read entry | -| `graph_entry_exists(address)` | Check if exists | -| `graph_entry_cost(public_key)` | Estimate creation cost | - ### Files & Directories | Method | Description | |--------|-------------| @@ -185,5 +177,4 @@ See the [examples/](examples/) directory: - `02_data.rb` — Public data put/get with cost estimate - `03_chunks.rb` — Chunk put/get - `04_files.rb` — File upload and download -- `05_graph.rb` — Graph entry CRUD - `06_private_data.rb` — Private data put/get diff --git a/antd-ruby/lib/antd/client.rb b/antd-ruby/lib/antd/client.rb index dae2fcb..af244d6 100644 --- a/antd-ruby/lib/antd/client.rb +++ b/antd-ruby/lib/antd/client.rb @@ -104,60 +104,6 @@ def chunk_get(address) b64_decode(j["data"]) end - # --- Graph --- - - # Create a new graph entry (DAG node). - # @param owner_secret_key [String] - # @param parents [Array] - # @param content [String] - # @param descendants [Array] - # @return [PutResult] - def graph_entry_put(owner_secret_key, parents, content, descendants) - descs = descendants.map { |d| { public_key: d.public_key, content: d.content } } - j = do_json(:post, "/v1/graph", { - owner_secret_key: owner_secret_key, - parents: parents, - content: content, - descendants: descs - }) - PutResult.new(cost: j["cost"], address: j["address"]) - end - - # Retrieve a graph entry by address. - # @param address [String] - # @return [GraphEntry] - def graph_entry_get(address) - j = do_json(:get, "/v1/graph/#{address}") - descs = (j["descendants"] || []).map do |d| - GraphDescendant.new(public_key: d["public_key"], content: d["content"]) - end - GraphEntry.new( - owner: j["owner"], - parents: j["parents"] || [], - content: j["content"], - descendants: descs - ) - end - - # Check if a graph entry exists at the given address. - # @param address [String] - # @return [Boolean] - def graph_entry_exists(address) - code = do_head("/v1/graph/#{address}") - return false if code == 404 - raise Antd.error_for_status(code, "graph entry exists check failed") if code >= 300 - - true - end - - # Estimate the cost of creating a graph entry. - # @param public_key [String] - # @return [String] cost in atto tokens - def graph_entry_cost(public_key) - j = do_json(:post, "/v1/graph/cost", { public_key: public_key }) - j["cost"] - end - # --- Files --- # Upload a local file to the network. diff --git a/antd-ruby/lib/antd/grpc_client.rb b/antd-ruby/lib/antd/grpc_client.rb index aa5d618..4007a95 100644 --- a/antd-ruby/lib/antd/grpc_client.rb +++ b/antd-ruby/lib/antd/grpc_client.rb @@ -6,7 +6,7 @@ # -I../../antd/proto \ # --ruby_out=lib --grpc_out=lib \ # antd/v1/common.proto antd/v1/health.proto antd/v1/data.proto \ -# antd/v1/chunks.proto antd/v1/graph.proto antd/v1/files.proto +# antd/v1/chunks.proto antd/v1/files.proto # # The generated files are expected under lib/antd/v1/. @@ -14,7 +14,6 @@ require_relative "v1/health_services_pb" require_relative "v1/data_services_pb" require_relative "v1/chunks_services_pb" -require_relative "v1/graph_services_pb" require_relative "v1/files_services_pb" module Antd @@ -22,7 +21,7 @@ module Antd # gRPC client for the antd daemon. # - # Provides the same 19 methods as the REST +Client+, but communicates over + # Provides the same methods as the REST +Client+, but communicates over # gRPC using the proto-generated stubs from +antd/v1/*.proto+. class GrpcClient # Creates a gRPC client using port discovery. @@ -43,7 +42,6 @@ def initialize(target: DEFAULT_GRPC_TARGET) @health_stub = Antd::V1::HealthService::Stub.new(target, :this_channel_is_insecure) @data_stub = Antd::V1::DataService::Stub.new(target, :this_channel_is_insecure) @chunk_stub = Antd::V1::ChunkService::Stub.new(target, :this_channel_is_insecure) - @graph_stub = Antd::V1::GraphService::Stub.new(target, :this_channel_is_insecure) @file_stub = Antd::V1::FileService::Stub.new(target, :this_channel_is_insecure) end @@ -123,63 +121,6 @@ def chunk_get(address) resp.data end - # --- Graph --- - - # Create a new graph entry (DAG node). - # @param owner_secret_key [String] - # @param parents [Array] - # @param content [String] - # @param descendants [Array] - # @return [PutResult] - def graph_entry_put(owner_secret_key, parents, content, descendants) - descs = descendants.map do |d| - Antd::V1::GraphDescendant.new(public_key: d.public_key, content: d.content) - end - req = Antd::V1::PutGraphEntryRequest.new( - owner_secret_key: owner_secret_key, - parents: parents, - content: content, - descendants: descs - ) - resp = grpc_call { @graph_stub.put(req) } - PutResult.new(cost: resp.cost.atto_tokens, address: resp.address) - end - - # Retrieve a graph entry by address. - # @param address [String] - # @return [GraphEntry] - def graph_entry_get(address) - req = Antd::V1::GetGraphEntryRequest.new(address: address) - resp = grpc_call { @graph_stub.get(req) } - descs = resp.descendants.map do |d| - GraphDescendant.new(public_key: d.public_key, content: d.content) - end - GraphEntry.new( - owner: resp.owner, - parents: resp.parents.to_a, - content: resp.content, - descendants: descs - ) - end - - # Check if a graph entry exists at the given address. - # @param address [String] - # @return [Boolean] - def graph_entry_exists(address) - req = Antd::V1::CheckGraphEntryRequest.new(address: address) - resp = grpc_call { @graph_stub.check_existence(req) } - resp.exists - end - - # Estimate the cost of creating a graph entry. - # @param public_key [String] - # @return [String] cost in atto tokens - def graph_entry_cost(public_key) - req = Antd::V1::GraphEntryCostRequest.new(public_key: public_key) - resp = grpc_call { @graph_stub.get_cost(req) } - resp.atto_tokens - end - # --- Files --- # Upload a local file to the network. diff --git a/antd-ruby/lib/antd/models.rb b/antd-ruby/lib/antd/models.rb index cec0f97..9b24802 100644 --- a/antd-ruby/lib/antd/models.rb +++ b/antd-ruby/lib/antd/models.rb @@ -7,12 +7,6 @@ module Antd # Result of a put/create operation. PutResult = Struct.new(:cost, :address, keyword_init: true) - # A descendant entry in a graph node. - GraphDescendant = Struct.new(:public_key, :content, keyword_init: true) - - # A DAG node from the network. - GraphEntry = Struct.new(:owner, :parents, :content, :descendants, keyword_init: true) - # A single entry in a file archive. ArchiveEntry = Struct.new(:path, :address, :created, :modified, :size, keyword_init: true) diff --git a/antd-ruby/test/test_client.rb b/antd-ruby/test/test_client.rb index fcd7d2e..a044d61 100644 --- a/antd-ruby/test/test_client.rb +++ b/antd-ruby/test/test_client.rb @@ -99,57 +99,6 @@ def test_chunk_get assert_equal "chunkdata", data end - # --- Graph --- - - def test_graph_entry_put - stub_request(:post, "#{BASE}/v1/graph") - .to_return(status: 200, body: '{"cost":"500","address":"ge1"}', - headers: { "Content-Type" => "application/json" }) - - result = @client.graph_entry_put("sk1", [], "abc", []) - assert_equal "500", result.cost - assert_equal "ge1", result.address - end - - def test_graph_entry_get - body = { - owner: "owner1", parents: [], content: "abc", - descendants: [{ public_key: "pk1", content: "desc1" }] - }.to_json - stub_request(:get, "#{BASE}/v1/graph/ge1") - .to_return(status: 200, body: body, - headers: { "Content-Type" => "application/json" }) - - ge = @client.graph_entry_get("ge1") - assert_equal "owner1", ge.owner - assert_equal 1, ge.descendants.length - assert_equal "pk1", ge.descendants[0].public_key - assert_equal "desc1", ge.descendants[0].content - end - - def test_graph_entry_exists - stub_request(:head, "#{BASE}/v1/graph/ge1") - .to_return(status: 200) - - assert @client.graph_entry_exists("ge1") - end - - def test_graph_entry_exists_not_found - stub_request(:head, "#{BASE}/v1/graph/missing") - .to_return(status: 404) - - refute @client.graph_entry_exists("missing") - end - - def test_graph_entry_cost - stub_request(:post, "#{BASE}/v1/graph/cost") - .to_return(status: 200, body: '{"cost":"500"}', - headers: { "Content-Type" => "application/json" }) - - cost = @client.graph_entry_cost("pk1") - assert_equal "500", cost - end - # --- Files --- def test_file_upload_public diff --git a/antd-ruby/test/test_grpc_client.rb b/antd-ruby/test/test_grpc_client.rb index 06d792d..2433c25 100644 --- a/antd-ruby/test/test_grpc_client.rb +++ b/antd-ruby/test/test_grpc_client.rb @@ -25,16 +25,6 @@ def data_get_private(m) grpc_call { @data_stub.get_private(nil).data } end def data_cost(d) grpc_call { @data_stub.get_cost(nil).atto_tokens } end def chunk_put(d) grpc_call { r = @chunk_stub.put(nil); PutResult.new(cost: r.cost.atto_tokens, address: r.address) } end def chunk_get(a) grpc_call { @chunk_stub.get(nil).data } end - def graph_entry_put(k, p, c, ds) grpc_call { r = @graph_stub.put(nil); PutResult.new(cost: r.cost.atto_tokens, address: r.address) } end - def graph_entry_get(a) - grpc_call do - r = @graph_stub.get(nil) - descs = r.descendants.map { |d| GraphDescendant.new(public_key: d.public_key, content: d.content) } - GraphEntry.new(owner: r.owner, parents: r.parents.to_a, content: r.content, descendants: descs) - end - end - def graph_entry_exists(a) grpc_call { @graph_stub.check_existence(nil).exists } end - def graph_entry_cost(k) grpc_call { @graph_stub.get_cost(nil).atto_tokens } end def file_upload_public(p) grpc_call { r = @file_stub.upload_public(nil); PutResult.new(cost: r.cost.atto_tokens, address: r.address) } end def file_download_public(a, d) grpc_call { @file_stub.download_public(nil); nil } end def dir_upload_public(p) grpc_call { r = @file_stub.dir_upload_public(nil); PutResult.new(cost: r.cost.atto_tokens, address: r.address) } end @@ -105,9 +95,6 @@ module FakeGrpc # Simulates a cost sub-message with an atto_tokens field. Cost = Struct.new(:atto_tokens, keyword_init: true) - # A canned gRPC response descriptor that responds to the proto methods. - GraphDescendant = Struct.new(:public_key, :content, keyword_init: true) - # Archive entry proto mimic. ArchiveEntry = Struct.new(:path, :address, :created, :modified, :size, keyword_init: true) @@ -153,29 +140,6 @@ def get(_req) end end - class GraphStub - def put(_req) - OpenStruct.new(cost: Cost.new(atto_tokens: "500"), address: "ge1") - end - - def get(_req) - OpenStruct.new( - owner: "owner1", - parents: [], - content: "abc", - descendants: [GraphDescendant.new(public_key: "pk1", content: "desc1")] - ) - end - - def check_existence(_req) - OpenStruct.new(exists: true) - end - - def get_cost(_req) - OpenStruct.new(atto_tokens: "500") - end - end - class FileStub def upload_public(_req) OpenStruct.new(cost: Cost.new(atto_tokens: "1000"), address: "file1") @@ -236,7 +200,6 @@ def build_fake_client client.instance_variable_set(:@health_stub, FakeGrpc::HealthStub.new) client.instance_variable_set(:@data_stub, FakeGrpc::DataStub.new) client.instance_variable_set(:@chunk_stub, FakeGrpc::ChunkStub.new) - client.instance_variable_set(:@graph_stub, FakeGrpc::GraphStub.new) client.instance_variable_set(:@file_stub, FakeGrpc::FileStub.new) client end @@ -247,7 +210,6 @@ def build_error_client(error) client.instance_variable_set(:@health_stub, stub) client.instance_variable_set(:@data_stub, stub) client.instance_variable_set(:@chunk_stub, stub) - client.instance_variable_set(:@graph_stub, stub) client.instance_variable_set(:@file_stub, stub) client end @@ -315,33 +277,6 @@ def test_chunk_get assert_equal "chunkdata", data end - # --- Graph --- - - def test_graph_entry_put - result = @client.graph_entry_put("sk1", [], "abc", []) - assert_equal "500", result.cost - assert_equal "ge1", result.address - end - - def test_graph_entry_get - ge = @client.graph_entry_get("ge1") - assert_equal "owner1", ge.owner - assert_equal [], ge.parents - assert_equal "abc", ge.content - assert_equal 1, ge.descendants.length - assert_equal "pk1", ge.descendants[0].public_key - assert_equal "desc1", ge.descendants[0].content - end - - def test_graph_entry_exists - assert @client.graph_entry_exists("ge1") - end - - def test_graph_entry_cost - cost = @client.graph_entry_cost("pk1") - assert_equal "500", cost - end - # --- Files --- def test_file_upload_public @@ -444,11 +379,6 @@ def test_error_propagates_from_chunk_get assert_raises(Antd::InternalError) { client.chunk_get("addr") } end - def test_error_propagates_from_graph_entry_put - client = build_error_client(grpc_error(:ALREADY_EXISTS, "dup")) - assert_raises(Antd::AlreadyExistsError) { client.graph_entry_put("sk", [], "c", []) } - end - def test_error_propagates_from_file_upload client = build_error_client(grpc_error(:RESOURCE_EXHAUSTED, "huge")) assert_raises(Antd::TooLargeError) { client.file_upload_public("/tmp/big") } diff --git a/antd-rust/README.md b/antd-rust/README.md index 7d94261..6af6dd5 100644 --- a/antd-rust/README.md +++ b/antd-rust/README.md @@ -87,14 +87,6 @@ All methods are `async` and return `Result`. | `chunk_put(data)` | Store a raw chunk | | `chunk_get(address)` | Retrieve a chunk | -### Graph Entries (DAG Nodes) -| Method | Description | -|--------|-------------| -| `graph_entry_put(secret_key, parents, content, descendants)` | Create entry | -| `graph_entry_get(address)` | Read entry | -| `graph_entry_exists(address)` | Check if exists | -| `graph_entry_cost(public_key)` | Estimate creation cost | - ### Files & Directories | Method | Description | |--------|-------------| @@ -108,7 +100,7 @@ All methods are `async` and return `Result`. ## gRPC Transport -The SDK also provides a gRPC client with the same 19 async methods. It connects to the +The SDK also provides a gRPC client with the same async methods. It connects to the antd daemon's gRPC endpoint (default `localhost:50051`) using [tonic](https://github.com/hyperium/tonic). ```rust @@ -174,7 +166,6 @@ See the [examples/](examples/) directory: - `02-data` — Public data storage and retrieval - `03-chunks` — Raw chunk operations - `04-files` — File and directory upload/download -- `05-graph` — Graph entry (DAG node) operations - `06-private-data` — Private encrypted data storage Run an example: diff --git a/antd-rust/src/client.rs b/antd-rust/src/client.rs index 5994bc5..6b83dfa 100644 --- a/antd-rust/src/client.rs +++ b/antd-rust/src/client.rs @@ -272,106 +272,6 @@ impl Client { Self::b64_decode(&Self::str_field(&j, "data")) } - // --- Graph --- - - /// Creates a new graph entry (DAG node). - pub async fn graph_entry_put( - &self, - owner_secret_key: &str, - parents: &[String], - content: &str, - descendants: &[GraphDescendant], - ) -> Result { - let descs: Vec = descendants - .iter() - .map(|d| json!({ "public_key": d.public_key, "content": d.content })) - .collect(); - let (j, _) = self - .do_json( - reqwest::Method::POST, - "/v1/graph", - Some(json!({ - "owner_secret_key": owner_secret_key, - "parents": parents, - "content": content, - "descendants": descs, - })), - ) - .await?; - let j = j.unwrap_or_default(); - Ok(PutResult { - cost: Self::str_field(&j, "cost"), - address: Self::str_field(&j, "address"), - }) - } - - /// Retrieves a graph entry by address. - pub async fn graph_entry_get(&self, address: &str) -> Result { - let (j, _) = self - .do_json( - reqwest::Method::GET, - &format!("/v1/graph/{address}"), - None, - ) - .await?; - let j = j.unwrap_or_default(); - let descendants = j - .get("descendants") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .map(|d| GraphDescendant { - public_key: Self::str_field(d, "public_key"), - content: Self::str_field(d, "content"), - }) - .collect() - }) - .unwrap_or_default(); - let parents = j - .get("parents") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - Ok(GraphEntry { - owner: Self::str_field(&j, "owner"), - parents, - content: Self::str_field(&j, "content"), - descendants, - }) - } - - /// Checks if a graph entry exists at the given address. - pub async fn graph_entry_exists(&self, address: &str) -> Result { - let code = self.do_head(&format!("/v1/graph/{address}")).await?; - if code == 404 { - return Ok(false); - } - if code >= 300 { - return Err(error_for_status( - code, - "graph entry exists check failed".to_string(), - )); - } - Ok(true) - } - - /// Estimates the cost of creating a graph entry. - pub async fn graph_entry_cost(&self, public_key: &str) -> Result { - let (j, _) = self - .do_json( - reqwest::Method::POST, - "/v1/graph/cost", - Some(json!({ "public_key": public_key })), - ) - .await?; - let j = j.unwrap_or_default(); - Ok(Self::str_field(&j, "cost")) - } - // --- Files --- /// Uploads a local file to the network. diff --git a/antd-rust/src/grpc_client.rs b/antd-rust/src/grpc_client.rs index 05fa05d..e96d2ad 100644 --- a/antd-rust/src/grpc_client.rs +++ b/antd-rust/src/grpc_client.rs @@ -17,7 +17,6 @@ use proto::antd::v1::{ chunk_service_client::ChunkServiceClient, data_service_client::DataServiceClient, file_service_client::FileServiceClient, - graph_service_client::GraphServiceClient, health_service_client::HealthServiceClient, }; @@ -26,14 +25,13 @@ pub const DEFAULT_GRPC_ENDPOINT: &str = "http://localhost:50051"; /// gRPC client for the antd daemon. /// -/// Provides the same 19 async methods as [`crate::Client`] but communicates +/// Provides the same async methods as [`crate::Client`] but communicates /// over gRPC instead of REST/JSON. #[derive(Debug, Clone)] pub struct GrpcClient { health: HealthServiceClient, data: DataServiceClient, chunks: ChunkServiceClient, - graph: GraphServiceClient, files: FileServiceClient, } @@ -65,7 +63,6 @@ impl GrpcClient { health: HealthServiceClient::new(channel.clone()), data: DataServiceClient::new(channel.clone()), chunks: ChunkServiceClient::new(channel.clone()), - graph: GraphServiceClient::new(channel.clone()), files: FileServiceClient::new(channel), }) } @@ -213,103 +210,6 @@ impl GrpcClient { Ok(resp.data) } - // --- Graph --- - - /// Creates a new graph entry (DAG node). - pub async fn graph_entry_put( - &self, - owner_secret_key: &str, - parents: &[String], - content: &str, - descendants: &[GraphDescendant], - ) -> Result { - let proto_descendants: Vec = descendants - .iter() - .map(|d| proto::antd::v1::GraphDescendant { - public_key: d.public_key.clone(), - content: d.content.clone(), - }) - .collect(); - - let resp = self - .graph - .clone() - .put(proto::antd::v1::PutGraphEntryRequest { - owner_secret_key: owner_secret_key.to_string(), - parents: parents.to_vec(), - content: content.to_string(), - descendants: proto_descendants, - }) - .await? - .into_inner(); - - let cost = resp - .cost - .map(|c| c.atto_tokens) - .unwrap_or_default(); - - Ok(PutResult { - cost, - address: resp.address, - }) - } - - /// Retrieves a graph entry by address. - pub async fn graph_entry_get(&self, address: &str) -> Result { - let resp = self - .graph - .clone() - .get(proto::antd::v1::GetGraphEntryRequest { - address: address.to_string(), - }) - .await? - .into_inner(); - - let descendants = resp - .descendants - .into_iter() - .map(|d| GraphDescendant { - public_key: d.public_key, - content: d.content, - }) - .collect(); - - Ok(GraphEntry { - owner: resp.owner, - parents: resp.parents, - content: resp.content, - descendants, - }) - } - - /// Checks if a graph entry exists at the given address. - pub async fn graph_entry_exists(&self, address: &str) -> Result { - let resp = self - .graph - .clone() - .check_existence(proto::antd::v1::CheckGraphEntryRequest { - address: address.to_string(), - }) - .await? - .into_inner(); - - Ok(resp.exists) - } - - /// Estimates the cost of creating a graph entry. - pub async fn graph_entry_cost(&self, public_key: &str) -> Result { - let resp = self - .graph - .clone() - .get_cost(proto::antd::v1::GraphEntryCostRequest { - public_key: public_key.to_string(), - }) - .await? - .into_inner(); - - Ok(resp.atto_tokens) - } - // --- Files --- /// Uploads a local file to the network. diff --git a/antd-rust/src/grpc_tests.rs b/antd-rust/src/grpc_tests.rs index 758de7c..e53974b 100644 --- a/antd-rust/src/grpc_tests.rs +++ b/antd-rust/src/grpc_tests.rs @@ -120,55 +120,6 @@ impl v1::chunk_service_server::ChunkService for MockChunkService { } } -#[derive(Default)] -struct MockGraphService; - -#[tonic::async_trait] -impl v1::graph_service_server::GraphService for MockGraphService { - async fn put( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(v1::PutGraphEntryResponse { - cost: Some(v1::Cost { - atto_tokens: "500".to_string(), - }), - address: "ge1".to_string(), - })) - } - - async fn get( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(v1::GetGraphEntryResponse { - owner: "owner1".to_string(), - parents: vec![], - content: "abc".to_string(), - descendants: vec![v1::GraphDescendant { - public_key: "pk1".to_string(), - content: "desc1".to_string(), - }], - })) - } - - async fn check_existence( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(v1::GraphExistsResponse { exists: true })) - } - - async fn get_cost( - &self, - _request: Request, - ) -> Result, Status> { - Ok(Response::new(v1::Cost { - atto_tokens: "500".to_string(), - })) - } -} - #[derive(Default)] struct MockFileService; @@ -285,9 +236,6 @@ async fn start_mock_server() -> GrpcClient { .add_service(v1::chunk_service_server::ChunkServiceServer::new( MockChunkService, )) - .add_service(v1::graph_service_server::GraphServiceServer::new( - MockGraphService, - )) .add_service(v1::file_service_server::FileServiceServer::new( MockFileService, )) @@ -328,7 +276,7 @@ async fn start_error_server(code: tonic::Code, msg: &str) -> GrpcClient { GrpcClient::new(&format!("http://{addr}")).await.unwrap() } -// --- Tests for all 19 gRPC methods --- +// --- Tests for all gRPC methods --- #[tokio::test] async fn test_grpc_health() { @@ -390,41 +338,6 @@ async fn test_grpc_chunk_get() { assert_eq!(data, b"chunkdata"); } -#[tokio::test] -async fn test_grpc_graph_entry_put() { - let client = start_mock_server().await; - let result = client - .graph_entry_put("sk1", &[], "abc", &[]) - .await - .unwrap(); - assert_eq!(result.address, "ge1"); - assert_eq!(result.cost, "500"); -} - -#[tokio::test] -async fn test_grpc_graph_entry_get() { - let client = start_mock_server().await; - let entry = client.graph_entry_get("ge1").await.unwrap(); - assert_eq!(entry.owner, "owner1"); - assert_eq!(entry.descendants.len(), 1); - assert_eq!(entry.descendants[0].public_key, "pk1"); - assert_eq!(entry.descendants[0].content, "desc1"); -} - -#[tokio::test] -async fn test_grpc_graph_entry_exists() { - let client = start_mock_server().await; - let exists = client.graph_entry_exists("ge1").await.unwrap(); - assert!(exists); -} - -#[tokio::test] -async fn test_grpc_graph_entry_cost() { - let client = start_mock_server().await; - let cost = client.graph_entry_cost("pk1").await.unwrap(); - assert_eq!(cost, "500"); -} - #[tokio::test] async fn test_grpc_file_upload_public() { let client = start_mock_server().await; diff --git a/antd-rust/src/models.rs b/antd-rust/src/models.rs index 510a9ca..dc9b371 100644 --- a/antd-rust/src/models.rs +++ b/antd-rust/src/models.rs @@ -16,24 +16,6 @@ pub struct PutResult { pub address: String, } -/// A descendant entry in a graph node. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GraphDescendant { - /// Hex-encoded public key. - pub public_key: String, - /// Hex-encoded content (32 bytes). - pub content: String, -} - -/// A DAG node from the network. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GraphEntry { - pub owner: String, - pub parents: Vec, - pub content: String, - pub descendants: Vec, -} - /// A single entry in a file archive. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ArchiveEntry { diff --git a/antd-rust/src/tests.rs b/antd-rust/src/tests.rs index 57308e0..53004df 100644 --- a/antd-rust/src/tests.rs +++ b/antd-rust/src/tests.rs @@ -85,42 +85,6 @@ fn mock_chunk_get(server: &mut ServerGuard) -> Mock { .create() } -fn mock_graph_entry_put(server: &mut ServerGuard) -> Mock { - server - .mock("POST", "/v1/graph") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(r#"{"cost":"500","address":"ge1"}"#) - .create() -} - -fn mock_graph_entry_get(server: &mut ServerGuard) -> Mock { - server - .mock("GET", "/v1/graph/ge1") - .with_status(200) - .with_header("content-type", "application/json") - .with_body( - r#"{"owner":"owner1","parents":[],"content":"abc","descendants":[{"public_key":"pk1","content":"desc1"}]}"#, - ) - .create() -} - -fn mock_graph_entry_exists(server: &mut ServerGuard) -> Mock { - server - .mock("HEAD", "/v1/graph/ge1") - .with_status(200) - .create() -} - -fn mock_graph_entry_cost(server: &mut ServerGuard) -> Mock { - server - .mock("POST", "/v1/graph/cost") - .with_status(200) - .with_header("content-type", "application/json") - .with_body(r#"{"cost":"500"}"#) - .create() -} - fn mock_file_upload_public(server: &mut ServerGuard) -> Mock { server .mock("POST", "/v1/files/upload/public") @@ -270,53 +234,6 @@ async fn test_chunk_get() { assert_eq!(data, b"chunkdata"); } -#[tokio::test] -async fn test_graph_entry_put() { - let mut server = mock_server().await; - let _m = mock_graph_entry_put(&mut server); - let client = Client::new(&server.url()); - - let result = client - .graph_entry_put("sk1", &[], "abc", &[]) - .await - .unwrap(); - assert_eq!(result.address, "ge1"); - assert_eq!(result.cost, "500"); -} - -#[tokio::test] -async fn test_graph_entry_get() { - let mut server = mock_server().await; - let _m = mock_graph_entry_get(&mut server); - let client = Client::new(&server.url()); - - let entry = client.graph_entry_get("ge1").await.unwrap(); - assert_eq!(entry.owner, "owner1"); - assert_eq!(entry.descendants.len(), 1); - assert_eq!(entry.descendants[0].public_key, "pk1"); - assert_eq!(entry.descendants[0].content, "desc1"); -} - -#[tokio::test] -async fn test_graph_entry_exists() { - let mut server = mock_server().await; - let _m = mock_graph_entry_exists(&mut server); - let client = Client::new(&server.url()); - - let exists = client.graph_entry_exists("ge1").await.unwrap(); - assert!(exists); -} - -#[tokio::test] -async fn test_graph_entry_cost() { - let mut server = mock_server().await; - let _m = mock_graph_entry_cost(&mut server); - let client = Client::new(&server.url()); - - let cost = client.graph_entry_cost("pk1").await.unwrap(); - assert_eq!(cost, "500"); -} - #[tokio::test] async fn test_file_upload_public() { let mut server = mock_server().await; @@ -523,7 +440,7 @@ async fn test_error_mapping_network() { async fn test_error_mapping_already_exists() { let mut server = mock_server().await; let _m = server - .mock("POST", "/v1/graph") + .mock("POST", "/v1/data/public") .with_status(409) .with_header("content-type", "application/json") .with_body(r#"{"error":"already exists"}"#) @@ -531,7 +448,7 @@ async fn test_error_mapping_already_exists() { let client = Client::new(&server.url()); let err = client - .graph_entry_put("sk1", &[], "abc", &[]) + .data_put_public(b"test") .await .unwrap_err(); match err { diff --git a/antd-swift/README.md b/antd-swift/README.md index 8907e3a..3ae99c5 100644 --- a/antd-swift/README.md +++ b/antd-swift/README.md @@ -65,7 +65,6 @@ All methods are `async throws` for use with Swift concurrency. | **Health** | `health()` | | **Data** | `dataPutPublic`, `dataGetPublic`, `dataPutPrivate`, `dataGetPrivate`, `dataCost` | | **Chunks** | `chunkPut`, `chunkGet` | -| **Graph** | `graphEntryPut`, `graphEntryGet`, `graphEntryExists`, `graphEntryCost` | | **Files** | `fileUploadPublic`, `fileDownloadPublic`, `dirUploadPublic`, `dirDownloadPublic`, `archiveGetPublic`, `archivePutPublic`, `fileCost` | ## Error Handling diff --git a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift index 09f0df3..68ed387 100644 --- a/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift +++ b/antd-swift/Sources/AntdSdk/AntdClientProtocol.swift @@ -20,12 +20,6 @@ public protocol AntdClientProtocol: Sendable { func chunkPut(_ data: Data) async throws -> PutResult func chunkGet(address: String) async throws -> Data - // Graph - func graphEntryPut(ownerSecretKey: String, parents: [String], content: String, descendants: [GraphDescendant]) async throws -> PutResult - func graphEntryGet(address: String) async throws -> GraphEntry - func graphEntryExists(address: String) async throws -> Bool - func graphEntryCost(publicKey: String) async throws -> String - // Files func fileUploadPublic(path: String, paymentMode: String?) async throws -> PutResult func fileDownloadPublic(address: String, destPath: String) async throws diff --git a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift index c6c5fae..3337f8c 100644 --- a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift @@ -32,10 +32,6 @@ public final class AntdGrpcClient: AntdClientProtocol, @unchecked Sendable { public func dataCost(_ data: Data) async throws -> String { throw notImplemented() } public func chunkPut(_ data: Data) async throws -> PutResult { throw notImplemented() } public func chunkGet(address: String) async throws -> Data { throw notImplemented() } - public func graphEntryPut(ownerSecretKey: String, parents: [String], content: String, descendants: [GraphDescendant]) async throws -> PutResult { throw notImplemented() } - public func graphEntryGet(address: String) async throws -> GraphEntry { throw notImplemented() } - public func graphEntryExists(address: String) async throws -> Bool { throw notImplemented() } - public func graphEntryCost(publicKey: String) async throws -> String { throw notImplemented() } public func fileUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { throw notImplemented() } public func fileDownloadPublic(address: String, destPath: String) async throws { throw notImplemented() } public func dirUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { throw notImplemented() } diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index 8fbe484..8a177f7 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -119,34 +119,6 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { return decoded } - // MARK: - Graph - - public func graphEntryPut(ownerSecretKey: String, parents: [String], content: String, descendants: [GraphDescendant]) async throws -> PutResult { - let body: [String: Any] = [ - "owner_secret_key": ownerSecretKey, - "parents": parents, - "content": content, - "descendants": descendants.map { ["public_key": $0.publicKey, "content": $0.content] }, - ] - let resp: CostAddressDTO = try await postJSON("/v1/graph", body: body) - return PutResult(cost: resp.cost, address: resp.address) - } - - public func graphEntryGet(address: String) async throws -> GraphEntry { - let resp: GraphEntryDTO = try await getJSON("/v1/graph/\(address)") - let desc = (resp.descendants ?? []).map { GraphDescendant(publicKey: $0.publicKey, content: $0.content) } - return GraphEntry(owner: resp.owner, parents: resp.parents ?? [], content: resp.content, descendants: desc) - } - - public func graphEntryExists(address: String) async throws -> Bool { - try await headExists("/v1/graph/\(address)") - } - - public func graphEntryCost(publicKey: String) async throws -> String { - let resp: CostDTO = try await postJSON("/v1/graph/cost", body: ["public_key": publicKey]) - return resp.cost - } - // MARK: - Files public func fileUploadPublic(path: String, paymentMode: String? = nil) async throws -> PutResult { @@ -262,18 +234,6 @@ private struct CostDTO: Decodable { let cost: String } -private struct GraphDescendantDTO: Decodable { - let publicKey: String - let content: String -} - -private struct GraphEntryDTO: Decodable { - let owner: String - let parents: [String]? - let content: String - let descendants: [GraphDescendantDTO]? -} - private struct ArchiveEntryDTO: Decodable { let path: String let address: String diff --git a/antd-swift/Sources/AntdSdk/Models.swift b/antd-swift/Sources/AntdSdk/Models.swift index fdfa405..d24ce29 100644 --- a/antd-swift/Sources/AntdSdk/Models.swift +++ b/antd-swift/Sources/AntdSdk/Models.swift @@ -22,32 +22,6 @@ public struct PutResult: Sendable, Equatable { } } -/// A descendant entry in a graph node. -public struct GraphDescendant: Sendable, Equatable { - public let publicKey: String - public let content: String - - public init(publicKey: String, content: String) { - self.publicKey = publicKey - self.content = content - } -} - -/// A graph entry retrieved from the network. -public struct GraphEntry: Sendable, Equatable { - public let owner: String - public let parents: [String] - public let content: String - public let descendants: [GraphDescendant] - - public init(owner: String, parents: [String], content: String, descendants: [GraphDescendant]) { - self.owner = owner - self.parents = parents - self.content = content - self.descendants = descendants - } -} - /// A single entry in an archive manifest. public struct ArchiveEntry: Sendable, Equatable { public let path: String diff --git a/antd-zig/README.md b/antd-zig/README.md index 4db0e56..65404c9 100644 --- a/antd-zig/README.md +++ b/antd-zig/README.md @@ -109,15 +109,6 @@ All methods return `!T` (error union) using Zig's standard error handling. | `chunkPut` | `fn (self: *Client, data: []const u8) !PutResult` | Store a raw chunk | | `chunkGet` | `fn (self: *Client, address: []const u8) ![]const u8` | Retrieve a chunk | -### Graph Entries (DAG Nodes) - -| Method | Signature | Description | -|--------|-----------|-------------| -| `graphEntryPut` | `fn (self: *Client, owner_secret_key: []const u8, parents: []const []const u8, content: []const u8, descendants: []const GraphDescendant) !PutResult` | Create entry | -| `graphEntryGet` | `fn (self: *Client, address: []const u8) !GraphEntry` | Read entry | -| `graphEntryExists` | `fn (self: *Client, address: []const u8) !bool` | Check if exists | -| `graphEntryCost` | `fn (self: *Client, public_key: []const u8) ![]const u8` | Estimate creation cost | - ### Files & Directories | Method | Signature | Description | @@ -177,8 +168,8 @@ const result = client.health() catch |err| { The Zig SDK follows Zig's explicit memory management conventions: - **Caller owns all returned allocations.** You must free them when done. -- Struct results (`HealthStatus`, `PutResult`, `GraphEntry`, `Archive`) have a `deinit(allocator)` method that frees all owned memory. -- Raw byte slices (`[]const u8`) returned by `dataGetPublic`, `dataGetPrivate`, `chunkGet`, `dataCost`, `graphEntryCost`, and `fileCost` must be freed with `allocator.free(result)`. +- Struct results (`HealthStatus`, `PutResult`, `Archive`) have a `deinit(allocator)` method that frees all owned memory. +- Raw byte slices (`[]const u8`) returned by `dataGetPublic`, `dataGetPrivate`, `chunkGet`, `dataCost`, and `fileCost` must be freed with `allocator.free(result)`. - Use `defer` immediately after receiving a result to ensure cleanup. ```zig @@ -201,7 +192,6 @@ zig build run-01-connect zig build run-02-data zig build run-03-chunks zig build run-04-files -zig build run-05-graph zig build run-06-private-data ``` @@ -213,5 +203,4 @@ See the [examples/](examples/) directory: - `02-data` -- Public data put/get - `03-chunks` -- Chunk put/get - `04-files` -- File upload/download -- `05-graph` -- Graph entry CRUD - `06-private-data` -- Private data put/get diff --git a/antd-zig/src/antd.zig b/antd-zig/src/antd.zig index 0602117..b5716c3 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -9,8 +9,6 @@ pub const discover = @import("discover.zig"); pub const HealthStatus = models.HealthStatus; pub const PutResult = models.PutResult; -pub const GraphDescendant = models.GraphDescendant; -pub const GraphEntry = models.GraphEntry; pub const ArchiveEntry = models.ArchiveEntry; pub const Archive = models.Archive; pub const WalletAddress = models.WalletAddress; @@ -250,59 +248,6 @@ pub const Client = struct { return json_helpers.parseBase64Data(self.allocator, resp); } - // --- Graph --- - - /// Create a new graph entry (DAG node). - pub fn graphEntryPut( - self: *Client, - owner_secret_key: []const u8, - parents: []const []const u8, - content: []const u8, - descendants: []const GraphDescendant, - ) !PutResult { - const req_body = try json_helpers.buildJsonBody(self.allocator, &.{ - .{ .key = "owner_secret_key", .value = .{ .string = owner_secret_key } }, - .{ .key = "parents", .value = .{ .string_array = parents } }, - .{ .key = "content", .value = .{ .string = content } }, - .{ .key = "descendants", .value = .{ .descendants = descendants } }, - }); - defer self.allocator.free(req_body); - const resp = try self.doRequest(.POST, "/v1/graph", req_body) orelse return error.JsonError; - defer self.allocator.free(resp); - return json_helpers.parsePutResult(self.allocator, resp, "address"); - } - - /// Retrieve a graph entry by address. - pub fn graphEntryGet(self: *Client, address: []const u8) !GraphEntry { - const path = try std.fmt.allocPrint(self.allocator, "/v1/graph/{s}", .{address}); - defer self.allocator.free(path); - const resp = try self.doRequest(.GET, path, null) orelse return error.JsonError; - defer self.allocator.free(resp); - return json_helpers.parseGraphEntry(self.allocator, resp); - } - - /// Check if a graph entry exists at the given address. - pub fn graphEntryExists(self: *Client, address: []const u8) !bool { - const path = try std.fmt.allocPrint(self.allocator, "/v1/graph/{s}", .{address}); - defer self.allocator.free(path); - _ = self.doRequest(.HEAD, path, null) catch |err| { - if (err == error.NotFound) return false; - return err; - }; - return true; - } - - /// Estimate the cost of creating a graph entry. - pub fn graphEntryCost(self: *Client, public_key: []const u8) ![]const u8 { - const req_body = try json_helpers.buildJsonBody(self.allocator, &.{ - .{ .key = "public_key", .value = .{ .string = public_key } }, - }); - defer self.allocator.free(req_body); - const resp = try self.doRequest(.POST, "/v1/graph/cost", req_body) orelse return error.JsonError; - defer self.allocator.free(resp); - return json_helpers.parseCost(self.allocator, resp); - } - // --- Wallet --- /// Get the wallet's public address. diff --git a/antd-zig/src/models.zig b/antd-zig/src/models.zig index dbe57a2..049456d 100644 --- a/antd-zig/src/models.zig +++ b/antd-zig/src/models.zig @@ -22,38 +22,6 @@ pub const PutResult = struct { } }; -/// A descendant entry in a graph node. -pub const GraphDescendant = struct { - public_key: []const u8, - content: []const u8, - - pub fn deinit(self: GraphDescendant, allocator: Allocator) void { - allocator.free(self.public_key); - allocator.free(self.content); - } -}; - -/// A DAG node retrieved from the network. -pub const GraphEntry = struct { - owner: []const u8, - parents: []const []const u8, - content: []const u8, - descendants: []const GraphDescendant, - - pub fn deinit(self: GraphEntry, allocator: Allocator) void { - allocator.free(self.owner); - for (self.parents) |p| { - allocator.free(p); - } - allocator.free(self.parents); - allocator.free(self.content); - for (self.descendants) |d| { - d.deinit(allocator); - } - allocator.free(self.descendants); - } -}; - /// A single entry in a file archive. pub const ArchiveEntry = struct { path: []const u8, diff --git a/antd/README.md b/antd/README.md index 9f94548..610eaf7 100644 --- a/antd/README.md +++ b/antd/README.md @@ -102,11 +102,6 @@ On startup, antd writes a `daemon.port` file containing the actual REST port, gR | `GET` | `/v1/wallet/address` | Get wallet public address | | `GET` | `/v1/wallet/balance` | Get token and gas balances | | `POST` | `/v1/wallet/approve` | Approve token spend for payment contracts | -| **Graph** *(stub)* | | | -| `POST` | `/v1/graph` | Create a graph entry | -| `GET` | `/v1/graph/{address}` | Get a graph entry | -| `HEAD` | `/v1/graph/{address}` | Check graph entry existence | -| `POST` | `/v1/graph/cost` | Estimate graph entry cost | | **Archives** *(stub)* | | | | `GET` | `/v1/archives/public/{address}` | Get archive manifest | | `POST` | `/v1/archives/public` | Create archive manifest | @@ -118,7 +113,6 @@ gRPC services mirror the REST API. Proto definitions are in `proto/antd/v1/`: - `HealthService` — Health check - `DataService` — Public/private data operations - `ChunkService` — Raw chunk operations -- `GraphService` — Graph entry operations *(stub)* - `FileService` — File upload/download *(stub)* - `EventService` — Event streaming @@ -133,7 +127,6 @@ antd/ │ ├── common.proto │ ├── data.proto │ ├── chunks.proto -│ ├── graph.proto │ ├── files.proto │ └── events.proto └── src/ @@ -150,7 +143,6 @@ antd/ │ ├── files.rs # File/dir upload/download/cost │ ├── upload.rs # External signer two-phase upload │ ├── wallet.rs # Wallet address/balance/approve - │ ├── graph.rs # Graph entries (stub) │ └── events.rs # Event streaming (stub) └── grpc/ # Tonic gRPC service ├── mod.rs diff --git a/antd/build.rs b/antd/build.rs index cbe8537..b19c967 100644 --- a/antd/build.rs +++ b/antd/build.rs @@ -7,7 +7,6 @@ fn main() -> Result<(), Box> { "proto/antd/v1/health.proto", "proto/antd/v1/data.proto", "proto/antd/v1/chunks.proto", - "proto/antd/v1/graph.proto", "proto/antd/v1/files.proto", "proto/antd/v1/events.proto", ], diff --git a/antd/openapi.yaml b/antd/openapi.yaml index 3c0a5d9..904bf55 100644 --- a/antd/openapi.yaml +++ b/antd/openapi.yaml @@ -15,8 +15,6 @@ tags: description: Public and private data storage - name: chunks description: Raw chunk operations - - name: graph - description: DAG graph entry operations - name: files description: File and directory upload/download @@ -230,95 +228,6 @@ paths: "502": $ref: "#/components/responses/NetworkError" - /v1/graph/{addr}: - get: - tags: [graph] - operationId: graphEntryGet - summary: Read graph entry - description: Read a graph entry (DAG node) from the network. - parameters: - - name: addr - in: path - required: true - schema: - type: string - description: Hex address of the graph entry - responses: - "200": - description: Graph entry data - content: - application/json: - schema: - $ref: "#/components/schemas/GraphEntryGetResponse" - "404": - $ref: "#/components/responses/NotFound" - head: - tags: [graph] - operationId: graphEntryCheckExistence - summary: Check graph entry existence - parameters: - - name: addr - in: path - required: true - schema: - type: string - responses: - "200": - description: Graph entry exists - "404": - description: Graph entry not found - - /v1/graph: - post: - tags: [graph] - operationId: graphEntryPut - summary: Create graph entry - description: Create a new graph entry (DAG node) on the network. - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/GraphEntryPutRequest" - responses: - "200": - description: Graph entry created - content: - application/json: - schema: - $ref: "#/components/schemas/GraphEntryPutResponse" - "400": - $ref: "#/components/responses/BadRequest" - "402": - $ref: "#/components/responses/PaymentError" - "500": - $ref: "#/components/responses/InternalError" - "502": - $ref: "#/components/responses/NetworkError" - - /v1/graph/cost: - post: - tags: [graph] - operationId: graphEntryCost - summary: Estimate graph entry cost - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/GraphEntryCostRequest" - responses: - "200": - description: Cost estimate - content: - application/json: - schema: - $ref: "#/components/schemas/CostResponse" - "400": - $ref: "#/components/responses/BadRequest" - "502": - $ref: "#/components/responses/NetworkError" - /v1/files/upload/public: post: tags: [files] @@ -583,71 +492,6 @@ components: type: string description: Base64-encoded chunk data - # -- Graph -- - GraphDescendant: - type: object - required: [public_key, content] - properties: - public_key: - type: string - description: Hex-encoded public key - content: - type: string - description: Hex-encoded content (32 bytes) - - GraphEntryPutRequest: - type: object - required: [owner_secret_key, parents, content, descendants] - properties: - owner_secret_key: - type: string - description: Hex-encoded owner secret key - parents: - type: array - items: - type: string - description: List of hex-encoded parent public keys - content: - type: string - description: Hex-encoded content (32 bytes) - descendants: - type: array - items: - $ref: "#/components/schemas/GraphDescendant" - - GraphEntryPutResponse: - type: object - required: [cost, address] - properties: - cost: - type: string - address: - type: string - - GraphEntryGetResponse: - type: object - required: [owner, parents, content, descendants] - properties: - owner: - type: string - parents: - type: array - items: - type: string - content: - type: string - descendants: - type: array - items: - $ref: "#/components/schemas/GraphDescendant" - - GraphEntryCostRequest: - type: object - required: [public_key] - properties: - public_key: - type: string - # -- Files -- FileUploadRequest: type: object diff --git a/antd/proto/antd/v1/graph.proto b/antd/proto/antd/v1/graph.proto deleted file mode 100644 index 820e406..0000000 --- a/antd/proto/antd/v1/graph.proto +++ /dev/null @@ -1,49 +0,0 @@ -syntax = "proto3"; - -package antd.v1; - -option csharp_namespace = "Antd.V1"; - -import "antd/v1/common.proto"; - -service GraphService { - rpc Get(GetGraphEntryRequest) returns (GetGraphEntryResponse); - rpc CheckExistence(CheckGraphEntryRequest) returns (GraphExistsResponse); - rpc Put(PutGraphEntryRequest) returns (PutGraphEntryResponse); - rpc GetCost(GraphEntryCostRequest) returns (Cost); -} - -message GetGraphEntryRequest { - string address = 1; // hex -} - -message GetGraphEntryResponse { - string owner = 1; - repeated string parents = 2; - string content = 3; // hex, 32 bytes - repeated GraphDescendant descendants = 4; -} - -message CheckGraphEntryRequest { - string address = 1; // hex -} - -message GraphExistsResponse { - bool exists = 1; -} - -message PutGraphEntryRequest { - string owner_secret_key = 1; // hex - repeated string parents = 2; // hex public keys - string content = 3; // hex, 32 bytes - repeated GraphDescendant descendants = 4; -} - -message PutGraphEntryResponse { - Cost cost = 1; - string address = 2; // hex -} - -message GraphEntryCostRequest { - string public_key = 1; // hex -} diff --git a/antd/src/grpc/mod.rs b/antd/src/grpc/mod.rs index 24e3704..fc5905e 100644 --- a/antd/src/grpc/mod.rs +++ b/antd/src/grpc/mod.rs @@ -13,14 +13,12 @@ use service::pb::{ data_service_server::DataServiceServer, event_service_server::EventServiceServer, file_service_server::FileServiceServer, - graph_service_server::GraphServiceServer, health_service_server::HealthServiceServer, }; pub async fn serve(listener: TcpListener, state: Arc) -> Result<(), Box> { let data_svc = DataServiceServer::new(service::DataServiceImpl { state: state.clone() }); let chunk_svc = ChunkServiceServer::new(service::ChunkServiceImpl { state: state.clone() }); - let graph_svc = GraphServiceServer::new(service::GraphServiceImpl { state: state.clone() }); let file_svc = FileServiceServer::new(service::FileServiceImpl { state: state.clone() }); let event_svc = EventServiceServer::new(service::EventServiceImpl { state: state.clone() }); let health_svc = HealthServiceServer::new(service::HealthServiceImpl { network: state.network.clone() }); @@ -32,7 +30,6 @@ pub async fn serve(listener: TcpListener, state: Arc) -> Result<(), Bo .add_service(health_svc) .add_service(data_svc) .add_service(chunk_svc) - .add_service(graph_svc) .add_service(file_svc) .add_service(event_svc) .serve_with_incoming(TcpListenerStream::new(listener)) diff --git a/antd/src/grpc/service.rs b/antd/src/grpc/service.rs index 360adf4..0524949 100644 --- a/antd/src/grpc/service.rs +++ b/antd/src/grpc/service.rs @@ -117,28 +117,6 @@ impl pb::chunk_service_server::ChunkService for ChunkServiceImpl { } } -// ── GraphService ── - -pub struct GraphServiceImpl { - pub state: Arc, -} - -#[tonic::async_trait] -impl pb::graph_service_server::GraphService for GraphServiceImpl { - async fn get(&self, _r: Request) -> Result, Status> { - Err(not_implemented("graph get")) - } - async fn check_existence(&self, _r: Request) -> Result, Status> { - Err(not_implemented("graph check existence")) - } - async fn put(&self, _r: Request) -> Result, Status> { - Err(not_implemented("graph put")) - } - async fn get_cost(&self, _r: Request) -> Result, Status> { - Err(not_implemented("graph cost")) - } -} - // ── FileService ── pub struct FileServiceImpl { diff --git a/antd/src/rest/graph.rs b/antd/src/rest/graph.rs deleted file mode 100644 index ed9461f..0000000 --- a/antd/src/rest/graph.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::sync::Arc; - -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use axum::Json; - -use crate::error::AntdError; -use crate::state::AppState; -use crate::types::*; - -// TODO: Implement graph operations on top of ant-node chunk protocol. -// Graph entry types exist in ant-node but the client API for graph -// operations is not yet available. - -pub async fn graph_entry_get( - State(_state): State>, - Path(_addr): Path, -) -> Result, AntdError> { - Err(AntdError::Internal("graph operations not yet implemented yet".into())) -} - -pub async fn graph_entry_check_existence( - State(_state): State>, - Path(_addr): Path, -) -> Result { - Err(AntdError::Internal("graph operations not yet implemented yet".into())) -} - -pub async fn graph_entry_put( - State(_state): State>, - Json(_req): Json, -) -> Result, AntdError> { - Err(AntdError::Internal("graph operations not yet implemented yet".into())) -} - -pub async fn graph_entry_cost( - State(_state): State>, - Json(_req): Json, -) -> Result, AntdError> { - Err(AntdError::Internal("graph cost not yet implemented yet".into())) -} diff --git a/antd/src/rest/mod.rs b/antd/src/rest/mod.rs index 4bd1a42..bc8120f 100644 --- a/antd/src/rest/mod.rs +++ b/antd/src/rest/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::extract::State; use axum::http::{HeaderValue, Method}; -use axum::routing::{get, head, post}; +use axum::routing::{get, post}; use axum::{Json, Router}; use tower_http::cors::CorsLayer; @@ -13,7 +13,6 @@ pub mod chunks; pub mod data; pub mod events; pub mod files; -pub mod graph; pub mod upload; pub mod wallet; @@ -31,11 +30,6 @@ pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router // Chunks .route("/v1/chunks/{addr}", get(chunks::chunk_get)) .route("/v1/chunks", post(chunks::chunk_put)) - // Graph - .route("/v1/graph/{addr}", get(graph::graph_entry_get)) - .route("/v1/graph/{addr}", head(graph::graph_entry_check_existence)) - .route("/v1/graph", post(graph::graph_entry_put)) - .route("/v1/graph/cost", post(graph::graph_entry_cost)) // Files .route("/v1/files/upload/public", post(files::file_upload_public)) .route("/v1/files/download/public", post(files::file_download_public)) diff --git a/antd/src/types.rs b/antd/src/types.rs index 3ea2a25..0eb7bcf 100644 --- a/antd/src/types.rs +++ b/antd/src/types.rs @@ -57,41 +57,6 @@ pub struct ChunkGetResponse { pub data: String, // base64 } -// ── Graph ── - -#[derive(Deserialize)] -pub struct GraphEntryPutRequest { - pub owner_secret_key: String, // hex - pub parents: Vec, // hex public keys - pub content: String, // hex, 32 bytes - pub descendants: Vec, -} - -#[derive(Serialize, Deserialize)] -pub struct GraphDescendantDto { - pub public_key: String, // hex - pub content: String, // hex, 32 bytes -} - -#[derive(Serialize)] -pub struct GraphEntryPutResponse { - pub cost: String, - pub address: String, -} - -#[derive(Serialize)] -pub struct GraphEntryGetResponse { - pub owner: String, - pub parents: Vec, - pub content: String, - pub descendants: Vec, -} - -#[derive(Deserialize)] -pub struct GraphEntryCostRequest { - pub public_key: String, // hex -} - // ── External Signer (two-phase upload) ── #[derive(Deserialize)] diff --git a/docs/architecture.md b/docs/architecture.md index 899d672..92f2046 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,21 +75,6 @@ Download: address ──▶ antd ──▶ local path Use for: file hosting, static websites, media storage. -### Append-Only Data - -#### Graph Entries - -DAG (Directed Acyclic Graph) nodes. Each entry has an owner, content, parent links, and descendant links. - -``` -GraphEntry (owned by key K) - ├── content: 0x... (32 bytes) - ├── parents: [addr1, addr2] - └── descendants: [{public_key, content}, ...] -``` - -Use for: linked data structures, version history, social graphs, dependency trees. - ## Payment Model Every write operation costs network tokens (measured in "atto tokens" — 1 token = 10^18 atto). @@ -118,32 +103,14 @@ print(f"Paid {result.cost} atto tokens") | Access control | IAM policies | Cryptographic keys | | Redundancy | Configurable | Built-in (network-wide replication) | | Privacy | Trust the provider | Self-encrypting (zero knowledge) | -| Mutability | Overwrite anything | Immutable by design (append-only graphs for linked structures) | +| Mutability | Overwrite anything | Immutable by design | | Cost model | Per-request + storage/month | One-time write fee, reads are free | ## Key Design Patterns -### Pattern 1: Linked History with Graph Entries - -Build append-only logs or version chains using graph entries. - -```python -import os - -# First entry (no parents) -key1 = os.urandom(32).hex() -content1 = os.urandom(32).hex() -entry1 = client.graph_entry_put(key1, parents=[], content=content1, descendants=[]) - -# Second entry links to the first -key2 = os.urandom(32).hex() -content2 = os.urandom(32).hex() -entry2 = client.graph_entry_put(key2, parents=[entry1.address], content=content2, descendants=[]) -``` - ## Security Model -- **Secret keys**: 32-byte hex-encoded keys used for graph entries and private data operations. +- **Secret keys**: 32-byte hex-encoded keys used for private data operations. - **Never share secret keys**: Treat them like passwords. The public key (derived from the secret key) is safe to share. - **Private data**: Self-encrypted using the network's encryption scheme. The data map is needed for decryption. - **No access revocation**: Once data is public, it cannot be made private. Plan your key management accordingly. diff --git a/docs/missing-features.md b/docs/missing-features.md index 000ff91..c4883d9 100644 --- a/docs/missing-features.md +++ b/docs/missing-features.md @@ -78,7 +78,7 @@ Critical — this is the most immediately useful feature. Developers currently f - The FFI layer (`ffi/rust/ant-ffi/src/keys.rs` and `key_derivation.rs`) has full BLS key types: `SecretKey`, `PublicKey`, `MainSecretKey`, `MainPubkey`, `DerivedSecretKey`, `DerivedPubkey`, `DerivationIndex`, `Signature` - SDK examples generate throwaway keys with `os.urandom(32).hex()` — no persistence or reuse -- Graph operations accept a raw hex secret key string — no identity concept +- SDK examples generate throwaway keys with `os.urandom(32).hex()` — no persistence or reuse - The daemon loads its wallet key from `AUTONOMI_WALLET_KEY` env var at startup - No key-related REST/gRPC endpoints exist @@ -173,7 +173,6 @@ Start with Approach A since the BLS derived key primitives already exist in the ### What exists today -- `HEAD /v1/graph/{addr}` checks graph entry existence (returns 200 or 404) - No existence check for data or chunks - No replication status API - No re-upload or pinning mechanism @@ -326,7 +325,7 @@ Medium — progress events are valuable for UX but not blocking. Resumable uploa 1. Update data and get a stable pointer to the latest version 2. Version history or changelog for a piece of data -3. High-level mutable reference abstraction built on top of graph entries +3. High-level mutable reference abstraction 4. Diff or compare between versions >> As there is no mutable data, this is out of scope. Also this would be handled by a higher level application @@ -334,7 +333,7 @@ Medium — progress events are valuable for UX but not blocking. Resumable uploa ## Higher-Level Abstractions 1. JSON document store (serialize and deserialize objects) -2. Key-value store built on graph entries +2. Key-value store 3. Append-only log abstraction 4. Pub/sub or message queue patterns 5. DNS-like naming (human-readable names to addresses) diff --git a/docs/quickstart-cpp.md b/docs/quickstart-cpp.md index 041838d..8b5b43d 100644 --- a/docs/quickstart-cpp.md +++ b/docs/quickstart-cpp.md @@ -132,44 +132,6 @@ client.dir_download_public(dir_result.address, "/path/to/output_dir"); auto cost = client.file_cost("/path/to/file.txt"); ``` -## Graph Entries (DAG Nodes) - -```cpp -#include -#include - -std::random_device rd; -std::mt19937 gen(rd()); -std::uniform_int_distribution dist(0, 255); - -auto random_hex = [&](size_t bytes) { - std::string hex; - hex.reserve(bytes * 2); - for (size_t i = 0; i < bytes; ++i) - std::format_to(std::back_inserter(hex), "{:02x}", dist(gen)); - return hex; -}; - -auto key = random_hex(32); -auto content = random_hex(32); - -// Create root node -auto result = client.graph_entry_put( - key, - {}, // parents - content, - {} // descendants -); -std::println("Graph entry: {}", result.address); - -// Read -auto entry = client.graph_entry_get(result.address); -std::println("Owner: {}", entry.owner); -std::println("Content: {}", entry.content); - -// Check existence -bool exists = client.graph_entry_exists(result.address); -``` ## Error Handling @@ -213,7 +175,6 @@ cmake --build build ./build/02_data ./build/03_chunks ./build/04_files -./build/05_graph ./build/06_private ``` diff --git a/docs/quickstart-csharp.md b/docs/quickstart-csharp.md index 713149e..3960f87 100644 --- a/docs/quickstart-csharp.md +++ b/docs/quickstart-csharp.md @@ -112,32 +112,6 @@ await client.DirDownloadPublicAsync(dirResult.Address, "/path/to/output_dir"); var cost = await client.FileCostAsync("/path/to/file.txt"); ``` -## Graph Entries (DAG Nodes) - -```csharp -var secretKey = Convert.ToHexString( - RandomNumberGenerator.GetBytes(32) -).ToLower(); -var content = Convert.ToHexString( - RandomNumberGenerator.GetBytes(32) -).ToLower(); - -// Create root node -var result = await client.GraphEntryPutAsync( - secretKey, - parents: new List(), - content: content, - descendants: new List() -); - -// Read -var entry = await client.GraphEntryGetAsync(result.Address); -Console.WriteLine($"Owner: {entry.Owner}"); -Console.WriteLine($"Parents: {entry.Parents.Count}"); - -// Check existence -var exists = await client.GraphEntryExistsAsync(result.Address); -``` ## Error Handling @@ -188,7 +162,6 @@ dotnet run -- 1 # Connect dotnet run -- 2 # Public data dotnet run -- 3 # Chunks dotnet run -- 4 # Files -dotnet run -- 5 # Graph entries dotnet run -- 6 # Private data dotnet run -- all # Run all examples ``` diff --git a/docs/quickstart-dart.md b/docs/quickstart-dart.md index ae4a8b7..ad94328 100644 --- a/docs/quickstart-dart.md +++ b/docs/quickstart-dart.md @@ -96,39 +96,6 @@ await client.dirDownloadPublic(dirResult.address, '/path/to/output_dir'); final cost = await client.fileCost('/path/to/file.txt'); ``` -## Graph Entries (DAG Nodes) - -```dart -import 'dart:math'; -import 'dart:typed_data'; - -String randomHex(int bytes) { - final rng = Random.secure(); - return List.generate(bytes, (_) => rng.nextInt(256).toRadixString(16).padLeft(2, '0')).join(); -} - -final key = randomHex(32); -final content = randomHex(32); - -// Create a root node -final result = await client.graphEntryPut( - key, - parents: [], - content: content, - descendants: [], -); -print('Graph entry: ${result.address}'); - -// Read -final entry = await client.graphEntryGet(result.address); -print('Owner: ${entry.owner}'); -print('Content: ${entry.content}'); -print('Parents: ${entry.parents}'); -print('Descendants: ${entry.descendants}'); - -// Check existence -final exists = await client.graphEntryExists(result.address); -``` ## Error Handling diff --git a/docs/quickstart-elixir.md b/docs/quickstart-elixir.md index 48fc861..80c9cea 100644 --- a/docs/quickstart-elixir.md +++ b/docs/quickstart-elixir.md @@ -105,30 +105,6 @@ IO.puts("File address: #{result.address}") {:ok, cost} = Antd.Client.file_cost(client, "/path/to/file.txt") ``` -## Graph Entries (DAG Nodes) - -```elixir -key = :crypto.strong_rand_bytes(32) |> Base.encode16(case: :lower) -content = :crypto.strong_rand_bytes(32) |> Base.encode16(case: :lower) - -# Create a root node -{:ok, result} = Antd.Client.graph_entry_put(client, key, - parents: [], - content: content, - descendants: [] -) -IO.puts("Graph entry: #{result.address}") - -# Read -{:ok, entry} = Antd.Client.graph_entry_get(client, result.address) -IO.puts("Owner: #{entry.owner}") -IO.puts("Content: #{entry.content}") -IO.puts("Parents: #{inspect(entry.parents)}") -IO.puts("Descendants: #{inspect(entry.descendants)}") - -# Check existence -{:ok, exists} = Antd.Client.graph_entry_exists(client, result.address) -``` ## Error Handling diff --git a/docs/quickstart-java.md b/docs/quickstart-java.md index 5d01304..672f9ac 100644 --- a/docs/quickstart-java.md +++ b/docs/quickstart-java.md @@ -127,44 +127,6 @@ client.dirDownloadPublic(dirResult.address(), "/path/to/output_dir"); long cost = client.fileCost("/path/to/file.txt"); ``` -## Graph Entries (DAG Nodes) - -```java -import com.autonomi.antd.GraphDescendant; -import java.security.SecureRandom; -import java.util.HexFormat; -import java.util.List; - -var random = new SecureRandom(); -var hex = HexFormat.of(); - -byte[] keyBytes = new byte[32]; -random.nextBytes(keyBytes); -String key = hex.formatHex(keyBytes); - -byte[] contentBytes = new byte[32]; -random.nextBytes(contentBytes); -String content = hex.formatHex(contentBytes); - -// Create root node -var result = client.graphEntryPut( - key, - List.of(), // parents - content, - List.of() // descendants -); -System.out.println("Graph entry: " + result.address()); - -// Read -var entry = client.graphEntryGet(result.address()); -System.out.println("Owner: " + entry.owner()); -System.out.println("Content: " + entry.content()); -System.out.println("Parents: " + entry.parents()); -System.out.println("Descendants: " + entry.descendants()); - -// Check existence -boolean exists = client.graphEntryExists(result.address()); -``` ## Error Handling @@ -207,7 +169,6 @@ cd antd-java ./gradlew run --args="2" # Public data ./gradlew run --args="3" # Chunks ./gradlew run --args="4" # Files -./gradlew run --args="5" # Graph entries ./gradlew run --args="6" # Private data ./gradlew run --args="all" # Run all examples diff --git a/docs/quickstart-kotlin.md b/docs/quickstart-kotlin.md index 04629a3..b24689a 100644 --- a/docs/quickstart-kotlin.md +++ b/docs/quickstart-kotlin.md @@ -111,30 +111,6 @@ client.dirDownloadPublic(dirResult.address, "/path/to/output_dir") val cost = client.fileCost("/path/to/file.txt") ``` -## Graph Entries (DAG Nodes) - -```kotlin -val secretKey = ByteArray(32).also { SecureRandom().nextBytes(it) } - .joinToString("") { "%02x".format(it) } -val content = ByteArray(32).also { SecureRandom().nextBytes(it) } - .joinToString("") { "%02x".format(it) } - -// Create root node -val result = client.graphEntryPut( - secretKey, - parents = emptyList(), - content = content, - descendants = emptyList(), -) - -// Read -val entry = client.graphEntryGet(result.address) -println("Owner: ${entry.owner}") -println("Parents: ${entry.parents.size}") - -// Check existence -val exists = client.graphEntryExists(result.address) -``` ## Error Handling @@ -176,7 +152,6 @@ cd antd-kotlin ./gradlew :examples:run --args="2" # Public data ./gradlew :examples:run --args="3" # Chunks ./gradlew :examples:run --args="4" # Files -./gradlew :examples:run --args="5" # Graph entries ./gradlew :examples:run --args="6" # Private data ./gradlew :examples:run --args="all" # Run all examples ``` diff --git a/docs/quickstart-lua.md b/docs/quickstart-lua.md index 8990366..f727986 100644 --- a/docs/quickstart-lua.md +++ b/docs/quickstart-lua.md @@ -108,41 +108,6 @@ local cost, err = client:file_cost("/path/to/file.txt") if err then error(err) end ``` -## Graph Entries (DAG Nodes) - -```lua -local function random_hex(bytes) - local hex = {} - for i = 1, bytes do - hex[i] = string.format("%02x", math.random(0, 255)) - end - return table.concat(hex) -end - -local key = random_hex(32) -local content = random_hex(32) - --- Create a root node -local result, err = client:graph_entry_put(key, { - parents = {}, - content = content, - descendants = {}, -}) -if err then error(err) end -print("Graph entry: " .. result.address) - --- Read -local entry, err = client:graph_entry_get(result.address) -if err then error(err) end -print("Owner: " .. entry.owner) -print("Content: " .. entry.content) -print("Parents: " .. #entry.parents) -print("Descendants: " .. #entry.descendants) - --- Check existence -local exists, err = client:graph_entry_exists(result.address) -if err then error(err) end -``` ## Error Handling diff --git a/docs/quickstart-php.md b/docs/quickstart-php.md index 8a69481..8303ce1 100644 --- a/docs/quickstart-php.md +++ b/docs/quickstart-php.md @@ -90,31 +90,6 @@ $client->dirDownloadPublic($result->address, "/path/to/output_dir"); $cost = $client->fileCost("/path/to/file.txt"); ``` -## Graph Entries (DAG Nodes) - -```php -$key = bin2hex(random_bytes(32)); -$content = bin2hex(random_bytes(32)); - -// Create a root node -$result = $client->graphEntryPut( - $key, - parents: [], - content: $content, - descendants: [], -); -echo "Graph entry: {$result->address}\n"; - -// Read -$entry = $client->graphEntryGet($result->address); -echo "Owner: {$entry->owner}\n"; -echo "Content: {$entry->content}\n"; -echo "Parents: " . count($entry->parents) . "\n"; -echo "Descendants: " . count($entry->descendants) . "\n"; - -// Check existence -$exists = $client->graphEntryExists($result->address); -``` ## Error Handling diff --git a/docs/quickstart-python.md b/docs/quickstart-python.md index 69f026b..855a007 100644 --- a/docs/quickstart-python.md +++ b/docs/quickstart-python.md @@ -92,34 +92,6 @@ client.dir_download_public(result.address, "/path/to/output_dir") cost = client.file_cost("/path/to/file.txt") ``` -## Graph Entries (DAG Nodes) - -```python -import os -from antd import GraphDescendant - -key = os.urandom(32).hex() -content = os.urandom(32).hex() - -# Create a root node -result = client.graph_entry_put( - key, - parents=[], - content=content, - descendants=[], -) -print(f"Graph entry: {result.address}") - -# Read -entry = client.graph_entry_get(result.address) -print(f"Owner: {entry.owner}") -print(f"Content: {entry.content}") -print(f"Parents: {entry.parents}") -print(f"Descendants: {entry.descendants}") - -# Check existence -exists = client.graph_entry_exists(result.address) -``` ## Async Usage diff --git a/docs/quickstart-ruby.md b/docs/quickstart-ruby.md index e0e7298..aec0c6e 100644 --- a/docs/quickstart-ruby.md +++ b/docs/quickstart-ruby.md @@ -91,33 +91,6 @@ client.dir_download_public(result.address, "/path/to/output_dir") cost = client.file_cost("/path/to/file.txt") ``` -## Graph Entries (DAG Nodes) - -```ruby -require 'securerandom' - -key = SecureRandom.hex(32) -content = SecureRandom.hex(32) - -# Create a root node -result = client.graph_entry_put( - key, - parents: [], - content: content, - descendants: [], -) -puts "Graph entry: #{result.address}" - -# Read -entry = client.graph_entry_get(result.address) -puts "Owner: #{entry.owner}" -puts "Content: #{entry.content}" -puts "Parents: #{entry.parents}" -puts "Descendants: #{entry.descendants}" - -# Check existence -exists = client.graph_entry_exists(result.address) -``` ## Error Handling diff --git a/docs/quickstart-rust.md b/docs/quickstart-rust.md index 44b4a24..c1326fe 100644 --- a/docs/quickstart-rust.md +++ b/docs/quickstart-rust.md @@ -104,35 +104,6 @@ client.dir_download_public(&dir_result.address, "/path/to/output_dir").await?; let cost = client.file_cost("/path/to/file.txt").await?; ``` -## Graph Entries (DAG Nodes) - -```rust -use antd_client::GraphDescendant; -use rand::Rng; - -let mut rng = rand::thread_rng(); -let key = hex::encode(rng.gen::<[u8; 32]>()); -let content = hex::encode(rng.gen::<[u8; 32]>()); - -// Create root node -let result = client.graph_entry_put( - &key, - &[], // parents - &content, - &[], // descendants -).await?; -println!("Graph entry: {}", result.address); - -// Read -let entry = client.graph_entry_get(&result.address).await?; -println!("Owner: {}", entry.owner); -println!("Content: {}", entry.content); -println!("Parents: {:?}", entry.parents); -println!("Descendants: {:?}", entry.descendants); - -// Check existence -let exists = client.graph_entry_exists(&result.address).await?; -``` ## Error Handling @@ -172,7 +143,6 @@ cargo run --example 01_connect cargo run --example 02_data cargo run --example 03_chunks cargo run --example 04_files -cargo run --example 05_graph cargo run --example 06_private ``` diff --git a/docs/quickstart-swift.md b/docs/quickstart-swift.md index fe909ea..2baf2bf 100644 --- a/docs/quickstart-swift.md +++ b/docs/quickstart-swift.md @@ -121,33 +121,6 @@ try await client.dirDownloadPublic(address: dirResult.address, destPath: "/path/ let cost = try await client.fileCost(path: "/path/to/file.txt", isPublic: true, includeArchive: false) ``` -## Graph Entries (DAG Nodes) - -```swift -var keyBytes = [UInt8](repeating: 0, count: 32) -_ = SecRandomCopyBytes(kSecRandomDefault, keyBytes.count, &keyBytes) -let secretKey = keyBytes.map { String(format: "%02x", $0) }.joined() - -var contentBytes = [UInt8](repeating: 0, count: 32) -_ = SecRandomCopyBytes(kSecRandomDefault, contentBytes.count, &contentBytes) -let content = contentBytes.map { String(format: "%02x", $0) }.joined() - -// Create root node -let result = try await client.graphEntryPut( - ownerSecretKey: secretKey, - parents: [], - content: content, - descendants: [] -) - -// Read -let entry = try await client.graphEntryGet(address: result.address) -print("Owner: \(entry.owner)") -print("Parents: \(entry.parents.count)") - -// Check existence -let exists = try await client.graphEntryExists(address: result.address) -``` ## Error Handling @@ -189,7 +162,6 @@ swift run AntdExamples 1 # Connect swift run AntdExamples 2 # Public data swift run AntdExamples 3 # Chunks swift run AntdExamples 4 # Files -swift run AntdExamples 5 # Graph entries swift run AntdExamples 6 # Private data swift run AntdExamples all # Run all examples ``` diff --git a/docs/quickstart-zig.md b/docs/quickstart-zig.md index dff02fe..ecb6a31 100644 --- a/docs/quickstart-zig.md +++ b/docs/quickstart-zig.md @@ -134,39 +134,6 @@ try client.dirDownloadPublic(dir_result.address, "/path/to/output_dir"); const cost = try client.fileCost("/path/to/file.txt"); ``` -## Graph Entries (DAG Nodes) - -```zig -var prng = std.Random.DefaultPrng.init(blk: { - var seed: u64 = undefined; - std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable; - break :blk seed; -}); -const random = prng.random(); - -var key_buf: [32]u8 = undefined; -random.bytes(&key_buf); -const key = std.fmt.bytesToHex(key_buf, .lower); - -var content_buf: [32]u8 = undefined; -random.bytes(&content_buf); -const content = std.fmt.bytesToHex(content_buf, .lower); - -// Create root node -const result = try client.graphEntryPut(&key, &.{}, &content, &.{}); -defer result.deinit(allocator); -std.debug.print("Graph entry: {s}\n", .{result.address}); - -// Read -const entry = try client.graphEntryGet(result.address); -defer entry.deinit(allocator); -std.debug.print("Owner: {s}\n", .{entry.owner}); -std.debug.print("Content: {s}\n", .{entry.content}); - -// Check existence -const exists = try client.graphEntryExists(result.address); -std.debug.print("Exists: {}\n", .{exists}); -``` ## Error Handling @@ -214,7 +181,6 @@ zig build run -- 1 # Connect zig build run -- 2 # Public data zig build run -- 3 # Chunks zig build run -- 4 # Files -zig build run -- 5 # Graph entries zig build run -- 6 # Private data zig build run -- all # Run all examples ``` diff --git a/ffi/README.md b/ffi/README.md index 475d125..82786fc 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -24,7 +24,6 @@ ffi/ │ ├── network.rs # Network wrapper │ ├── payment.rs # Wallet, PaymentOption │ ├── data.rs # Chunk, ChunkAddress, DataAddress, DataMapChunk -│ ├── graph.rs # GraphEntry, GraphEntryAddress │ └── files.rs # PublicArchive, PrivateArchive, Metadata ├── csharp/ │ ├── AntFfi.sln @@ -108,7 +107,6 @@ All methods are exposed on the `Client` object: | **Init** | `init()`, `init_local()`, `init_with_peers()` | | **Chunks** | `chunk_get`, `chunk_put`, `chunk_cost` | | **Data** | `data_get_public`, `data_put_public`, `data_get`, `data_put`, `data_cost` | -| **Graph** | `graph_entry_get`, `graph_entry_put`, `graph_entry_cost`, `graph_entry_check_existence` | | **Files** | `file_upload`, `file_upload_public`, `file_download`, `file_download_public`, `file_cost` | | **Directories** | `dir_upload`, `dir_upload_public`, `dir_download`, `dir_download_public` | | **Archives** | `archive_get`, `archive_get_public`, `archive_put`, `archive_put_public`, `archive_cost` | diff --git a/llms-full.txt b/llms-full.txt index 7f27423..3df959a 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -264,46 +264,6 @@ Retrieve a raw chunk by hex address. Response: `{"data": ""}` -### Graph - -DAG (directed acyclic graph) entries with parent/descendant relationships. - -#### `POST /v1/graph` -Create a graph entry. - -Request: -```json -{ - "owner_secret_key": "", - "parents": ["", ...], - "content": "", - "descendants": [{"public_key": "", "content": ""}, ...] -} -``` -Response: `{"cost": "", "address": ""}` - -#### `GET /v1/graph/{addr}` -Read a graph entry. - -Response: -```json -{ - "owner": "", - "parents": ["", ...], - "content": "", - "descendants": [{"public_key": "", "content": ""}, ...] -} -``` - -#### `HEAD /v1/graph/{addr}` -Check if a graph entry exists. Returns 200 or 404. - -#### `POST /v1/graph/cost` -Estimate cost to create a graph entry. - -Request: `{"public_key": ""}` -Response: `{"cost": ""}` - ### Files Upload and download files/directories from the local filesystem. @@ -438,12 +398,6 @@ Default target: `localhost:50051`. Package: `antd.v1`. - `Get(GetChunkRequest{address}) → GetChunkResponse{data: bytes}` - `Put(PutChunkRequest{data: bytes}) → PutChunkResponse{cost, address}` -### GraphService (graph.proto) -- `Get(GetGraphEntryRequest{address}) → GetGraphEntryResponse{owner, parents, content, descendants}` -- `CheckExistence(CheckGraphEntryRequest{address}) → GraphExistsResponse{exists: bool}` -- `Put(PutGraphEntryRequest{owner_secret_key, parents, content, descendants}) → PutGraphEntryResponse{cost, address}` -- `GetCost(GraphEntryCostRequest{public_key}) → Cost{atto_tokens}` - ### FileService (files.proto) - `UploadPublic(UploadFileRequest{path}) → UploadPublicResponse{cost, address}` - `DownloadPublic(DownloadPublicRequest{address, dest_path}) → DownloadResponse{}` @@ -470,8 +424,6 @@ JS/TS, PHP, Lua, and Zig are REST-only. |------|--------|-------------| | HealthStatus | ok: bool, network: string | Health check result | | PutResult | cost: string, address: string | Result from any create/put operation | -| GraphDescendant | public_key: string, content: string (hex 32B) | Graph descendant | -| GraphEntry | owner, parents: list[string], content: string, descendants: list[GraphDescendant] | Graph entry | | ArchiveEntry | path, address, created: int, modified: int, size: int | Archive file entry | | Archive | entries: list[ArchiveEntry] | Archive manifest | @@ -530,11 +482,7 @@ client.dataCost(data: Buffer): Promise client.chunkPut(data: Buffer): Promise client.chunkGet(address: string): Promise -// Graph -client.graphEntryPut(ownerSecretKey: string, parents: string[], content: string, descendants: GraphDescendant[]): Promise -client.graphEntryGet(address: string): Promise -client.graphEntryExists(address: string): Promise -client.graphEntryCost(publicKey: string): Promise + // Files client.fileUploadPublic(path: string): Promise @@ -575,11 +523,7 @@ client.data_cost(data: bytes) → str client.chunk_put(data: bytes) → PutResult client.chunk_get(address: str) → bytes -# Graph -client.graph_entry_put(owner_secret_key: str, parents: list[str], content: str, descendants: list[GraphDescendant]) → PutResult -client.graph_entry_get(address: str) → GraphEntry -client.graph_entry_exists(address: str) → bool -client.graph_entry_cost(public_key: str) → str + # Files client.file_upload_public(path: str) → PutResult @@ -620,11 +564,7 @@ Task DataCostAsync(byte[] data); Task ChunkPutAsync(byte[] data); Task ChunkGetAsync(string address); -// Graph -Task GraphEntryPutAsync(string ownerSecretKey, List parents, string content, List descendants); -Task GraphEntryGetAsync(string address); -Task GraphEntryExistsAsync(string address); -Task GraphEntryCostAsync(string publicKey); + // Files Task FileUploadPublicAsync(string path); @@ -665,11 +605,7 @@ suspend fun dataCost(data: ByteArray): String suspend fun chunkPut(data: ByteArray): PutResult suspend fun chunkGet(address: String): ByteArray -// Graph -suspend fun graphEntryPut(ownerSecretKey: String, parents: List, content: String, descendants: List): PutResult -suspend fun graphEntryGet(address: String): GraphEntry -suspend fun graphEntryExists(address: String): Boolean -suspend fun graphEntryCost(publicKey: String): String + // Files suspend fun fileUploadPublic(path: String): PutResult @@ -712,11 +648,7 @@ func dataCost(_ data: Data) async throws -> String func chunkPut(_ data: Data) async throws -> PutResult func chunkGet(address: String) async throws -> Data -// Graph -func graphEntryPut(ownerSecretKey: String, parents: [String], content: String, descendants: [GraphDescendant]) async throws -> PutResult -func graphEntryGet(address: String) async throws -> GraphEntry -func graphEntryExists(address: String) async throws -> Bool -func graphEntryCost(publicKey: String) async throws -> String + // Files func fileUploadPublic(path: String) async throws -> PutResult @@ -756,11 +688,7 @@ func (c *Client) DataCost(ctx context.Context, data []byte) (string, error) func (c *Client) ChunkPut(ctx context.Context, data []byte) (*PutResult, error) func (c *Client) ChunkGet(ctx context.Context, address string) ([]byte, error) -// Graph -func (c *Client) GraphEntryPut(ctx context.Context, ownerSecretKey string, parents []string, content string, descendants []GraphDescendant) (*PutResult, error) -func (c *Client) GraphEntryGet(ctx context.Context, address string) (*GraphEntry, error) -func (c *Client) GraphEntryExists(ctx context.Context, address string) (bool, error) -func (c *Client) GraphEntryCost(ctx context.Context, publicKey string) (string, error) + // Files func (c *Client) FileUploadPublic(ctx context.Context, path string) (*PutResult, error) @@ -803,11 +731,7 @@ String dataCost(byte[] data) throws AntdException PutResult chunkPut(byte[] data) throws AntdException byte[] chunkGet(String address) throws AntdException -// Graph -PutResult graphEntryPut(String ownerSecretKey, List parents, String content, List descendants) throws AntdException -GraphEntry graphEntryGet(String address) throws AntdException -boolean graphEntryExists(String address) throws AntdException -String graphEntryCost(String publicKey) throws AntdException + // Files PutResult fileUploadPublic(String path) throws AntdException @@ -832,7 +756,7 @@ boolean walletApprove() throws AntdException ```rust // cargo add antd-client -use antd_client::{Client, PutResult, HealthStatus, GraphEntry, GraphDescendant, Archive, AntdError}; +use antd_client::{Client, PutResult, HealthStatus, Archive, AntdError}; let client = Client::new("http://localhost:8082"); // REST, localhost:8082 @@ -850,11 +774,7 @@ async fn data_cost(&self, data: &[u8]) -> Result async fn chunk_put(&self, data: &[u8]) -> Result async fn chunk_get(&self, address: &str) -> Result, AntdError> -// Graph -async fn graph_entry_put(&self, owner_secret_key: &str, parents: &[String], content: &str, descendants: &[GraphDescendant]) -> Result -async fn graph_entry_get(&self, address: &str) -> Result -async fn graph_entry_exists(&self, address: &str) -> Result -async fn graph_entry_cost(&self, public_key: &str) -> Result + // Files async fn file_upload_public(&self, path: &str) -> Result @@ -898,11 +818,7 @@ std::string dataCost(const std::vector& data) antd::PutResult chunkPut(const std::vector& data) std::vector chunkGet(const std::string& address) -// Graph -antd::PutResult graphEntryPut(const std::string& ownerSecretKey, const std::vector& parents, const std::string& content, const std::vector& descendants) -antd::GraphEntry graphEntryGet(const std::string& address) -bool graphEntryExists(const std::string& address) -std::string graphEntryCost(const std::string& publicKey) + // Files antd::PutResult fileUploadPublic(const std::string& path) @@ -946,11 +862,7 @@ client.data_cost(data) → String client.chunk_put(data) → PutResult client.chunk_get(address) → String -# Graph -client.graph_entry_put(owner_secret_key:, parents:, content:, descendants:) → PutResult -client.graph_entry_get(address) → GraphEntry -client.graph_entry_exists?(address) → Boolean -client.graph_entry_cost(public_key) → String + # Files client.file_upload_public(path) → PutResult @@ -991,11 +903,7 @@ $client->dataCost(string $data): string $client->chunkPut(string $data): PutResult $client->chunkGet(string $address): string -// Graph -$client->graphEntryPut(string $ownerSecretKey, array $parents, string $content, array $descendants): PutResult -$client->graphEntryGet(string $address): GraphEntry -$client->graphEntryExists(string $address): bool -$client->graphEntryCost(string $publicKey): string + // Files $client->fileUploadPublic(string $path): PutResult @@ -1036,11 +944,7 @@ Future dataCost(List data) Future chunkPut(List data) Future> chunkGet(String address) -// Graph -Future graphEntryPut(String ownerSecretKey, List parents, String content, List descendants) -Future graphEntryGet(String address) -Future graphEntryExists(String address) -Future graphEntryCost(String publicKey) + // Files Future fileUploadPublic(String path) @@ -1081,11 +985,7 @@ client:data_cost(data) → string client:chunk_put(data) → PutResult client:chunk_get(address) → string --- Graph -client:graph_entry_put(owner_secret_key, parents, content, descendants) → PutResult -client:graph_entry_get(address) → GraphEntry -client:graph_entry_exists(address) → boolean -client:graph_entry_cost(public_key) → string + -- Files client:file_upload_public(path) → PutResult @@ -1124,11 +1024,7 @@ Antd.Client.data_cost(client, data) :: {:ok, String.t()} | {:error, term()} Antd.Client.chunk_put(client, data) :: {:ok, PutResult.t()} | {:error, term()} Antd.Client.chunk_get(client, address) :: {:ok, binary()} | {:error, term()} -# Graph -Antd.Client.graph_entry_put(client, owner_secret_key, parents, content, descendants) :: {:ok, PutResult.t()} | {:error, term()} -Antd.Client.graph_entry_get(client, address) :: {:ok, GraphEntry.t()} | {:error, term()} -Antd.Client.graph_entry_exists?(client, address) :: {:ok, boolean()} | {:error, term()} -Antd.Client.graph_entry_cost(client, public_key) :: {:ok, String.t()} | {:error, term()} + # Files Antd.Client.file_upload_public(client, path) :: {:ok, PutResult.t()} | {:error, term()} @@ -1170,11 +1066,7 @@ fn dataCost(self: *Client, data: []const u8) ![]u8 fn chunkPut(self: *Client, data: []const u8) !PutResult fn chunkGet(self: *Client, address: []const u8) ![]u8 -// Graph -fn graphEntryPut(self: *Client, owner_secret_key: []const u8, parents: []const []const u8, content: []const u8, descendants: []const GraphDescendant) !PutResult -fn graphEntryGet(self: *Client, address: []const u8) !GraphEntry -fn graphEntryExists(self: *Client, address: []const u8) !bool -fn graphEntryCost(self: *Client, public_key: []const u8) ![]u8 + // Files fn fileUploadPublic(self: *Client, path: []const u8) !PutResult @@ -1201,10 +1093,6 @@ fn walletApprove(self: *Client) !bool | `retrieve_data` | `data_get_public` / `data_get_private` | Retrieve text from network | | `upload_file` | `file_upload_public` / `dir_upload_public` | Upload file or directory | | `download_file` | `file_download_public` / `dir_download_public` | Download file or directory | -| `create_graph_entry` | `graph_entry_put` | Create graph entry | -| `get_graph_entry` | `graph_entry_get` | Read graph entry | -| `graph_entry_exists` | `graph_entry_exists` | Check graph entry existence | -| `graph_entry_cost` | `graph_entry_cost` | Estimate graph entry cost | | `chunk_put` | `chunk_put` | Store raw chunk | | `chunk_get` | `chunk_get` | Retrieve raw chunk | | `archive_get` | `archive_get_public` | List archive entries | diff --git a/llms.txt b/llms.txt index 990280b..0079ea7 100644 --- a/llms.txt +++ b/llms.txt @@ -49,10 +49,6 @@ data = client.data_get_public(result.address) | POST | `/v1/data/cost` | Estimate data storage cost | | GET | `/v1/chunks/{addr}` | Get raw chunk by address | | POST | `/v1/chunks` | Store raw chunk | -| GET | `/v1/graph/{addr}` | Read graph entry (DAG node) | -| HEAD | `/v1/graph/{addr}` | Check graph entry existence | -| POST | `/v1/graph` | Create graph entry | -| POST | `/v1/graph/cost` | Estimate graph entry cost | | POST | `/v1/files/upload/public` | Upload file to network (accepts optional `payment_mode`) | | POST | `/v1/files/download/public` | Download file from network | | POST | `/v1/dirs/upload/public` | Upload directory to network (accepts optional `payment_mode`) | @@ -74,7 +70,6 @@ data = client.data_get_public(result.address) | HealthService | Check | health.proto | | DataService | GetPublic, PutPublic, StreamPublic, GetPrivate, PutPrivate, GetCost | data.proto | | ChunkService | Get, Put | chunks.proto | -| GraphService | Get, CheckExistence, Put, GetCost | graph.proto | | FileService | UploadPublic, DownloadPublic, DirUploadPublic, DirDownloadPublic, ArchiveGetPublic, ArchivePutPublic, GetFileCost | files.proto | | EventService | Subscribe | events.proto | diff --git a/scripts/test-api.ps1 b/scripts/test-api.ps1 index 3919a99..e80dba4 100644 --- a/scripts/test-api.ps1 +++ b/scripts/test-api.ps1 @@ -8,7 +8,7 @@ ## .\scripts\test-api.ps1 ## ## Currently tests health + chunks (working with ant-node). -## Data, files, graph, and private data are not yet implemented. +## Data, files, and private data are not yet implemented. $ErrorActionPreference = "Continue" @@ -97,7 +97,7 @@ Write-Host "" # ══════════════════════════════════════════════════════════════════════ # Test 01: Health Check # ══════════════════════════════════════════════════════════════════════ -Write-Host "[01/06] Health Check" -ForegroundColor Yellow +Write-Host "[01/05] Health Check" -ForegroundColor Yellow $health = Api-Get "/health" Assert-Eq "status is ok" "ok" $health.status @@ -108,14 +108,14 @@ Write-Host " Network: $($health.network)" -ForegroundColor Gray # Test 02: Public Data (SKIPPED — not yet implemented for ant-node) # ══════════════════════════════════════════════════════════════════════ Write-Host "" -Write-Host "[02/06] Public Data" -ForegroundColor Yellow +Write-Host "[02/05] Public Data" -ForegroundColor Yellow Skip-Test "public data put/get/cost" # ══════════════════════════════════════════════════════════════════════ # Test 03: Raw Chunks - store and retrieve # ══════════════════════════════════════════════════════════════════════ Write-Host "" -Write-Host "[03/06] Chunks" -ForegroundColor Yellow +Write-Host "[03/05] Chunks" -ForegroundColor Yellow $chunkPayload = "Raw chunk content for direct storage" $chunkB64 = B64Encode $chunkPayload @@ -140,21 +140,14 @@ if ($chunkPut -and $chunkPut.address) { # Test 04: Files (SKIPPED — not yet implemented for ant-node) # ══════════════════════════════════════════════════════════════════════ Write-Host "" -Write-Host "[04/06] Files" -ForegroundColor Yellow +Write-Host "[04/05] Files" -ForegroundColor Yellow Skip-Test "file upload/download/cost" # ══════════════════════════════════════════════════════════════════════ -# Test 05: Graph Entries (SKIPPED — not yet implemented for ant-node) +# Test 05: Private Data (SKIPPED — not yet implemented for ant-node) # ══════════════════════════════════════════════════════════════════════ Write-Host "" -Write-Host "[05/06] Graph Entries" -ForegroundColor Yellow -Skip-Test "graph entry put/get/exists/cost" - -# ══════════════════════════════════════════════════════════════════════ -# Test 06: Private Data (SKIPPED — not yet implemented for ant-node) -# ══════════════════════════════════════════════════════════════════════ -Write-Host "" -Write-Host "[06/06] Private Data" -ForegroundColor Yellow +Write-Host "[05/05] Private Data" -ForegroundColor Yellow Skip-Test "private data put/get" # ══════════════════════════════════════════════════════════════════════ diff --git a/scripts/test-api.sh b/scripts/test-api.sh index 215415d..3478441 100644 --- a/scripts/test-api.sh +++ b/scripts/test-api.sh @@ -7,7 +7,7 @@ set -uo pipefail ## Prerequisite: antd running on local testnet (./scripts/start-local.sh) ## ## Currently tests health + chunks (working with ant-node). -## Data, files, graph, and private data are not yet implemented. +## Data, files, and private data are not yet implemented. BASE_URL="${ANTD_BASE_URL:-http://localhost:8082}" PASS=0 @@ -67,7 +67,7 @@ echo "" # ══════════════════════════════════════════════════════════════════════ # Test 01: Health Check # ══════════════════════════════════════════════════════════════════════ -echo -e "${YELLOW}[01/06] Health Check${NC}" +echo -e "${YELLOW}[01/05] Health Check${NC}" RESP=$(curl -s "$BASE_URL/health") STATUS=$(echo "$RESP" | jq -r '.status // empty') @@ -81,14 +81,14 @@ echo -e " ${GRAY}Network: $NETWORK${NC}" # Test 02: Public Data (SKIPPED) # ══════════════════════════════════════════════════════════════════════ echo "" -echo -e "${YELLOW}[02/06] Public Data${NC}" +echo -e "${YELLOW}[02/05] Public Data${NC}" skip_test "public data put/get/cost" # ══════════════════════════════════════════════════════════════════════ # Test 03: Raw Chunks — store and retrieve # ══════════════════════════════════════════════════════════════════════ echo "" -echo -e "${YELLOW}[03/06] Chunks${NC}" +echo -e "${YELLOW}[03/05] Chunks${NC}" CHUNK_PAYLOAD="Raw chunk content for direct storage" CHUNK_B64=$(b64encode "$CHUNK_PAYLOAD") @@ -119,21 +119,14 @@ fi # Test 04: Files (SKIPPED) # ══════════════════════════════════════════════════════════════════════ echo "" -echo -e "${YELLOW}[04/06] Files${NC}" +echo -e "${YELLOW}[04/05] Files${NC}" skip_test "file upload/download/cost" # ══════════════════════════════════════════════════════════════════════ -# Test 05: Graph Entries (SKIPPED) +# Test 05: Private Data (SKIPPED) # ══════════════════════════════════════════════════════════════════════ echo "" -echo -e "${YELLOW}[05/06] Graph Entries${NC}" -skip_test "graph entry put/get/exists/cost" - -# ══════════════════════════════════════════════════════════════════════ -# Test 06: Private Data (SKIPPED) -# ══════════════════════════════════════════════════════════════════════ -echo "" -echo -e "${YELLOW}[06/06] Private Data${NC}" +echo -e "${YELLOW}[05/05] Private Data${NC}" skip_test "private data put/get" # ══════════════════════════════════════════════════════════════════════ diff --git a/skill.md b/skill.md index 5456218..82ca6f2 100644 --- a/skill.md +++ b/skill.md @@ -55,12 +55,6 @@ This is the most important decision. Match the developer's use case to the right | **Chunks** | You need custom chunking logic | Advanced/low-level use cases only | | **Files** | Uploading local files or directories | Static sites, media hosting, backups | -### Append-only - -| Primitive | Use When | Example | -|-----------|----------|---------| -| **Graph Entries** | Building linked data structures | Version history, social graphs, audit logs | - ## Common Patterns ### Pattern 1: Immutable Data Storage @@ -96,18 +90,7 @@ data = client.data_get_private(result.address) **When to suggest this:** Developer wants encrypted storage that only they can access. -### Pattern 3: Graph (Linked History) - -DAG nodes with parent/descendant links for building append-only structures. - -```python -entry1 = client.graph_entry_put(key1, parents=[], content=content1, descendants=[]) -entry2 = client.graph_entry_put(key2, parents=[entry1.address], content=content2, descendants=[]) -``` - -**When to suggest this:** Developer needs an audit log, version chain, social graph, or any linked data structure. - -### Pattern 4: External Signer (Two-Phase Upload) +### Pattern 3: External Signer (Two-Phase Upload) When the application manages its own wallet (e.g. a browser wallet or hardware signer), use the two-phase upload flow instead of the daemon's built-in wallet: