Skip to content
Closed
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
1 change: 1 addition & 0 deletions antd-swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let package = Package(
.target(
name: "AntdSdk",
dependencies: [
.product(name: "GRPCCore", 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
14 changes: 13 additions & 1 deletion antd-swift/Sources/AntdSdk/AntdClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,20 @@ public enum AntdClient {
}

/// Create a gRPC client connecting to the antd daemon.
///
/// > Requires macOS 15+ / iOS 18+ / tvOS 18+ / watchOS 11+ (grpc-swift 2.x).
/// > Use ``createRest(baseURL:timeout:)`` on older platforms.
///
/// - Parameter target: gRPC target address (default: localhost:50051)
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *)
public static func createGrpc(target: String = "localhost:50051") -> AntdClientProtocol {
AntdGrpcClient(target: target)
}

/// Create a client using the specified transport.
///
/// > `transport: "grpc"` requires macOS 15+ / iOS 18+ / tvOS 18+ / watchOS 11+.
///
/// - Parameters:
/// - transport: "rest" or "grpc"
/// - endpoint: Optional custom endpoint override
Expand All @@ -34,7 +42,11 @@ public enum AntdClient {
case "rest":
return createRest(baseURL: endpoint ?? "http://localhost:8082")
case "grpc":
return createGrpc(target: endpoint ?? "localhost:50051")
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *) {
return createGrpc(target: endpoint ?? "localhost:50051")
} else {
fatalError("gRPC transport requires macOS 15+ / iOS 18+. Use 'rest' on older platforms.")
}
default:
fatalError("Unknown transport: \(transport). Use 'rest' or 'grpc'.")
}
Expand Down
90 changes: 76 additions & 14 deletions antd-swift/Sources/AntdSdk/AntdGrpcClient.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,62 @@
import Foundation
import GRPCCore
import GRPCNIOTransportHTTP2
import GRPCProtobuf

/// gRPC client for the antd daemon.
/// gRPC client for the antd daemon (grpc-swift 2.x).
///
/// > Note: The gRPC client requires the generated protobuf stubs from `antd/proto/antd/v1/`.
/// > Run `scripts/generate-protos.sh` to generate them into `Sources/AntdSdk/Proto/`.
/// > Until proto generation is set up, use ``AntdRestClient`` instead.
/// > Note: grpc-swift 2.x requires macOS 15+ / iOS 18+ / tvOS 18+ / watchOS 11+.
/// > Use ``AntdRestClient`` via ``AntdClient/createRest(baseURL:timeout:)`` on
/// > older platforms.
///
/// V2-286 implements the wallet surface (`walletAddress`, `walletBalance`,
/// `walletApprove`); other RPCs throw `notImplemented()` until subsequent
/// gRPC fan-out work lands.
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, *)
public final class AntdGrpcClient: AntdClientProtocol, @unchecked Sendable {

private let target: String
private let host: String
private let port: Int

public init(target: String = "localhost:50051") {
self.target = target
let (h, p) = Self.parseTarget(target)
self.host = h
self.port = p
}

/// Accepts `"host:port"` or `"host"` (default port 50051).
private static func parseTarget(_ target: String) -> (String, Int) {
if let colon = target.lastIndex(of: ":"),
let p = Int(target[target.index(after: colon)...]) {
return (String(target[..<colon]), p)
}
return (target, 50051)
}

// MARK: - Placeholder implementations
// Full gRPC implementation requires generated proto stubs.
// The REST client is the recommended default. These methods
// throw an error until proto generation is configured.
// MARK: - Connection helper

/// Opens a one-shot grpc-swift connection to the daemon for the duration of
/// `body`. Per-call connection setup keeps the SDK surface simple; the cost
/// is one TCP handshake per RPC, which is acceptable for the wallet flow
/// (caller queries balance/address occasionally, approves once).
private func withGRPC<T: Sendable>(
_ body: @Sendable (GRPCClient<HTTP2ClientTransport.Posix>) async throws -> T
) async throws -> T {
do {
let transport = try HTTP2ClientTransport.Posix(
target: .dns(host: host, port: port),
transportSecurity: .plaintext
)
return try await withGRPCClient(transport: transport, handleClient: body)
} catch let rpcError as RPCError {
throw ErrorMapping.fromGRPCStatus(code: rpcError.code.rawValue, detail: rpcError.message)
}
}

// MARK: - Out-of-scope (later gRPC tickets)

private func notImplemented() -> AntdError {
InternalError("gRPC client requires generated proto stubs. Use AntdClient.createRest() or run scripts/generate-protos.sh first.")
InternalError("not yet implemented on the gRPC client; use AntdClient.createRest()")
}

public func health() async throws -> HealthStatus { throw notImplemented() }
Expand All @@ -39,12 +74,39 @@ public final class AntdGrpcClient: AntdClientProtocol, @unchecked Sendable {
public func filePutPublic(path: String, paymentMode: PaymentMode = .auto) async throws -> FilePutPublicResult { throw notImplemented() }
public func fileGetPublic(address: String, destPath: String) async throws { throw notImplemented() }
public func fileCost(path: String, isPublic: Bool = true, paymentMode: PaymentMode = .auto) async throws -> UploadCostEstimate { 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() }
public func prepareUpload(path: String, visibility: String? = nil) async throws -> PrepareUploadResult { throw notImplemented() }
public func prepareUploadPublic(path: String) async throws -> PrepareUploadResult { throw notImplemented() }
public func prepareDataUpload(_ data: Data) async throws -> PrepareUploadResult { throw notImplemented() }
public func finalizeUpload(uploadId: String, txHashes: [String: String]) async throws -> FinalizeUploadResult { throw notImplemented() }
public func finalizeMerkleUpload(uploadId: String, winnerPoolHash: String) async throws -> FinalizeMerkleUploadResult { throw notImplemented() }

// MARK: - Wallet (V2-286)
//
// A missing daemon wallet emits gRPC `failedPrecondition`, which
// `ErrorMapping.fromGRPCStatus` maps to `PaymentError`. (Semantic a bit
// off vs REST's 503 but matches every other SDK's gRPC->SDK mapping.)

public func walletAddress() async throws -> WalletAddress {
try await withGRPC { client in
let req = Antd_V1_GetWalletAddressRequest()
let resp = try await Antd_V1_WalletService.Client(wrapping: client).getAddress(req)
return WalletAddress(address: resp.address)
}
}

public func walletBalance() async throws -> WalletBalance {
try await withGRPC { client in
let req = Antd_V1_GetWalletBalanceRequest()
let resp = try await Antd_V1_WalletService.Client(wrapping: client).getBalance(req)
return WalletBalance(balance: resp.balance, gasBalance: resp.gasBalance)
}
}

public func walletApprove() async throws -> Bool {
try await withGRPC { client in
let req = Antd_V1_WalletApproveRequest()
let resp = try await Antd_V1_WalletService.Client(wrapping: client).approve(req)
return resp.approved
}
}
}
Loading