diff --git a/README.md b/README.md index d101d85..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 @@ -29,6 +29,53 @@ 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`). + +### 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"`): + +- **`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 @@ -36,7 +83,7 @@ A developer-friendly SDK for the [Autonomi](https://autonomi.com) decentralized | 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 @@ -65,9 +112,9 @@ A developer-friendly SDK for the [Autonomi](https://autonomi.com) decentralized **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` +- **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): @@ -131,7 +178,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") @@ -139,6 +186,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) @@ -216,7 +266,7 @@ import ( ) func main() { - client := antd.NewClient(antd.DefaultBaseURL) + client, _ := antd.NewClientAutoDiscover() ctx := context.Background() health, err := client.Health(ctx) @@ -371,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/ant-dev/src/ant_dev/cmd_start.py b/ant-dev/src/ant_dev/cmd_start.py index 5c1a78e..02225a6 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) ── @@ -109,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) @@ -122,21 +142,22 @@ 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, }) 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(white(" Wallet: configured")) 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..98e3635 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") @@ -58,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 caa381e..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 @@ -8,33 +8,59 @@ 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() 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: - 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.") - print(f"Key: {key[:10]}...") else: print("Daemon is not healthy. Check: ant dev status") except (httpx.HTTPError, OSError): diff --git a/antd-cpp/README.md b/antd-cpp/README.md index 2af621d..b12dcec 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(); @@ -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. @@ -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 @@ -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/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-cpp/include/antd/client.hpp b/antd-cpp/include/antd/client.hpp index 3f4a0cd..b8ae7fa 100644 --- a/antd-cpp/include/antd/client.hpp +++ b/antd-cpp/include/antd/client.hpp @@ -1,19 +1,21 @@ #pragma once #include +#include #include #include #include #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 +36,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. @@ -42,13 +52,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); @@ -64,33 +74,16 @@ 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. - 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); @@ -104,6 +97,30 @@ 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(); + + /// 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); + + /// 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); + private: struct Impl; std::unique_ptr impl_; diff --git a/antd-cpp/include/antd/discover.hpp b/antd-cpp/include/antd/discover.hpp new file mode 100644 index 0000000..2b8da74 --- /dev/null +++ b/antd-cpp/include/antd/discover.hpp @@ -0,0 +1,19 @@ +#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 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, 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/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-cpp/include/antd/grpc_client.hpp b/antd-cpp/include/antd/grpc_client.hpp index e82489c..fdcc13c 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" @@ -16,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. @@ -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. @@ -68,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 3a1ea1c..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; @@ -46,4 +32,38 @@ 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 +}; + +/// 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 264f81c..3bd3510 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", ""), @@ -164,86 +168,16 @@ 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 // --------------------------------------------------------------------------- -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 +191,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", ""), @@ -325,4 +261,103 @@ 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", ""), + }; +} + +bool Client::wallet_approve() { + auto j = impl_->do_json("POST", "/v1/wallet/approve", json::object()); + 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; +} + +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(); + 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-cpp/src/discover.cpp b/antd-cpp/src/discover.cpp new file mode 100644 index 0000000..ddf7d1b --- /dev/null +++ b/antd-cpp/src/discover.cpp @@ -0,0 +1,120 @@ +#include "antd/discover.hpp" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace fs = std::filesystem; + +namespace antd { +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 { + 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: 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}; + + auto path = dir / kPortFileName; + std::ifstream ifs(path); + if (!ifs.is_open()) return {0, 0}; + + std::string line1, line2, line3; + if (!std::getline(ifs, line1)) return {0, 0}; + 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)}; +} + +} // 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.Tests/TestRunner.cs b/antd-csharp/Antd.Sdk.Tests/TestRunner.cs index 093e3cb..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 = "") @@ -74,7 +67,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"); @@ -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/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/Antd.Sdk/AntdGrpcClient.cs b/antd-csharp/Antd.Sdk/AntdGrpcClient.cs index ba5b750..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,10 +19,19 @@ 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); } + /// + /// 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); @@ -53,7 +61,7 @@ public async Task HealthAsync() // ── Data ── - public async Task DataPutPublicAsync(byte[] data) + public async Task DataPutPublicAsync(byte[] data, string? paymentMode = null) { try { @@ -73,7 +81,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 { @@ -125,67 +133,9 @@ 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) + public async Task FileUploadPublicAsync(string path, string? paymentMode = null) { try { @@ -204,7 +154,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 f0fa8ce..2e8e69e 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 ── @@ -81,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); } @@ -93,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); } @@ -125,41 +141,14 @@ 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) + 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); } @@ -168,9 +157,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); } @@ -210,6 +202,61 @@ 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); + } + + /// + /// Approves the wallet to spend tokens on payment contracts (one-time operation). + /// + public async Task WalletApproveAsync() + { + var resp = await PostJsonAsync("/v1/wallet/approve", new { }); + return resp.Approved; + } + + // ── External Signer (Two-Phase Upload) ── + + /// + /// Prepares a file upload for external signing. + /// + 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); + } + + /// + /// 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. + /// + 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( @@ -230,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, @@ -249,4 +286,31 @@ 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); + + 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/DaemonDiscovery.cs b/antd-csharp/Antd.Sdk/DaemonDiscovery.cs new file mode 100644 index 0000000..73e54cb --- /dev/null +++ b/antd-csharp/Antd.Sdk/DaemonDiscovery.cs @@ -0,0 +1,122 @@ +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 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 +{ + 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]); + + // 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 + { + return (0, 0); + } + } + + 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; + } + + /// + /// 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-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-csharp/Antd.Sdk/IAntdClient.cs b/antd-csharp/Antd.Sdk/IAntdClient.cs index 4ba37e8..b9f1871 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); @@ -16,18 +16,22 @@ 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); + 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); Task FileCostAsync(string path, bool isPublic = true, bool includeArchive = false); + + // Wallet + Task WalletAddressAsync(); + Task WalletBalanceAsync(); + Task WalletApproveAsync(); + + // External Signer (Two-Phase Upload) + Task PrepareUploadAsync(string path); + Task PrepareDataUploadAsync(byte[] data); + Task FinalizeUploadAsync(string uploadId, Dictionary txHashes); } diff --git a/antd-csharp/Antd.Sdk/Models.cs b/antd-csharp/Antd.Sdk/Models.cs index 8ff377c..70c9be7 100644 --- a/antd-csharp/Antd.Sdk/Models.cs +++ b/antd-csharp/Antd.Sdk/Models.cs @@ -6,14 +6,29 @@ 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); /// 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); + +/// 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-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 24444dd..094ff60 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) ); @@ -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-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..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 @@ -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 @@ -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/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..05c36e6 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) { @@ -126,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!); } @@ -140,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'); } @@ -178,57 +199,15 @@ 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. - 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!); } @@ -241,10 +220,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!); } @@ -283,4 +264,51 @@ 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!); + } + + /// 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; + } + + // --- 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!); + } + + /// 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, + 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/discover.dart b/antd-dart/lib/src/discover.dart new file mode 100644 index 0000000..440d7e5 --- /dev/null +++ b/antd-dart/lib/src/discover.dart @@ -0,0 +1,121 @@ +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 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) { + 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); + } + + // 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) { + 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/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-dart/lib/src/grpc_client.dart b/antd-dart/lib/src/grpc_client.dart index 74a0d2f..9911618 100644 --- a/antd-dart/lib/src/grpc_client.dart +++ b/antd-dart/lib/src/grpc_client.dart @@ -10,18 +10,16 @@ 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; +import 'discover.dart'; import 'errors.dart'; 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 @@ -39,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. @@ -86,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( @@ -105,6 +93,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', @@ -264,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 3c2b503..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. @@ -177,3 +108,126 @@ 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)'; +} + +/// 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-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 53b375d..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/`. @@ -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 @@ -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 0749bc4..476e9da 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 @@ -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 3956831..d3b5b8e 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 @@ -63,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"]}} @@ -75,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()} @@ -92,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"]}} @@ -104,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()} @@ -168,106 +204,19 @@ 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 # --------------------------------------------------------------------------- @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"]}} @@ -277,8 +226,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()} @@ -296,9 +245,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"]}} @@ -308,8 +262,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()} @@ -398,6 +352,154 @@ 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)) + + @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)) + + # --------------------------------------------------------------------------- + # 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 "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 + 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/discover.ex b/antd-elixir/lib/antd/discover.ex new file mode 100644 index 0000000..ba52b7c --- /dev/null +++ b/antd-elixir/lib/antd/discover.ex @@ -0,0 +1,142 @@ +defmodule Antd.Discover do + @moduledoc """ + Auto-discovers the antd daemon by reading the `daemon.port` file that antd + writes on startup. + + 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` + - 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, "")) + 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} + 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 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, _} -> + 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/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-elixir/lib/antd/grpc_client.ex b/antd-elixir/lib/antd/grpc_client.ex index ffa0132..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/`. """ @@ -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. @@ -201,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 450c3cf..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." @@ -73,3 +47,67 @@ 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 + +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/README.md b/antd-go/README.md index 9365ea4..dacb785 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 @@ -70,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 @@ -96,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 | |--------|-------------| @@ -118,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 b15272e..641fd83 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{ @@ -181,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 } @@ -204,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 } @@ -256,79 +287,17 @@ 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. -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 } @@ -345,10 +314,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 } @@ -415,3 +388,117 @@ 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 +} + +// 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 +} + +// --- 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 +} + +// 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. +// 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, + "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/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/discover.go b/antd-go/discover.go new file mode 100644 index 0000000..b752140 --- /dev/null +++ b/antd-go/discover.go @@ -0,0 +1,114 @@ +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: 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 == "" { + 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 + } + + // 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]) + } + 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 { + 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/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-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-go/grpc_client.go b/antd-go/grpc_client.go index 85469dc..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,19 @@ 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 +// 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 @@ -71,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 @@ -247,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 84d128b..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"` @@ -39,3 +25,38 @@ 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 +} + +// 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 { + 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-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 1f0e1c2..58f09d2 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 @@ -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 | @@ -171,14 +162,14 @@ 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`. +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 fc3db20..8d1b9ee 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); } @@ -184,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")); } @@ -195,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")); } @@ -225,51 +251,16 @@ 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) { - 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")); } @@ -280,7 +271,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")); } @@ -318,4 +315,93 @@ 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")); + } + + /** + * 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; + } + + // ── 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") + ); + } + + /** + * 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. + * + * @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/AsyncAntdClient.java b/antd-java/src/main/java/com/autonomi/antd/AsyncAntdClient.java index 5d83024..3a24ff5 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); @@ -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/DaemonDiscovery.java b/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java new file mode 100644 index 0000000..29dcd00 --- /dev/null +++ b/antd-java/src/main/java/com/autonomi/antd/DaemonDiscovery.java @@ -0,0 +1,165 @@ +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 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: + *

    + *
  • 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. + * 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 + */ + 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; + } + + // 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). + * + * @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..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,9 +74,22 @@ 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; + /** + * 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). */ @@ -113,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); } @@ -274,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/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-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/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/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-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-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 d961106..3da7996 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 @@ -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/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-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/discover.ts b/antd-js/src/discover.ts new file mode 100644 index 0000000..4e7681d --- /dev/null +++ b/antd-js/src/discover.ts @@ -0,0 +1,112 @@ +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 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(); + 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 }; + } + + // 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) { + 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/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 { - 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 }; } @@ -120,10 +141,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 }; } @@ -153,61 +174,12 @@ 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): 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 }; } @@ -218,10 +190,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 }; } @@ -271,4 +243,84 @@ 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 }; + } + + /** 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; + } + + // ---- 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, + }; + } + + /** 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, + 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/README.md b/antd-kotlin/README.md index a8f4528..0f31569 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") @@ -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/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 f470c01..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 @@ -8,11 +8,21 @@ 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) private val chunkStub = ChunkServiceGrpcKt.ChunkServiceCoroutineStub(channel) - private val graphStub = GraphServiceGrpcKt.GraphServiceCoroutineStub(channel) private val fileStub = FileServiceGrpcKt.FileServiceCoroutineStub(channel) override fun close() { @@ -38,7 +48,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) } @@ -48,7 +58,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) } @@ -75,46 +85,9 @@ 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): 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) } @@ -124,7 +97,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) } @@ -158,4 +131,18 @@ 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") + } + + override suspend fun walletApprove(): Boolean { + throw UnsupportedOperationException("walletApprove not available 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 1bf4fcd..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 @@ -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) @@ -92,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) } @@ -103,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) } @@ -133,49 +150,13 @@ class AntdRestClient( return fromB64(resp.data) } - // ── Graph ── + // ── Files ── - override suspend fun graphEntryPut( - ownerSecretKey: String, - parents: List, - content: String, - descendants: List, - ): PutResult { + override suspend fun fileUploadPublic(path: String, paymentMode: String?): 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) - }) - } - } + put("path", path) + if (paymentMode != null) put("payment_mode", paymentMode) }.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): PutResult { - val body = buildJsonObject { put("path", path) }.toString() val resp = postJson("/v1/files/upload/public", body) return PutResult(resp.cost, resp.address) } @@ -188,8 +169,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) } @@ -235,4 +219,53 @@ 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) + } + + /** 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 + } + + // ── 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) + } + + /** 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 { + 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/DaemonDiscovery.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt new file mode 100644 index 0000000..70670a7 --- /dev/null +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/DaemonDiscovery.kt @@ -0,0 +1,91 @@ +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 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 { + + 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() } + + // 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 + } catch (_: Exception) { + 0 to 0 + } + } + + /** + * 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 { + 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-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Exceptions.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Exceptions.kt index 0fce12f..d4121cb 100644 --- a/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Exceptions.kt +++ b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Exceptions.kt @@ -14,6 +14,7 @@ class PaymentException(message: String, statusCode: Int = 402) : AntdException(m class NetworkException(message: String, statusCode: Int = 502) : AntdException(message, statusCode) class TooLargeException(message: String, statusCode: Int = 413) : AntdException(message, statusCode) class InternalException(message: String, statusCode: Int = 500) : AntdException(message, statusCode) +class ServiceUnavailableException(message: String, statusCode: Int = 503) : AntdException(message, statusCode) internal object ExceptionMapping { @@ -25,6 +26,7 @@ internal object ExceptionMapping { 413 -> TooLargeException(body, statusCode) 500 -> InternalException(body, statusCode) 502 -> NetworkException(body, statusCode) + 503 -> ServiceUnavailableException(body, statusCode) else -> AntdException(body, statusCode) } 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..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 @@ -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 @@ -24,18 +24,22 @@ 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): 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 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 + + // 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-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt b/antd-kotlin/lib/src/main/kotlin/com/autonomi/sdk/Models.kt index e229a0c..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,18 +9,34 @@ 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) /** 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) + +/** 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 @@ -51,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, @@ -78,3 +80,42 @@ 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, +) + +@Serializable +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-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 74cb645..ba66ed1 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 @@ -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/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..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" })) @@ -153,7 +139,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() @@ -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 fa867fd..9a8ea5d 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) @@ -146,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 @@ -166,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 @@ -217,90 +228,20 @@ 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. -- @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 @@ -319,11 +260,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 @@ -401,4 +347,125 @@ 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 + +--- 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 + +-- ── 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 + +--- 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 +-- @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 } +-- @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..85d88cc --- /dev/null +++ b/antd-lua/src/antd/discover.lua @@ -0,0 +1,123 @@ +--- 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 + +--- 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() + 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 + + -- 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 + + 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/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-lua/src/antd/init.lua b/antd-lua/src/antd/init.lua index 0ff90a5..662dc20 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) @@ -27,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 ce1394e..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 @@ -78,4 +52,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/README.md b/antd-mcp/README.md index 3a12fe1..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 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 13 tools for AI agents. Works with Claude Desktop, Claude Code, and any MCP-compatible client. ## Installation @@ -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,50 +36,58 @@ 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 | # | 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 | -### Chunk Operations +### Wallet Operations | # | Tool | Description | |---|------|-------------| -| 7 | `chunk_put(data)` | Store a raw chunk (base64 input) | -| 8 | `chunk_get(address)` | Retrieve a chunk (base64 output) | +| 7 | `wallet_address()` | Get wallet public address | +| 8 | `wallet_balance()` | Get wallet token and gas balances | +| 9 | `wallet_approve()` | Approve wallet to spend tokens on payment contracts (one-time) | -### Graph Operations +### Chunk 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 | +| 10 | `chunk_put(data)` | Store a raw chunk (base64 input) | +| 11 | `chunk_get(address)` | Retrieve a chunk (base64 output) | ### Archive Operations | # | Tool | Description | |---|------|-------------| -| 13 | `archive_get(address)` | List files in an archive | -| 14 | `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 + +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 @@ -109,6 +119,7 @@ antd-mcp/ ├── pyproject.toml └── src/antd_mcp/ ├── __init__.py - ├── server.py # 14 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/discover.py b/antd-mcp/src/antd_mcp/discover.py new file mode 100644 index 0000000..82e37d2 --- /dev/null +++ b/antd-mcp/src/antd_mcp/discover.py @@ -0,0 +1,136 @@ +"""Port discovery for the antd daemon. + +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. +""" + +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 _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, 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() + 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 + + # 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 + + +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..855b3c7 100644 --- a/antd-mcp/src/antd_mcp/server.py +++ b/antd-mcp/src/antd_mcp/server.py @@ -12,18 +12,20 @@ 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 # --------------------------------------------------------------------------- # 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" @@ -83,12 +85,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. @@ -97,9 +103,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) @@ -148,12 +154,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. @@ -161,9 +171,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) @@ -265,7 +275,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 # --------------------------------------------------------------------------- @@ -293,7 +349,7 @@ async def chunk_put( # --------------------------------------------------------------------------- -# Tool 8: chunk_get +# Tool 10: chunk_get # --------------------------------------------------------------------------- @@ -320,39 +376,38 @@ async def chunk_get( # --------------------------------------------------------------------------- -# Tool 9: create_graph_entry +# Tool 11: archive_get # --------------------------------------------------------------------------- @mcp.tool() -async def create_graph_entry( - owner_secret_key: str, - content: str, - parents: list[str] | None = None, - descendants: list[dict] | None = None, +async def archive_get( + address: str, ) -> str: - """Create a graph entry (DAG node) on the Autonomi network. + """List files in a public archive 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. + address: Hex address of the archive. Returns: - JSON with graph entry address and cost, or error details. + JSON with list of archive entries (path, address, created, modified, size), + 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) + archive = await client.archive_get_public(address) + return _ok({ + "entries": [ + { + "path": e.path, + "address": e.address, + "created": e.created, + "modified": e.modified, + "size": e.size, + } + for e in archive.entries + ], + }, network) except AntdError as exc: return _err_antd(exc, network) except Exception as exc: @@ -360,35 +415,24 @@ async def create_graph_entry( # --------------------------------------------------------------------------- -# Tool 10: get_graph_entry +# Tool 12: wallet_approve # --------------------------------------------------------------------------- @mcp.tool() -async def get_graph_entry( - address: str, -) -> str: - """Read a graph entry from the Autonomi network. +async def wallet_approve() -> str: + """Approve the wallet to spend tokens on payment contracts. - Args: - address: Hex address of the graph entry. + 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 graph entry details (owner, parents, content, descendants), - or error details. + JSON with approved boolean, 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) + result = await client.wallet_approve() + return _ok({"approved": result}, network) except AntdError as exc: return _err_antd(exc, network) except Exception as exc: @@ -396,26 +440,38 @@ async def get_graph_entry( # --------------------------------------------------------------------------- -# Tool 11: graph_entry_exists +# Tool 13: archive_put # --------------------------------------------------------------------------- @mcp.tool() -async def graph_entry_exists( - address: str, +async def archive_put( + entries: list[dict], ) -> str: - """Check if a graph entry exists on the Autonomi network. + """Create a public archive from a list of file entries. Args: - address: Hex address of the graph entry. + entries: List of entry objects, each with "path", "address", "created", + "modified", and "size" fields. Returns: - JSON with exists boolean, or error details. + JSON with archive address and cost, or error details. """ client, network = _get_ctx() try: - exists = await client.graph_entry_exists(address) - return _ok({"exists": exists}, network) + from antd.models import Archive, ArchiveEntry + archive = Archive(entries=[ + ArchiveEntry( + path=e["path"], + address=e["address"], + created=e["created"], + modified=e["modified"], + size=e["size"], + ) + for e in entries + ]) + result = await client.archive_put_public(archive) + return _ok({"address": result.address, "cost": result.cost}, network) except AntdError as exc: return _err_antd(exc, network) except Exception as exc: @@ -423,26 +479,44 @@ async def graph_entry_exists( # --------------------------------------------------------------------------- -# Tool 12: graph_entry_cost +# Tool 14: prepare_upload # --------------------------------------------------------------------------- @mcp.tool() -async def graph_entry_cost( - public_key: str, +async def prepare_upload( + path: str, ) -> str: - """Estimate the cost to create a graph entry. + """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: - public_key: Hex-encoded public key of the graph entry owner. + path: Absolute path to the local file to upload. Returns: - JSON with cost in atto tokens, or error details. + 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: - cost = await client.graph_entry_cost(public_key) - return _ok({"cost": cost}, network) + 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: @@ -450,37 +524,45 @@ async def graph_entry_cost( # --------------------------------------------------------------------------- -# Tool 13: archive_get +# Tool 15: prepare_data_upload # --------------------------------------------------------------------------- @mcp.tool() -async def archive_get( - address: str, +async def prepare_data_upload( + data: str, ) -> str: - """List files in a public archive on the Autonomi network. + """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: - address: Hex address of the archive. + data: Base64-encoded bytes to upload. Returns: - JSON with list of archive entries (path, address, created, modified, size), - or error details. + 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: - archive = await client.archive_get_public(address) + raw = base64.b64decode(data) + result = await client.prepare_data_upload(raw) return _ok({ - "entries": [ + "upload_id": result.upload_id, + "payments": [ { - "path": e.path, - "address": e.address, - "created": e.created, - "modified": e.modified, - "size": e.size, + "quote_hash": p.quote_hash, + "rewards_address": p.rewards_address, + "amount": p.amount, } - for e in archive.entries + 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) @@ -489,38 +571,28 @@ async def archive_get( # --------------------------------------------------------------------------- -# Tool 14: archive_put +# Tool 16: finalize_upload # --------------------------------------------------------------------------- @mcp.tool() -async def archive_put( - entries: list[dict], +async def finalize_upload( + upload_id: str, + tx_hashes: dict[str, str], ) -> str: - """Create a public archive from a list of file entries. + """Finalize a two-phase upload after payment transactions are submitted. Args: - entries: List of entry objects, each with "path", "address", "created", - "modified", and "size" fields. + upload_id: The upload ID returned by prepare_upload. + tx_hashes: Map of quote_hash to tx_hash for each payment. Returns: - JSON with archive address and cost, or error details. + JSON with address (hex) and chunks_stored count. """ client, network = _get_ctx() try: - from antd.models import Archive, ArchiveEntry - archive = Archive(entries=[ - ArchiveEntry( - path=e["path"], - address=e["address"], - created=e["created"], - modified=e["modified"], - size=e["size"], - ) - for e in entries - ]) - result = await client.archive_put_public(archive) - return _ok({"address": result.address, "cost": result.cost}, network) + 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: diff --git a/antd-php/README.md b/antd-php/README.md index 7172a46..ef39da4 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 @@ -78,14 +78,6 @@ $client = new AntdClient('http://localhost:8080', 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/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/src/AntdClient.php b/antd-php/src/AntdClient.php index 7fdb39e..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; @@ -25,7 +23,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 +34,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 @@ -193,11 +209,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'] ?? '', @@ -209,11 +227,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'] ?? '', @@ -245,11 +265,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'] ?? '', @@ -261,11 +283,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'] ?? '', @@ -373,187 +397,18 @@ 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 --- /** * 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'] ?? '', @@ -565,11 +420,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'] ?? '', @@ -604,11 +461,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'] ?? '', @@ -620,11 +479,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'] ?? '', @@ -750,6 +611,86 @@ 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'] ?? '', + ], + ); + } + + /** + * 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. */ @@ -778,4 +719,122 @@ 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'] ?? '', + ], + ); + } + + /** + * 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. + * + * @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-php/src/DaemonDiscovery.php b/antd-php/src/DaemonDiscovery.php new file mode 100644 index 0000000..5f1c6bd --- /dev/null +++ b/antd-php/src/DaemonDiscovery.php @@ -0,0 +1,159 @@ += 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); + 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-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 @@ + $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 @@ -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 16021e7..bd970c7 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") @@ -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/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/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/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-py/src/antd/__init__.py b/antd-py/src/antd/__init__.py index 3c1bc8c..925db70 100644 --- a/antd-py/src/antd/__init__.py +++ b/antd-py/src/antd/__init__.py @@ -14,11 +14,15 @@ from .models import ( Archive, ArchiveEntry, - GraphDescendant, - GraphEntry, + FinalizeUploadResult, HealthStatus, + PaymentInfo, + PrepareUploadResult, PutResult, + WalletAddress, + WalletBalance, ) +from ._discover import discover_daemon_url, discover_grpc_target from .exceptions import ( AntdError, AlreadyExistsError, @@ -32,6 +36,9 @@ ) __all__ = [ + # Discovery + "discover_daemon_url", + "discover_grpc_target", # Factory functions "AntdClient", "AsyncAntdClient", @@ -39,9 +46,9 @@ "HealthStatus", "Archive", "ArchiveEntry", - "GraphDescendant", - "GraphEntry", "PutResult", + "WalletAddress", + "WalletBalance", # Exceptions "AntdError", "AlreadyExistsError", @@ -61,7 +68,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 +87,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..150b3be --- /dev/null +++ b/antd-py/src/antd/_discover.py @@ -0,0 +1,127 @@ +"""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 + - 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 + +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 + + # 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: + 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..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 @@ -56,12 +52,26 @@ 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) 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: @@ -137,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: @@ -256,12 +217,26 @@ 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) 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: @@ -337,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 de64ef1..1ac7d3f 100644 --- a/antd-py/src/antd/_rest.py +++ b/antd-py/src/antd/_rest.py @@ -11,10 +11,13 @@ from .models import ( Archive, ArchiveEntry, - GraphDescendant, - GraphEntry, + FinalizeUploadResult, HealthStatus, + PaymentInfo, + PrepareUploadResult, PutResult, + WalletAddress, + WalletBalance, ) if TYPE_CHECKING: @@ -43,10 +46,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() @@ -66,8 +84,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"]) @@ -77,8 +98,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"]) @@ -106,48 +130,13 @@ 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) -> 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"]) @@ -159,8 +148,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"]) @@ -206,14 +198,120 @@ 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"]) + + 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) + + # --- 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 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. + + 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.""" - 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() @@ -233,8 +331,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"]) @@ -244,8 +345,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"]) @@ -273,48 +377,13 @@ 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) -> 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"]) @@ -326,8 +395,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"]) @@ -372,3 +444,94 @@ 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"]) + + 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) + + # --- 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 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. + + 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/exceptions.py b/antd-py/src/antd/exceptions.py index 939d2e8..d9432ab 100644 --- a/antd-py/src/antd/exceptions.py +++ b/antd-py/src/antd/exceptions.py @@ -39,6 +39,11 @@ class NetworkError(AntdError): pass +class ServiceUnavailableError(AntdError): + """Service unavailable, e.g. wallet not configured (HTTP 503 / gRPC UNAVAILABLE).""" + pass + + class TooLargeError(AntdError): """Payload too large (HTTP 413 / gRPC RESOURCE_EXHAUSTED).""" pass @@ -58,6 +63,7 @@ class InternalError(AntdError): 413: TooLargeError, 500: InternalError, 502: NetworkError, + 503: ServiceUnavailableError, } diff --git a/antd-py/src/antd/models.py b/antd-py/src/antd/models.py index 21d6e8e..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.""" @@ -48,3 +32,42 @@ 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 + + +@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-py/tests/test_discover.py b/antd-py/tests/test_discover.py new file mode 100644 index 0000000..d457ad8 --- /dev/null +++ b/antd-py/tests/test_discover.py @@ -0,0 +1,172 @@ +"""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, + _is_process_alive, + _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 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") + 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/README.md b/antd-ruby/README.md index 79bed6d..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/`. @@ -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 @@ -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/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." 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..af244d6 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) @@ -32,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 @@ -48,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 @@ -87,67 +104,15 @@ 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. # @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 @@ -163,8 +128,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 @@ -219,6 +186,88 @@ 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 + + # 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 + + # --- 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 + + # 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 + # @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/discover.rb b/antd-ruby/lib/antd/discover.rb new file mode 100644 index 0000000..92f033d --- /dev/null +++ b/antd-ruby/lib/antd/discover.rb @@ -0,0 +1,128 @@ +# 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 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 + # - 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]) + pid = parse_pid(lines[2]) + + return [0, 0] if pid > 0 && !process_alive?(pid) + + [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 + + # @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-ruby/lib/antd/errors.rb b/antd-ruby/lib/antd/errors.rb index c0193bd..3ddf710 100644 --- a/antd-ruby/lib/antd/errors.rb +++ b/antd-ruby/lib/antd/errors.rb @@ -51,6 +51,11 @@ class NetworkError < AntdError def initialize(message) = super(message, status_code: 502) end + # Service unavailable, e.g. wallet not configured (HTTP 503). + class ServiceUnavailableError < AntdError + def initialize(message) = super(message, status_code: 503) + end + # Returns the appropriate error type for an HTTP status code. def self.error_for_status(code, message) case code @@ -61,6 +66,7 @@ def self.error_for_status(code, message) when 413 then TooLargeError.new(message) when 500 then InternalError.new(message) when 502 then NetworkError.new(message) + when 503 then ServiceUnavailableError.new(message) else AntdError.new(message, status_code: code) end end diff --git a/antd-ruby/lib/antd/grpc_client.rb b/antd-ruby/lib/antd/grpc_client.rb index 3e40d5e..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,16 +21,27 @@ 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. + # + # 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 @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 @@ -111,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 71dde71..9b24802 100644 --- a/antd-ruby/lib/antd/models.rb +++ b/antd-ruby/lib/antd/models.rb @@ -7,15 +7,24 @@ 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) # 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) + + # 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-ruby/test/test_client.rb b/antd-ruby/test/test_client.rb index d371c8c..a044d61 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) @@ -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 a02b9c8..6af6dd5 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 @@ -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 96fdaf5..6b83dfa 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() @@ -145,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(); @@ -174,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(); @@ -247,115 +272,19 @@ 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. - 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(); @@ -381,12 +310,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(); @@ -489,4 +422,139 @@ 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"), + }) + } + + /// 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)) + } + + // --- 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"), + }) + } + + /// 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, + 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/discover.rs b/antd-rust/src/discover.rs new file mode 100644 index 0000000..7600228 --- /dev/null +++ b/antd-rust/src/discover.rs @@ -0,0 +1,206 @@ +//! Port discovery for the antd daemon. +//! +//! The antd daemon writes a `daemon.port` file on startup containing the REST +//! 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; +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, +/// 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, +/// 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?; + Some(format!("http://127.0.0.1:{grpc}")) +} + +/// 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` +/// - 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 { + 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("8082\n50051\n"); + assert_eq!(result, Some((8082, Some(50051)))); + } + + #[test] + fn parse_single_line_port_file() { + let result = parse("8082\n"); + assert_eq!(result, Some((8082, None))); + } + + #[test] + fn parse_empty_returns_none() { + let result = parse(""); + assert_eq!(result, None); + } + + #[test] + fn parse_invalid_port_returns_none() { + let result = parse("notanumber\n"); + assert_eq!(result, None); + } + + #[test] + fn parse_with_whitespace() { + 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-rust/src/errors.rs b/antd-rust/src/errors.rs index 8a4ccad..aa07325 100644 --- a/antd-rust/src/errors.rs +++ b/antd-rust/src/errors.rs @@ -35,6 +35,10 @@ pub enum AntdError { #[error("antd error 502: {0}")] Network(String), + /// Service unavailable, e.g. wallet not configured (HTTP 503). + #[error("antd error 503: {0}")] + ServiceUnavailable(String), + /// HTTP transport error from reqwest. #[error("http error: {0}")] Http(#[from] reqwest::Error), @@ -58,6 +62,7 @@ pub fn error_for_status(code: u16, message: String) -> 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-rust/src/grpc_client.rs b/antd-rust/src/grpc_client.rs index 723d7bb..e96d2ad 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::*; @@ -16,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, }; @@ -25,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, } @@ -44,6 +43,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()) @@ -56,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), }) } @@ -204,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/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-rust/src/models.rs b/antd-rust/src/models.rs index e8bf682..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 { @@ -49,3 +31,56 @@ 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, +} + +/// 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-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 d665c2f..3ae99c5 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") @@ -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/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 42be06b..68ed387 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 @@ -20,18 +20,22 @@ 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) 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 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 + + // 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/AntdGrpcClient.swift b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift index 8c36bba..3337f8c 100644 --- a/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdGrpcClient.swift @@ -25,22 +25,21 @@ 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() } 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) 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-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index b38bb40..8a177f7 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 @@ -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) } @@ -115,38 +119,12 @@ 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) 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 +132,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) } @@ -185,6 +165,48 @@ 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) + } + + /// 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: - 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) + } + + /// 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] + let resp: FinalizeUploadDTO = try await postJSON("/v1/upload/finalize", body: body) + return FinalizeUploadResult(address: resp.address, chunksStored: resp.chunksStored) + } } // MARK: - Internal DTOs @@ -212,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 @@ -236,6 +246,39 @@ private struct ArchiveDTO: Decodable { let entries: [ArchiveEntryDTO]? } +private struct WalletAddressDTO: Decodable { + let address: String +} + +private struct WalletBalanceDTO: Decodable { + let balance: String + let gasBalance: String +} + +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/DaemonDiscovery.swift b/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift new file mode 100644 index 0000000..1eda050 --- /dev/null +++ b/antd-swift/Sources/AntdSdk/DaemonDiscovery.swift @@ -0,0 +1,115 @@ +import Foundation + +/// Discovers the antd daemon by reading the `daemon.port` file written on startup. +/// +/// 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` +/// - 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 } + + // 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 + } + + 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-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-swift/Sources/AntdSdk/Models.swift b/antd-swift/Sources/AntdSdk/Models.swift index 9ab921e..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 @@ -73,3 +47,66 @@ 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 + } +} + +/// 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/README.md b/antd-zig/README.md index 8d7aee6..65404c9 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(); @@ -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 eac71d5..b5716c3 100644 --- a/antd-zig/src/antd.zig +++ b/antd-zig/src/antd.zig @@ -5,20 +5,23 @@ 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; -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; 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| { @@ -164,8 +178,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); @@ -182,8 +199,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); @@ -228,66 +248,42 @@ pub const Client = struct { return json_helpers.parseBase64Data(self.allocator, resp); } - // --- Graph --- + // --- Wallet --- - /// 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; + /// 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.parsePutResult(self.allocator, resp, "address"); + return json_helpers.parseWalletAddress(self.allocator, resp); } - /// 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; + /// 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.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; + return json_helpers.parseWalletBalance(self.allocator, resp); } - /// 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; + /// 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.parseCost(self.allocator, resp); + return json_helpers.parseBoolField(self.allocator, resp, "approved"); } // --- 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); @@ -305,10 +301,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); @@ -345,6 +347,38 @@ 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; + } + + /// 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 { + // 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-zig/src/discover.zig b/antd-zig/src/discover.zig new file mode 100644 index 0000000..c575489 --- /dev/null +++ b/antd-zig/src/discover.zig @@ -0,0 +1,125 @@ +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 pid_line: ?[]const u8 = null; + + 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); + } + 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; +} + +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-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-zig/src/json_helpers.zig b/antd-zig/src/json_helpers.zig index 17b4aac..7d0dee7 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, @@ -374,6 +390,60 @@ 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 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-zig/src/models.zig b/antd-zig/src/models.zig index 7d2aecb..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, @@ -68,6 +36,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/Cargo.lock b/antd/Cargo.lock index 8b7261b..da5f791 100644 --- a/antd/Cargo.lock +++ b/antd/Cargo.lock @@ -826,6 +826,49 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ant-core" +version = "0.1.1" +source = "git+https://github.com/WithAutonomi/ant-client#7bcb6cbca9efb052843b22d912259acf26f47d13" +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", + "toml", + "tower-http", + "tracing", + "tracing-subscriber", + "utoipa", + "windows-sys 0.61.2", + "xor_name", + "zip", +] + [[package]] name = "ant-evm" version = "0.1.21" @@ -858,16 +901,70 @@ dependencies = [ "sha2", ] +[[package]] +name = "ant-node" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9161d53c72cfcc0dd8fee14bff64c0bad06642f074d52c5c91d9ee203acb4042" +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-core", "ant-evm", "axum 0.8.8", "base64", "blake3", "bytes", "clap", + "dirs 6.0.0", "evmlib", "futures", "hex", @@ -875,7 +972,7 @@ dependencies = [ "prost", "rand 0.8.5", "rmp-serde", - "saorsa-node", + "self_encryption", "serde", "serde_json", "thiserror 2.0.18", @@ -2490,6 +2587,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" @@ -2705,6 +2811,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" @@ -3217,6 +3338,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" @@ -3235,9 +3372,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -4013,6 +4152,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" @@ -4209,12 +4365,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" @@ -5018,15 +5212,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", @@ -5037,13 +5237,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", ] @@ -5410,56 +5613,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" @@ -5589,7 +5742,7 @@ dependencies = [ "serde_yaml", "slab", "socket2 0.5.10", - "system-configuration", + "system-configuration 0.6.1", "thiserror 2.0.18", "time", "tinyvec", @@ -6159,6 +6312,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" @@ -6352,6 +6516,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" @@ -6799,6 +6973,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" @@ -6817,6 +7015,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" @@ -6956,6 +7160,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" @@ -7147,6 +7364,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 66c24ce..1e410a0 100644 --- a/antd/Cargo.toml +++ b/antd/Cargo.toml @@ -4,15 +4,17 @@ 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" +self_encryption = "0.35.0" evmlib = "0.4.9" axum = { version = "0.8", features = ["macros"] } 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/README.md b/antd/README.md index 7b7aea7..610eaf7 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:8080 \ - --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 @@ -36,50 +45,66 @@ 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` | +| `--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 | +| `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 +- **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 -### REST API (default: `http://localhost:8080`) +### REST API (default: `http://localhost:8082`) | 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 | -| `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 | +| **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/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** | | | +| `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 | +| **Archives** *(stub)* | | | +| `GET` | `/v1/archives/public/{address}` | Get archive manifest | +| `POST` | `/v1/archives/public` | Create archive manifest | ### gRPC API (default: `localhost:50051`) @@ -88,8 +113,7 @@ 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 +- `FileService` — File upload/download *(stub)* - `EventService` — Event streaming ## Project Structure @@ -103,22 +127,23 @@ antd/ │ ├── common.proto │ ├── data.proto │ ├── chunks.proto -│ ├── graph.proto │ ├── 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 + │ └── events.rs # Event streaming (stub) └── grpc/ # Tonic gRPC service ├── mod.rs └── service.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 f550f4e..904bf55 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: @@ -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/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/error.rs b/antd/src/error.rs index f6f6c79..16809be 100644 --- a/antd/src/error.rs +++ b/antd/src/error.rs @@ -25,10 +25,35 @@ pub enum AntdError { #[error("Timeout: {0}")] Timeout(String), + #[error("Service unavailable: {0}")] + ServiceUnavailable(String), + + #[error("Not implemented: {0}")] + NotImplemented(String), + #[error("Internal error: {0}")] 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, @@ -44,6 +69,8 @@ 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, }; let body = serde_json::to_string(&ErrorBody { @@ -64,29 +91,9 @@ 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), } } } - -// 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/mod.rs b/antd/src/grpc/mod.rs index 1e6b0de..fc5905e 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; @@ -11,28 +13,26 @@ 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(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() }); 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() }); + let addr = listener.local_addr()?; tracing::info!("gRPC server listening on {addr}"); Server::builder() .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(addr) + .serve_with_incoming(TcpListenerStream::new(listener)) .await?; Ok(()) diff --git a/antd/src/grpc/service.rs b/antd/src/grpc/service.rs index c02c135..0524949 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,90 +100,23 @@ 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::unavailable( + "wallet not 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), })) } } -// ── 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/main.rs b/antd/src/main.rs index 7376704..fbe1eef 100644 --- a/antd/src/main.rs +++ b/antd/src/main.rs @@ -3,11 +3,14 @@ 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; mod grpc; +mod port_file; mod rest; mod state; mod types; @@ -23,16 +26,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 @@ -105,71 +128,79 @@ 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()); + 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(); + 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, + merkle_addr.as_deref(), + ); + 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}"); + } + } + } else { + // 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(); - tracing::info!(%rpc_url, "loading EVM wallet..."); + 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(), ); - 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 - } - } - } - Err(_) => { - tracing::info!("no AUTONOMI_WALLET_KEY set — write operations will fail"); - 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 { - node: Arc::new(node), + client: Arc::new(client), network: config.network.clone(), bootstrap_peers, - wallet, + pending_uploads: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())), }); - // 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); + let app = rest::router(state.clone(), config.cors, actual_rest_addr.port()); // 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 +211,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..b9aea07 --- /dev/null +++ b/antd/src/port_file.rs @@ -0,0 +1,73 @@ +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. +/// 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) { + 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 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(()) + })(); + + 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/antd/src/rest/chunks.rs b/antd/src/rest/chunks.rs index fb80cb4..e03c9cd 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::ServiceUnavailable( + "wallet not 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/data.rs b/antd/src/rest/data.rs index dd6db95..3b6dde7 100644 --- a/antd/src/rest/data.rs +++ b/antd/src/rest/data.rs @@ -5,55 +5,161 @@ 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::*; -// 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_put_public( + State(state): State>, + Json(req): Json, +) -> Result, AntdError> { + if state.client.wallet().is_none() { + return Err(AntdError::ServiceUnavailable("wallet not 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::Internal("data operations not yet implemented yet".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_public( - State(_state): State>, - Json(_req): Json, -) -> Result, AntdError> { - Err(AntdError::Internal("data operations not yet implemented yet".into())) +pub async fn data_put_private( + State(state): State>, + Json(req): Json, +) -> Result, AntdError> { + if state.client.wallet().is_none() { + return Err(AntdError::ServiceUnavailable("wallet not 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::Internal("private data operations not yet implemented yet".into())) -} + let data_map_bytes = hex::decode(&query.data_map) + .map_err(|e| AntdError::BadRequest(format!("invalid hex data_map: {e}")))?; -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())) + 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( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("data cost not yet implemented yet".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( 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..428c820 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,55 +8,169 @@ 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. - pub async fn file_upload_public( - State(_state): State>, - Json(_req): Json, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("file operations not yet implemented yet".into())) + if state.client.wallet().is_none() { + return Err(AntdError::ServiceUnavailable("wallet not 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::Internal("file operations not yet implemented yet".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::Internal("directory operations not yet implemented yet".into())) + if state.client.wallet().is_none() { + return Err(AntdError::ServiceUnavailable("wallet not 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::Internal("directory operations not yet implemented yet".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( 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, + State(state): State>, + Json(req): Json, ) -> Result, AntdError> { - Err(AntdError::Internal("file cost not yet implemented yet".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/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 d53c5d0..bc8120f 100644 --- a/antd/src/rest/mod.rs +++ b/antd/src/rest/mod.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use axum::extract::State; -use axum::routing::{get, head, post}; +use axum::http::{HeaderValue, Method}; +use axum::routing::{get, post}; use axum::{Json, Router}; use tower_http::cors::CorsLayer; @@ -12,9 +13,10 @@ pub mod chunks; 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) -> Router { +pub fn router(state: Arc, enable_cors: bool, rest_port: u16) -> Router { let app = Router::new() // Health .route("/health", get(health)) @@ -28,11 +30,6 @@ pub fn router(state: Arc, enable_cors: bool) -> 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)) @@ -42,10 +39,28 @@ 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)) + // 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)) + .route("/v1/wallet/balance", get(wallet::wallet_balance)) + .route("/v1/wallet/approve", post(wallet::wallet_approve)) .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/upload.rs b/antd/src/rest/upload.rs new file mode 100644 index 0000000..9d985a6 --- /dev/null +++ b/antd/src/rest/upload.rs @@ -0,0 +1,178 @@ +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 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 +/// 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 store_on_network = req.store_data_map; + let client = state.client.clone(); + 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 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 { + data_map: data_map_hex, + address, + chunks_stored: chunks_stored as u64, + })) +} diff --git a/antd/src/rest/wallet.rs b/antd/src/rest/wallet.rs new file mode 100644 index 0000000..0a1092e --- /dev/null +++ b/antd/src/rest/wallet.rs @@ -0,0 +1,55 @@ +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.client.wallet() + .ok_or_else(|| AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".into()))?; + + Ok(Json(WalletAddressResponse { + address: format!("{:#x}", wallet.address()), + })) +} + +pub async fn wallet_balance( + State(state): State>, +) -> Result, AntdError> { + let wallet = state.client.wallet() + .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}")))?; + + 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(), + })) +} + +pub async fn wallet_approve( + State(state): State>, +) -> Result, AntdError> { + if state.client.wallet().is_none() { + return Err(AntdError::ServiceUnavailable("wallet not configured — set AUTONOMI_WALLET_KEY".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/state.rs b/antd/src/state.rs index c2d4601..9ac06c6 100644 --- a/antd/src/state.rs +++ b/antd/src/state.rs @@ -1,16 +1,18 @@ +use std::collections::HashMap; use std::sync::Arc; -use ant_node::core::P2PNode; +use ant_core::data::{Client, MultiAddr, PreparedUpload}; +use tokio::sync::Mutex; /// 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: Arc, /// 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, + /// 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 feeacb6..0eb7bcf 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)] @@ -52,39 +57,65 @@ pub struct ChunkGetResponse { pub data: String, // base64 } -// ── Graph ── +// ── External Signer (two-phase upload) ── #[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, +pub struct PrepareUploadRequest { + pub path: String, } -#[derive(Serialize, Deserialize)] -pub struct GraphDescendantDto { - pub public_key: String, // hex - pub content: String, // hex, 32 bytes +#[derive(Deserialize)] +pub struct PrepareDataUploadRequest { + pub data: String, // base64 } #[derive(Serialize)] -pub struct GraphEntryPutResponse { - pub cost: String, - pub address: String, +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 GraphEntryGetResponse { - pub owner: String, - pub parents: Vec, - pub content: String, - pub descendants: Vec, +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 GraphEntryCostRequest { - pub public_key: String, // hex +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, + /// 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 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, } // ── Files ── @@ -92,6 +123,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 +201,47 @@ 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)] +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, +} + +#[derive(Serialize)] +pub struct WalletApproveResponse { + /// Whether the token spend was approved. + pub approved: bool, +} + // ── Health ── #[derive(Serialize)] diff --git a/docs/architecture.md b/docs/architecture.md index 1b8cc53..92f2046 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) @@ -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 4f63854..8b5b43d 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(); @@ -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 9dafca0..3960f87 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) ); @@ -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 696f60b..ad94328 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'); @@ -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 7ab9b57..80c9cea 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") @@ -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 c02ae4d..672f9ac 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 @@ -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 8ce1e42..b24689a 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), ) @@ -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 09e3c53..f727986 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 @@ -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 5f9d837..8303ce1 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'); @@ -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 9d86ee5..855a007 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") @@ -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 f383862..aec0c6e 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") @@ -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 32c4adb..c1326fe 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()?; @@ -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 3102023..2baf2bf 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 ) @@ -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 06e2edf..ecb6a31 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(); @@ -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/ffi/rust/Cargo.lock b/ffi/rust/Cargo.lock index 6f613e4..528bd53 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#7bcb6cbca9efb052843b22d912259acf26f47d13" 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,19 @@ 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", + "rand 0.8.5", + "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 +917,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 +974,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 +1268,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 +1281,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 +1328,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 +1360,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 +1500,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 +1536,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 +1553,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 +1576,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 +1596,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" @@ -1689,8 +1662,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] -name = "byteorder" -version = "1.5.0" +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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" @@ -1703,6 +1682,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 +1748,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 +1755,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 +1855,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 +1867,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 +1919,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 +1972,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 +2003,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 +2061,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 +2095,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 +2308,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 +2359,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 +2415,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 +2486,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 +2546,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -2488,9 +2583,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 +2596,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 +2652,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 +2716,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 +2733,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 +2749,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 +2797,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 +2825,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 +2858,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 +2938,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 +2949,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" @@ -2961,33 +3076,18 @@ dependencies = [ [[package]] name = "group" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.12.1", - "rand 0.8.5", + "ff", "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", - "rand_core 0.6.4", - "rand_xorshift 0.3.0", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.13" +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ @@ -3004,6 +3104,15 @@ dependencies = [ "tracing", ] +[[package]] +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" @@ -3015,9 +3124,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 +3147,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 +3157,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 +3177,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 +3254,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 +3313,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 +3333,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -3250,7 +3356,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 +3393,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 +3412,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -3414,60 +3538,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 +3593,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 +3651,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 +3748,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 +3794,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 +3818,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,78 +3855,31 @@ dependencies = [ ] [[package]] -name = "libp2p-dns" -version = "0.44.0" +name = "libp2p-identify" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b770c1c8476736ca98c578cba4b505104ff8e842c2876b528925f9766379f9a" +checksum = "8ab792a8b68fdef443a62155b01970c81c3aadab5e659621b063ef252a8e65e8" dependencies = [ - "async-trait", + "asynchronous-codec", + "either", "futures", - "hickory-resolver", + "futures-bounded", + "futures-timer", "libp2p-core", "libp2p-identity", - "parking_lot", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", "smallvec", + "thiserror 2.0.18", "tracing", ] [[package]] -name = "libp2p-gossipsub" -version = "0.49.2" +name = "libp2p-identity" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f58e37d8d6848e5c4c9e3c35c6f61133235bff2960c9c00a663b0849301221" -dependencies = [ - "async-channel", - "asynchronous-codec", - "base64", - "byteorder", - "bytes", - "either", - "fnv", - "futures", - "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", - "tracing", -] - -[[package]] -name = "libp2p-identity" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" +checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ "bs58", "ed25519-dalek", @@ -3850,7 +3887,6 @@ dependencies = [ "multihash", "quick-protobuf", "rand 0.8.5", - "serde", "sha2", "thiserror 2.0.18", "tracing", @@ -3876,7 +3912,6 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "rand 0.8.5", - "serde", "sha2", "smallvec", "thiserror 2.0.18", @@ -3885,25 +3920,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 +3931,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 +3947,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 +3981,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 +4022,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 +4065,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 +4128,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 +4166,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 +4184,20 @@ dependencies = [ ] [[package]] -name = "netlink-packet-core" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" -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" +name = "native-tls" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ - "bytes", - "futures-util", "libc", "log", - "tokio", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] @@ -4378,26 +4210,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 +4321,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 +4383,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 +4396,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 +4453,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 +4500,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 +4518,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 +4579,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 +4663,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 +4698,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 +4744,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 +4770,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 +4839,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 +4882,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 +4900,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 +4930,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 +5032,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 +5072,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 +5093,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 +5113,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 +5181,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 +5206,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 +5310,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 +5389,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 +5398,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -5468,6 +5408,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 +5418,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 +5449,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 +5822,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 +5948,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 +6020,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 +6100,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 +6132,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 +6159,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 +6180,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 +6231,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 +6286,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 +6306,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 +6324,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 +6338,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 +6364,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -6258,7 +6511,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 +6530,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 +6571,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] @@ -6319,6 +6585,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 +6615,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 +6636,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 +6651,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 +6669,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6412,6 +6708,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 +6784,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 +6854,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 +6900,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 +6963,7 @@ dependencies = [ "quote", "serde", "syn 2.0.117", - "toml", + "toml 0.5.11", "uniffi_meta", ] @@ -6675,7 +6974,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 +6986,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 +7014,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 +7063,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 +7095,7 @@ checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -6776,16 +7106,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 +7250,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 +7310,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 +7337,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 +7379,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 +7406,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 +7435,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 +7464,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 +7492,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 +7513,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 +7556,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 +7620,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 +7643,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 +7667,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 +7703,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 +7727,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 +7751,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 +7775,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 +7808,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 +7824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -7435,7 +7835,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 +7925,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 +7935,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 +7966,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 +8100,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..efcf590 100644 --- a/ffi/rust/ant-ffi/Cargo.toml +++ b/ffi/rust/ant-ffi/Cargo.toml @@ -1,18 +1,19 @@ [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" +rand = "0.8" +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..36be63f 100644 --- a/ffi/rust/ant-ffi/src/client.rs +++ b/ffi/rust/ant-ffi/src/client.rs @@ -1,511 +1,323 @@ -use autonomi::client::payment::PaymentOption as AutonomiPaymentOption; -use bytes::Bytes; +use std::collections::HashMap; +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 tokio::sync::Mutex; + +use ant_core::data::{ + Client as CoreClient, ClientConfig, CoreNodeConfig, MultiAddr, NodeMode, P2PNode, + PreparedUpload, finalize_batch_payment, }; -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, + FinalizeUploadResult, FilePutPublicResult, PaymentEntry, PrepareUploadResult, }; -/// 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, + /// Pending prepared uploads (external signer flow). + pending_uploads: Mutex>, } #[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), - })) - } - - #[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), - })) - } - + /// Connect to a local test network. #[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, pending_uploads: Mutex::new(HashMap::new()) })) } - 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, pending_uploads: Mutex::new(HashMap::new()) })) } - 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, pending_uploads: Mutex::new(HashMap::new()) })) } - // ===== 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()) + } + + /// 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_put_public( + // ===== Data Operations ===== + + /// 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, addr) = self + let result = self .inner - .archive_put_public(&archive.inner, autonomi_payment) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + .data_upload_with_mode(Bytes::from(data), mode) + .await?; - Ok(PublicArchivePutResult { - cost: cost.to_string(), - address: Arc::new(ArchiveAddress { inner: addr }), + 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), }) } - 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 })) + /// 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 archive_put( + /// Upload private data. Returns the serialized data map (hex). + pub async fn data_put_private( &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?; + + let data_map_bytes = rmp_serde::to_vec(&result.data_map).map_err(|e| { + ClientError::InternalError { + reason: format!("failed to serialize data map: {e}"), + } + })?; - Ok(PrivateArchivePutResult { - cost: cost.to_string(), - data_map: Arc::new(DataMapChunk { inner: data_map }), + 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), }) } - // ===== 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(), + /// 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(cost.to_string()) - } - - pub async fn file_upload( - &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 (cost, data_map) = self - .inner - .file_content_upload(path, autonomi_payment.into()) - .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(FileUploadResult { - cost: cost.to_string(), - data_map: Arc::new(DataMapChunk { inner: data_map }), - }) + let content = self.inner.data_download(&data_map).await?; + Ok(content.to_vec()) } + // ===== File Operations ===== + + /// Upload a file from disk (public). Returns the address. 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()) - } - }; + 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 - .file_content_upload_public(path, autonomi_payment.into()) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + .file_upload_with_mode(&file_path, mode) + .await?; - Ok(FileUploadPublicResult { - cost: cost.to_string(), - address: Arc::new(DataAddress { inner: addr }), - }) - } + let address = self.inner.data_map_store(&result.data_map).await?; - 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(), - })?; - Ok(()) + Ok(FilePutPublicResult { + address: hex::encode(address), + }) } + /// Download a file to disk by hex-encoded address. pub async fn file_download_public( &self, - address: 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 - .file_download_public(&address.inner, path) + .file_download(&data_map, &dest) .await .map_err(|e| ClientError::NetworkError { reason: e.to_string(), @@ -513,75 +325,122 @@ impl Client { Ok(()) } - pub async fn dir_upload( + // ===== 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, - path: String, - wallet: Arc, - ) -> Result { - let path = std::path::PathBuf::from(path); + 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 (cost, data_map) = self - .inner - .dir_upload(path, &wallet.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + let total_amount = prepared.payment_intent.total_amount.to_string(); - Ok(DirUploadResult { - cost: cost.to_string(), - data_map: Arc::new(PrivateArchiveDataMap { inner: data_map }), - }) + 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 }) } - pub async fn dir_upload_public( + /// Prepare a file upload for external signing. + pub async fn prepare_file_upload( &self, path: String, - wallet: Arc, - ) -> Result { - let path = std::path::PathBuf::from(path); + ) -> 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 (cost, addr) = self - .inner - .dir_upload_public(path, &wallet.inner) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), - })?; + let total_amount = prepared.payment_intent.total_amount.to_string(); - Ok(DirUploadPublicResult { - cost: cost.to_string(), - address: Arc::new(ArchiveAddress { inner: addr }), - }) + 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 }) } - pub async fn dir_download( + /// Finalize an upload after external payment. + /// Takes a map of quote_hash (hex) → tx_hash (hex). + pub async fn finalize_upload( &self, - data_map: Arc, - path: String, - ) -> Result<(), ClientError> { - let path = std::path::PathBuf::from(path); - self.inner - .dir_download(&data_map.inner, path) - .await - .map_err(|e| ClientError::NetworkError { - reason: e.to_string(), + 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"), })?; - Ok(()) + + 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, + }) } - 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..3bef83d 100644 --- a/ffi/rust/ant-ffi/src/lib.rs +++ b/ffi/rust/ant-ffi/src/lib.rs @@ -1,119 +1,124 @@ -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, + /// Hex-encoded chunk address (32 bytes). + pub address: String, } -/// Result of uploading a public archive to the network +/// Result of a public data upload (data map stored as public chunk). #[derive(uniffi::Record)] -pub struct PublicArchivePutResult { - 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 private archive to the network +/// Result of a private data upload (data map returned to caller). #[derive(uniffi::Record)] -pub struct PrivateArchivePutResult { - 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 file to the network (private) +/// Result of uploading a file (public). #[derive(uniffi::Record)] -pub struct FileUploadResult { - pub cost: String, - pub data_map: Arc, +pub struct FilePutPublicResult { + /// Hex-encoded address of the stored data map. + pub address: String, } -/// Result of uploading a file to the network (public) +/// Payment entry for external signing. #[derive(uniffi::Record)] -pub struct FileUploadPublicResult { - pub cost: String, - pub address: Arc, +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 uploading a directory to the network (private) +/// Result of preparing an upload for external signing. #[derive(uniffi::Record)] -pub struct DirUploadResult { - pub cost: String, - pub data_map: Arc, +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 uploading a public directory to the network +/// Result of finalizing an externally-signed upload. #[derive(uniffi::Record)] -pub struct DirUploadPublicResult { - pub cost: String, - pub address: Arc, +pub struct FinalizeUploadResult { + /// Number of chunks stored on the network. + pub chunks_stored: u64, } // ===== 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()) + } +} diff --git a/llms-full.txt b/llms-full.txt index bfee75d..3df959a 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,27 +1,60 @@ # 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 +# 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 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 +63,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 +75,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 +87,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 +98,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 +110,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 +122,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 +134,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 +145,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 +157,8 @@ data = client.data_get_public(result.address) dataPutPublic("hello"); $data = $client->dataGetPublic($result->address); @@ -135,8 +168,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 +179,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 +188,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 +199,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"); @@ -189,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}` @@ -203,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` @@ -231,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. @@ -278,7 +271,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` @@ -289,7 +282,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` @@ -321,6 +314,69 @@ 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. + +#### `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. + +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 @@ -342,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{}` @@ -374,20 +424,48 @@ 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 | --- +## 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 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 @@ -404,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 @@ -418,6 +492,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 ``` --- @@ -427,7 +506,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 @@ -444,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 @@ -458,6 +533,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 ``` --- @@ -467,7 +547,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 @@ -484,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); @@ -498,6 +574,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(); ``` --- @@ -507,7 +588,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 @@ -524,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 @@ -538,6 +615,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 ``` --- @@ -549,7 +631,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 @@ -566,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 @@ -580,6 +658,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 ``` --- @@ -589,7 +672,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) @@ -605,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) @@ -619,6 +698,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 ``` --- @@ -630,8 +714,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 @@ -647,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 @@ -662,6 +742,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 ``` @@ -671,9 +756,9 @@ String fileCost(String path, boolean isPublic, boolean includeArchive) throws An ```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:8080"); // REST, localhost:8080 +let client = Client::new("http://localhost:8082"); // REST, localhost:8082 // Health async fn health(&self) -> Result @@ -689,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 @@ -704,6 +785,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 ``` @@ -715,8 +801,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 @@ -732,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) @@ -747,6 +829,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 ``` @@ -758,8 +845,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 @@ -775,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 @@ -789,6 +872,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 ``` --- @@ -798,8 +886,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 @@ -815,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 @@ -829,6 +913,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 ``` --- @@ -838,8 +927,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() @@ -855,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) @@ -869,6 +954,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() ``` --- @@ -878,8 +968,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 @@ -895,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 @@ -909,6 +995,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 ``` --- @@ -916,8 +1007,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()} @@ -933,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()} @@ -947,6 +1034,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()} ``` --- @@ -956,8 +1048,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 @@ -974,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 @@ -988,6 +1076,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 ``` --- @@ -1000,16 +1093,15 @@ fn fileCost(self: *Client, path: []const u8, is_public: bool, include_archive: b | `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 | | `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 | --- @@ -1025,6 +1117,7 @@ fn fileCost(self: *Client, path: []const u8, is_public: bool, include_archive: b | 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. @@ -1128,7 +1221,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..0079ea7 100644 --- a/llms.txt +++ b/llms.txt @@ -1,12 +1,12 @@ # 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 ```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) ``` @@ -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 @@ -43,23 +43,25 @@ 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 | -| 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 | +| 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 | | 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 | +| 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 @@ -68,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 | @@ -83,6 +84,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 @@ -102,7 +104,37 @@ 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` + +## 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/scripts/start-local.ps1 b/scripts/start-local.ps1 index 66db118..67e8775 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() @@ -160,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 243c79c..e7d0a5f 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=$! @@ -137,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}" 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 1759813..82ca6f2 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 @@ -53,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 @@ -66,14 +62,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 @@ -89,20 +90,27 @@ 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) +### Pattern 3: External Signer (Two-Phase Upload) -DAG nodes with parent/descendant links for building append-only structures. +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 -entry1 = client.graph_entry_put(key1, parents=[], content=content1, descendants=[]) -entry2 = client.graph_entry_put(key2, parents=[entry1.address], content=content2, descendants=[]) +# 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 needs an audit log, version chain, social graph, or any linked data structure. +**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. 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. @@ -118,6 +126,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`. @@ -128,7 +137,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