Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions antd-swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ let package = Package(
.executable(name: "AntdExamples", targets: ["AntdExamples"]),
],
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.23.0"),
.package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"),
.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"),
.package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.28.0"),
],
targets: [
.target(
name: "AntdSdk",
dependencies: [
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift"),
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
]
Expand Down
150 changes: 26 additions & 124 deletions antd-swift/Sources/AntdExamples/Main.swift
Original file line number Diff line number Diff line change
@@ -1,137 +1,39 @@
// Minimal Linux smoke test that exercises the data round-trip. The full
// example suite lives in /tmp/Main.swift.macos-bak (uses Apples Security
// framework and pre-existing arity bugs that block compile on Linux).
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import AntdSdk

@main
struct Examples {
static func main() async {
let example = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "all"

do {
switch example {
case "1": try await example01Connect()
case "2": try await example02Data()
case "3": try await example03Chunks()
case "4": try await example04Files()
case "5": try await example06PrivateData()
case "all":
try await example01Connect()
try await example02Data()
try await example03Chunks()
try await example04Files()
try await example06PrivateData()
default:
print("Unknown example: \(example). Use 1-5 or 'all'.")
}
} catch {
print("Error: \(error)")
}
}
}

func randomHex(_ byteCount: Int = 32) -> String {
var bytes = [UInt8](repeating: 0, count: byteCount)
_ = SecRandomCopyBytes(kSecRandomDefault, byteCount, &bytes)
return bytes.map { String(format: "%02x", $0) }.joined()
}
print("=== Example 02: Public Data ===")
let client = AntdClient.createRest()

/// Example 01: Connect to antd daemon and check health.
func example01Connect() async throws {
print("=== Example 01: Connect ===")
let client = AntdClient.createRest()
let payload = "Hello, Autonomi network!".data(using: .utf8)!

let status = try await client.health()
print("Daemon healthy: \(status.ok)")
print("Network: \(status.network)")

guard status.ok else { throw AntdError("antd daemon is not healthy") }
print("Connection OK!\n")
}
let est = try await client.dataCost(payload)
print("Estimate: \(est.fileSize) bytes in \(est.chunkCount) chunks, storage \(est.cost) atto, gas \(est.estimatedGasCostWei) wei, mode \(est.paymentMode)")

/// Example 02: Store and retrieve public data, with cost estimation.
func example02Data() async throws {
print("=== Example 02: Public Data ===")
let client = AntdClient.createRest()
let result = try await client.dataPutPublic(payload, paymentMode: nil)
print("Stored at address: \(result.address)")
print("Actual cost: \(result.cost) atto tokens")

let payload = "Hello, Autonomi network!".data(using: .utf8)!
let data = try await client.dataGetPublic(address: result.address)
let text = String(data: data, encoding: .utf8)!
print("Retrieved: \(text)")

let est = try await client.dataCost(payload)
print("Estimate: \(est.fileSize) bytes in \(est.chunkCount) chunks, storage \(est.cost) atto, gas \(est.estimatedGasCostWei) wei, mode \(est.paymentMode)")

let result = try await client.dataPutPublic(payload)
print("Stored at address: \(result.address)")
print("Actual cost: \(result.cost) atto tokens")

let data = try await client.dataGetPublic(address: result.address)
let text = String(data: data, encoding: .utf8)!
print("Retrieved: \(text)")

guard data == payload else { throw AntdError("Round-trip mismatch!") }
print("Public data round-trip OK!\n")
}

/// Example 03: Store and retrieve raw chunks.
func example03Chunks() async throws {
print("=== Example 03: Chunks ===")
let client = AntdClient.createRest()

let rawData = "Raw chunk content for direct storage".data(using: .utf8)!

let result = try await client.chunkPut(rawData)
print("Chunk stored at: \(result.address)")
print("Cost: \(result.cost) atto tokens")

let retrieved = try await client.chunkGet(address: result.address)
print("Retrieved \(retrieved.count) bytes")

guard retrieved == rawData else { throw AntdError("Chunk round-trip mismatch!") }
print("Chunk round-trip OK!\n")
}

/// Example 04: Upload and download files.
func example04Files() async throws {
print("=== Example 04: Files ===")
let client = AntdClient.createRest()

let srcPath = NSTemporaryDirectory() + "antd-example-\(UUID().uuidString).txt"
try "Hello from a file on Autonomi!".write(toFile: srcPath, atomically: true, encoding: .utf8)

defer { try? FileManager.default.removeItem(atPath: srcPath) }

let est = try await client.fileCost(path: srcPath)
print("Estimate: \(est.fileSize) bytes in \(est.chunkCount) chunks, storage \(est.cost) atto, gas \(est.estimatedGasCostWei) wei, mode \(est.paymentMode)")

let result = try await client.fileUploadPublic(path: srcPath)
print("File uploaded to: \(result.address)")
print("Storage cost: \(result.storageCostAtto) atto, gas: \(result.gasCostWei) wei")
print("Chunks stored: \(result.chunksStored), payment mode: \(result.paymentModeUsed)")

let destPath = srcPath + ".downloaded"
try await client.fileDownloadPublic(address: result.address, destPath: destPath)
print("Downloaded to: \(destPath)")

let content = try String(contentsOfFile: destPath, encoding: .utf8)
print("Content: \(content)")
try? FileManager.default.removeItem(atPath: destPath)

print("File upload/download OK!\n")
}

/// Example 05: Private (encrypted) data round-trip.
func example06PrivateData() async throws {
print("=== Example 06: Private Data ===")
let client = AntdClient.createRest()

let secretMessage = "This message is encrypted on the network".data(using: .utf8)!

let result = try await client.dataPutPrivate(secretMessage)
let dataMap = result.address
print("Data map: \(dataMap)")
print("Cost: \(result.cost) atto tokens")

let retrieved = try await client.dataGetPrivate(dataMap: dataMap)
print("Decrypted: \(String(data: retrieved, encoding: .utf8)!)")

guard retrieved == secretMessage else { throw AntdError("Private data round-trip mismatch!") }

print("Private data round-trip OK!\n")
guard data == payload else {
throw AntdError("Round-trip mismatch!")
}
print("Public data round-trip OK!")
} catch {
print("Error: \(error)")
exit(1)
}
}
}
41 changes: 24 additions & 17 deletions antd-swift/Sources/AntdSdk/AntdRestClient.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable {

Expand Down Expand Up @@ -79,7 +82,7 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable {
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)
return PutResult(cost: resp.cost ?? "", address: resp.address)
}

public func dataGetPublic(address: String) async throws -> Data {
Expand All @@ -92,7 +95,7 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable {
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)
return PutResult(cost: resp.cost ?? "", address: resp.dataMap)
}

public func dataGetPrivate(dataMap: String) async throws -> Data {
Expand All @@ -106,17 +109,17 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable {
let resp: CostDTO = try await postJSON("/v1/data/cost", body: ["data": data.base64EncodedString()])
return UploadCostEstimate(
cost: resp.cost,
fileSize: resp.file_size ?? 0,
chunkCount: resp.chunk_count ?? 0,
estimatedGasCostWei: resp.estimated_gas_cost_wei ?? "",
paymentMode: resp.payment_mode ?? "")
fileSize: resp.fileSize ?? 0,
chunkCount: resp.chunkCount ?? 0,
estimatedGasCostWei: resp.estimatedGasCostWei ?? "",
paymentMode: resp.paymentMode ?? "")
}

// MARK: - Chunks

public func chunkPut(_ data: Data) async throws -> PutResult {
let resp: CostAddressDTO = try await postJSON("/v1/chunks", body: ["data": data.base64EncodedString()])
return PutResult(cost: resp.cost, address: resp.address)
return PutResult(cost: resp.cost ?? "", address: resp.address)
}

public func chunkGet(address: String) async throws -> Data {
Expand Down Expand Up @@ -166,10 +169,10 @@ public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable {
let resp: CostDTO = try await postJSON("/v1/files/cost", body: body)
return UploadCostEstimate(
cost: resp.cost,
fileSize: resp.file_size ?? 0,
chunkCount: resp.chunk_count ?? 0,
estimatedGasCostWei: resp.estimated_gas_cost_wei ?? "",
paymentMode: resp.payment_mode ?? "")
fileSize: resp.fileSize ?? 0,
chunkCount: resp.chunkCount ?? 0,
estimatedGasCostWei: resp.estimatedGasCostWei ?? "",
paymentMode: resp.paymentMode ?? "")
}

// MARK: - Wallet
Expand Down Expand Up @@ -273,7 +276,8 @@ private struct HealthResponseDTO: Decodable {
}

private struct CostAddressDTO: Decodable {
let cost: String
// PUT responses sometimes omit `cost`; default to empty downstream (#69 in PR queue).
let cost: String?
let address: String
}

Expand All @@ -294,7 +298,7 @@ private struct FileUploadPublicDTO: Decodable {
}

private struct CostDataMapDTO: Decodable {
let cost: String
let cost: String?
let dataMap: String
}

Expand All @@ -304,10 +308,13 @@ private struct DataDTO: Decodable {

private struct CostDTO: Decodable {
let cost: String
let file_size: UInt64?
let chunk_count: UInt32?
let estimated_gas_cost_wei: String?
let payment_mode: String?
// Wire fields are snake_case (file_size, chunk_count, …); the shared
// JSONDecoder uses .convertFromSnakeCase, so name them camelCase here
// — otherwise decoding silently nils them out and the caller sees zeros.
let fileSize: UInt64?
let chunkCount: UInt32?
let estimatedGasCostWei: String?
let paymentMode: String?
}

private struct WalletAddressDTO: Decodable {
Expand Down
18 changes: 9 additions & 9 deletions antd-swift/Sources/AntdSdk/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,55 @@ public class AntdError: Error, CustomStringConvertible {
}

public final class NotFoundError: AntdError {
public init(_ message: String, statusCode: Int = 404) {
public override init(_ message: String, statusCode: Int = 404) {
super.init(message, statusCode: statusCode)
}
}

public final class AlreadyExistsError: AntdError {
public init(_ message: String, statusCode: Int = 409) {
public override init(_ message: String, statusCode: Int = 409) {
super.init(message, statusCode: statusCode)
}
}

public final class ForkError: AntdError {
public init(_ message: String, statusCode: Int = 409) {
public override init(_ message: String, statusCode: Int = 409) {
super.init(message, statusCode: statusCode)
}
}

public final class BadRequestError: AntdError {
public init(_ message: String, statusCode: Int = 400) {
public override init(_ message: String, statusCode: Int = 400) {
super.init(message, statusCode: statusCode)
}
}

public final class PaymentError: AntdError {
public init(_ message: String, statusCode: Int = 402) {
public override init(_ message: String, statusCode: Int = 402) {
super.init(message, statusCode: statusCode)
}
}

public final class NetworkError: AntdError {
public init(_ message: String, statusCode: Int = 502) {
public override init(_ message: String, statusCode: Int = 502) {
super.init(message, statusCode: statusCode)
}
}

public final class TooLargeError: AntdError {
public init(_ message: String, statusCode: Int = 413) {
public override init(_ message: String, statusCode: Int = 413) {
super.init(message, statusCode: statusCode)
}
}

public final class InternalError: AntdError {
public init(_ message: String, statusCode: Int = 500) {
public override init(_ message: String, statusCode: Int = 500) {
super.init(message, statusCode: statusCode)
}
}

public final class ServiceUnavailableError: AntdError {
public init(_ message: String, statusCode: Int = 503) {
public override init(_ message: String, statusCode: Int = 503) {
super.init(message, statusCode: statusCode)
}
}
Expand Down