C++ SDK for the antd daemon — the gateway to the Autonomi decentralized network.
Add to your CMakeLists.txt:
include(FetchContent)
FetchContent_Declare(
antd-cpp
GIT_REPOSITORY https://github.com/WithAutonomi/ant-sdk.git
SOURCE_SUBDIR antd-cpp
)
FetchContent_MakeAvailable(antd-cpp)
target_link_libraries(your_target PRIVATE antd)All dependencies (nlohmann_json, cpp-httplib) are fetched automatically.
git clone https://github.com/WithAutonomi/ant-sdk.git
cd ant-sdk/antd-cpp
cmake -B build
cmake --build build#include "antd/antd.hpp"
#include <iostream>
int main() {
antd::Client client; // defaults to http://localhost:8082
// Check daemon health
auto health = client.health();
std::cout << "OK: " << health.ok << ", Network: " << health.network << "\n";
// Store data
std::string msg = "Hello, Autonomi!";
std::vector<uint8_t> data(msg.begin(), msg.end());
auto result = client.data_put_public(data);
std::cout << "Stored at " << result.address << " (chunks: " << result.chunks_stored << ")\n";
// Retrieve data
auto retrieved = client.data_get_public(result.address);
std::string text(retrieved.begin(), retrieved.end());
std::cout << "Retrieved: " << text << "\n";
}The SDK ships an AsyncClient that wraps every synchronous method in
std::async(std::launch::async, ...) and returns a std::future<T>.
No additional dependencies are required — only C++20 <future>.
#include "antd/antd.hpp"
#include <iostream>
int main() {
antd::AsyncClient client; // defaults to http://localhost:8082
// Fire off two requests concurrently
auto health_future = client.health();
auto cost_future = client.data_cost({0x01, 0x02, 0x03});
// Block until the health check completes
auto health = health_future.get();
std::cout << "OK: " << health.ok << "\n";
// Block until the cost estimate completes — returns UploadCostEstimate
auto est = cost_future.get();
std::cout << "Estimate: " << est.file_size << " bytes in " << est.chunk_count
<< " chunks, " << est.cost << " atto, gas " << est.estimated_gas_cost_wei
<< " wei, mode " << est.payment_mode << "\n";
}auto future = client.data_put_public(data);
// Wait up to 10 seconds
if (future.wait_for(std::chrono::seconds(10)) == std::future_status::ready) {
auto result = future.get();
std::cout << "Stored at " << result.address << "\n";
} else {
std::cerr << "Upload still in progress...\n";
}Exceptions thrown by the underlying synchronous client propagate through the
future. Calling .get() on a failed future rethrows the original exception:
try {
auto data = client.data_get_public("bad-address").get();
} catch (const antd::NotFoundError& e) {
std::cerr << "Not found\n";
} catch (const antd::AntdError& e) {
std::cerr << "Error " << e.status_code << ": " << e.what() << "\n";
}// Launch many downloads in parallel
std::vector<std::future<std::vector<uint8_t>>> futures;
for (const auto& addr : addresses) {
futures.push_back(client.data_get_public(addr));
}
// Collect results
for (auto& f : futures) {
auto data = f.get(); // blocks until this particular download finishes
process(data);
}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.
Enable the gRPC target by passing -DANTD_BUILD_GRPC=ON to CMake:
cmake -B build -DANTD_BUILD_GRPC=ON
cmake --build buildThis requires protoc, grpc_cpp_plugin, and a gRPC installation (e.g. via
vcpkg, apt install libgrpc++-dev, or building from source). The CMake
configuration will automatically run protoc against the proto files in
antd/proto/antd/v1/ and generate the C++ stubs.
Link against the antd_grpc target instead of (or in addition to) antd:
target_link_libraries(your_target PRIVATE antd_grpc)#include "antd/grpc_client.hpp"
#include <iostream>
int main() {
antd::GrpcClient client; // defaults to localhost:50051
// Custom target
// antd::GrpcClient client("my-host:50051");
auto health = client.health();
std::cout << "OK: " << health.ok << ", Network: " << health.network << "\n";
std::string msg = "Hello via gRPC!";
std::vector<uint8_t> data(msg.begin(), msg.end());
auto result = client.data_put_public(data);
std::cout << "Stored at " << result.address << "\n";
auto retrieved = client.data_get_public(result.address);
std::string text(retrieved.begin(), retrieved.end());
std::cout << "Retrieved: " << text << "\n";
}The GrpcClient throws the same antd::AntdError hierarchy as the REST
client, translating gRPC status codes to the appropriate error subclass.
Note: Wallet operations (address, balance, approve) and payment_mode are available via REST only.
- C++20 compiler (GCC 10+, Clang 10+, MSVC 19.29+)
- CMake 3.14+
- A running antd daemon. Start it with:
ant dev start// 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:8082", 30);All methods throw antd::AntdError (or a subclass) on failure.
| Method | Description |
|---|---|
health() |
Check daemon status |
| Method | Description |
|---|---|
data_put_public(data, payment_mode) |
Store public data — returns DataPutPublicResult (DataMap stored on-network) |
data_get_public(address) |
Retrieve public data by address |
data_put(data, payment_mode) |
Store encrypted private data — returns DataPutResult (DataMap returned to caller) |
data_get(data_map) |
Retrieve private data using a caller-held DataMap |
data_cost(data, payment_mode) |
Estimate storage cost — returns UploadCostEstimate with size, chunks, gas, payment mode |
| Method | Description |
|---|---|
chunk_put(data) |
Store a raw chunk |
chunk_get(address) |
Retrieve a chunk |
| Method | Description |
|---|---|
file_put(path, payment_mode) |
Upload a file privately — returns FilePutResult (DataMap returned to caller) |
file_get(data_map, dest_path) |
Download a private file using a caller-held DataMap |
file_put_public(path, payment_mode) |
Upload a file publicly — returns FilePutPublicResult (DataMap stored on-network) |
file_get_public(address, dest_path) |
Download a public file by address |
file_cost(path, is_public, payment_mode) |
Estimate upload cost — returns UploadCostEstimate with size, chunks, gas, payment mode |
All errors inherit from antd::AntdError (which inherits from std::runtime_error), so you can catch them at any granularity:
try {
auto data = client.data_get_public(address);
} catch (const antd::NotFoundError& e) {
std::cerr << "Not found on network\n";
} catch (const antd::PaymentError& e) {
std::cerr << "Insufficient funds\n";
} catch (const antd::AntdError& e) {
std::cerr << "antd error " << e.status_code << ": " << e.what() << "\n";
}| Error Type | HTTP Status | When |
|---|---|---|
BadRequestError |
400 | Invalid parameters |
PaymentError |
402 | Insufficient funds |
NotFoundError |
404 | Resource not found |
AlreadyExistsError |
409 | Resource exists |
ForkError |
409 | Version conflict |
TooLargeError |
413 | Payload too large |
InternalError |
500 | Server error |
NetworkError |
502 | Network unreachable |
PartialUploadError |
502 (code: PARTIAL_UPLOAD) |
Finalize paid and stored some chunks, others missed quorum — see below |
finalize_upload / finalize_merkle_upload can fail after the wallet has
paid: some chunks store, others miss quorum after the daemon's own retries.
That surfaces as antd::PartialUploadError (HTTP 502 with
code: "PARTIAL_UPLOAD"; gRPC ABORTED whose message starts with
Partial upload:, where the fields are parsed from the status message). The
gRPC match is anchored at the start of the message: any other ABORTED,
including one that only quotes Partial upload: further into its text, stays
a plain AntdError. Over REST, a count of the wrong JSON type reads as zero,
and a body whose code is not the string PARTIAL_UPLOAD keeps the plain
status mapping; the error mapping never throws anything but an AntdError
subclass. It derives from NetworkError, so existing 502 handlers keep
working — catch it first to handle the partial case specifically. The
on-chain payment persists and the stored chunks stay on the network. Two
flags say how to finish, retryable and retention_known (retryable
implies retention_known):
retryable: the daemon kept the paid attempt under the sameupload_id. Call the same finalize method again with the sameupload_idand payment artefacts to store the remainder against the same payment — no re-prepare, no second signature, no double payment. Bound the loop: a persistent failure throws on every call, so cap attempts and treat achunks_failedthat stops shrinking as stuck.retention_known && !retryable: the daemon confirmed nothing was retained (for example a merkle finalize with deliberately unpaid batches). Re-prepare the same content — already-stored chunks are skipped, so the retry pays only for the remainder.!retention_known: retention is unknown, and the daemon may still hold the paid attempt (it records the resume handle before it returns the error). Stop automatic recovery, keep theupload_idand the original payment artefacts, and reconcile before re-preparing or paying again; never pay again on this signal alone. Daemons older than 0.14.0 never sendretryable, so their REST partials read as unknown.
retention_known is true over REST only when the body's retryable is
present and a JSON boolean. Over gRPC the status message reads
Partial upload: <stored>/<total> chunks stored, <failed> failed after retries: <reason> (<hint>),
and retention_known is true only when the counts right after the
Partial upload: prefix parse (all three) and the message ends with one of
the daemon's two hints: (paid attempt retained...) sets retryable, and
(stored chunks persist; re-prepare the same content...) means the daemon
confirmed nothing was retained (daemons older than 0.14.0 write only this
one). A message whose counts do not parse reads as zero counts with
retention unknown, even with a hint. Readable counts with a missing,
truncated or unrecognised hint, or text after it, keep the counts but read
as retention unknown: stop and reconcile, not "nothing retained".
for (int attempt = 1;; ++attempt) {
try {
auto fin = client.finalize_upload(upload_id, tx_hashes);
break; // every chunk stored
} catch (const antd::PartialUploadError& e) {
if (!e.retention_known) throw; // unknown: stop, keep upload_id + tx_hashes, reconcile
if (!e.retryable) throw; // confirmed not retained: re-prepare the same content
if (attempt >= 5) throw; // still retained: retry the same finalize later
std::cerr << e.chunks_stored << "/" << e.total_chunks << " stored, "
<< e.chunks_failed << " unstored — retrying same upload_id\n";
}
}examples/07-external-signer.cpp has a complete finalize_with_retry with
backoff and stuck detection. Contract reference:
docs/external-signer-flow.md §6.
cmake -B build
cmake --build build
# Run tests
cd build && ctest --output-on-failure
# Build without examples
cmake -B build -DANTD_BUILD_EXAMPLES=OFFSee the examples/ directory:
01-connect— Health check02-data— Public data storage and retrieval03-chunks— Raw chunk operations04-files— File and directory upload/download06-private-data— Private encrypted data storage07-external-signer— Two-phase upload paid by an external signer (runs foundry'scastwithout a shell, after validating the daemon's payment fields), with a boundedfinalize_with_retryfor partial stores