diff --git a/antd-swift/Package.swift b/antd-swift/Package.swift index c7b90a6..9be2ec8 100644 --- a/antd-swift/Package.swift +++ b/antd-swift/Package.swift @@ -13,7 +13,8 @@ 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"), ], @@ -21,7 +22,7 @@ let package = Package( .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"), ] diff --git a/antd-swift/Sources/AntdExamples/Main.swift b/antd-swift/Sources/AntdExamples/Main.swift index 8781cf5..c9fc7b1 100644 --- a/antd-swift/Sources/AntdExamples/Main.swift +++ b/antd-swift/Sources/AntdExamples/Main.swift @@ -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) + } + } } diff --git a/antd-swift/Sources/AntdSdk/AntdRestClient.swift b/antd-swift/Sources/AntdSdk/AntdRestClient.swift index ed24372..a8e8c96 100644 --- a/antd-swift/Sources/AntdSdk/AntdRestClient.swift +++ b/antd-swift/Sources/AntdSdk/AntdRestClient.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif public final class AntdRestClient: AntdClientProtocol, @unchecked Sendable { @@ -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 { @@ -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 { @@ -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 { @@ -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 @@ -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 } @@ -294,7 +298,7 @@ private struct FileUploadPublicDTO: Decodable { } private struct CostDataMapDTO: Decodable { - let cost: String + let cost: String? let dataMap: String } @@ -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 { diff --git a/antd-swift/Sources/AntdSdk/Errors.swift b/antd-swift/Sources/AntdSdk/Errors.swift index de09bbc..d9a0d45 100644 --- a/antd-swift/Sources/AntdSdk/Errors.swift +++ b/antd-swift/Sources/AntdSdk/Errors.swift @@ -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) } }