synor/sdk/swift/Sources/Synor/Governance/SynorGovernance.swift
Gulshan Yadav 6607223c9e feat(sdk): complete Phase 4 SDKs for all remaining languages
Add Economics, Governance, and Mining SDKs for:
- Java: Full SDK with CompletableFuture async operations
- Kotlin: Coroutine-based SDK with suspend functions
- Swift: Modern Swift SDK with async/await
- Flutter/Dart: Complete Dart SDK with Future-based API
- C: Header files and implementations with opaque handles
- C++: Modern C++17 with std::future and PIMPL pattern
- C#: Records, async/await Tasks, and IDisposable
- Ruby: Struct-based types with Faraday HTTP client

Also includes minor Dart lint fixes (const exceptions).
2026-01-28 08:33:20 +05:30

286 lines
12 KiB
Swift

import Foundation
/// Synor Governance SDK client for Swift.
///
/// Provides proposals, voting, DAOs, and vesting operations.
public class SynorGovernance {
private let config: GovernanceConfig
private let session: URLSession
private let decoder: JSONDecoder
private let encoder: JSONEncoder
private var closed = false
private static let finalStatuses: Set<ProposalStatus> = [.passed, .rejected, .executed, .cancelled]
public init(config: GovernanceConfig) {
self.config = config
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = config.timeout
sessionConfig.timeoutIntervalForResource = config.timeout * 2
self.session = URLSession(configuration: sessionConfig)
self.decoder = JSONDecoder()
self.encoder = JSONEncoder()
}
// MARK: - Proposal Operations
/// Create a proposal.
public func createProposal(_ proposal: ProposalDraft) async throws -> Proposal {
var body: [String: Any] = [
"title": proposal.title,
"description": proposal.description
]
if let url = proposal.discussionUrl { body["discussionUrl"] = url }
if let start = proposal.votingStartTime { body["votingStartTime"] = start }
if let end = proposal.votingEndTime { body["votingEndTime"] = end }
if let daoId = proposal.daoId { body["daoId"] = daoId }
if let actions = proposal.actions {
body["actions"] = actions.map { ["target": $0.target, "method": $0.method, "data": $0.data, "value": $0.value as Any] }
}
return try await post(path: "/proposals", body: body)
}
/// Get proposal by ID.
public func getProposal(proposalId: String) async throws -> Proposal {
return try await get(path: "/proposals/\(proposalId.urlEncoded)")
}
/// List proposals.
public func listProposals(filter: ProposalFilter? = nil) async throws -> [Proposal] {
var params: [String] = []
if let status = filter?.status { params.append("status=\(status.rawValue)") }
if let proposer = filter?.proposer { params.append("proposer=\(proposer.urlEncoded)") }
if let daoId = filter?.daoId { params.append("daoId=\(daoId.urlEncoded)") }
if let limit = filter?.limit { params.append("limit=\(limit)") }
if let offset = filter?.offset { params.append("offset=\(offset)") }
let path = params.isEmpty ? "/proposals" : "/proposals?\(params.joined(separator: "&"))"
let response: ProposalsResponse = try await get(path: path)
return response.proposals ?? []
}
/// Cancel a proposal.
public func cancelProposal(proposalId: String) async throws -> Proposal {
return try await post(path: "/proposals/\(proposalId.urlEncoded)/cancel", body: [:] as [String: String])
}
/// Execute a passed proposal.
public func executeProposal(proposalId: String) async throws -> Proposal {
return try await post(path: "/proposals/\(proposalId.urlEncoded)/execute", body: [:] as [String: String])
}
/// Wait for proposal to reach a final state.
public func waitForProposal(proposalId: String, pollInterval: TimeInterval = 60, maxWait: TimeInterval = 604800) async throws -> Proposal {
let deadline = Date().addingTimeInterval(maxWait)
while Date() < deadline {
let proposal = try await getProposal(proposalId: proposalId)
if Self.finalStatuses.contains(proposal.status) { return proposal }
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
}
throw GovernanceError(message: "Timeout waiting for proposal completion")
}
// MARK: - Voting Operations
/// Vote on a proposal.
public func vote(proposalId: String, vote: Vote, weight: String? = nil) async throws -> VoteReceipt {
var body: [String: Any] = ["choice": vote.choice.rawValue]
if let reason = vote.reason { body["reason"] = reason }
if let weight = weight { body["weight"] = weight }
return try await post(path: "/proposals/\(proposalId.urlEncoded)/vote", body: body)
}
/// Get votes for a proposal.
public func getVotes(proposalId: String) async throws -> [VoteReceipt] {
let response: VotesResponse = try await get(path: "/proposals/\(proposalId.urlEncoded)/votes")
return response.votes ?? []
}
/// Get my vote on a proposal.
public func getMyVote(proposalId: String) async throws -> VoteReceipt {
return try await get(path: "/proposals/\(proposalId.urlEncoded)/votes/me")
}
/// Delegate voting power.
public func delegate(delegatee: String, amount: String? = nil) async throws -> DelegationReceipt {
var body: [String: Any] = ["delegatee": delegatee]
if let amount = amount { body["amount"] = amount }
return try await post(path: "/voting/delegate", body: body)
}
/// Undelegate voting power.
public func undelegate(delegatee: String) async throws -> DelegationReceipt {
return try await post(path: "/voting/undelegate", body: ["delegatee": delegatee])
}
/// Get voting power for an address.
public func getVotingPower(address: String) async throws -> VotingPower {
return try await get(path: "/voting/power/\(address.urlEncoded)")
}
/// Get delegations for an address.
public func getDelegations(address: String) async throws -> [DelegationReceipt] {
let response: DelegationsResponse = try await get(path: "/voting/delegations/\(address.urlEncoded)")
return response.delegations ?? []
}
// MARK: - DAO Operations
/// Create a DAO.
public func createDao(_ daoConfig: DaoConfig) async throws -> Dao {
var body: [String: Any] = [
"name": daoConfig.name,
"description": daoConfig.description,
"type": daoConfig.type.rawValue,
"votingPeriodDays": daoConfig.votingPeriodDays,
"timelockDays": daoConfig.timelockDays
]
if let token = daoConfig.tokenAddress { body["tokenAddress"] = token }
if let quorum = daoConfig.quorumPercent { body["quorumPercent"] = quorum }
if let threshold = daoConfig.proposalThreshold { body["proposalThreshold"] = threshold }
if let members = daoConfig.multisigMembers { body["multisigMembers"] = members }
if let threshold = daoConfig.multisigThreshold { body["multisigThreshold"] = threshold }
return try await post(path: "/daos", body: body)
}
/// Get DAO by ID.
public func getDao(daoId: String) async throws -> Dao {
return try await get(path: "/daos/\(daoId.urlEncoded)")
}
/// List DAOs.
public func listDaos(limit: Int? = nil, offset: Int? = nil) async throws -> [Dao] {
var params: [String] = []
if let limit = limit { params.append("limit=\(limit)") }
if let offset = offset { params.append("offset=\(offset)") }
let path = params.isEmpty ? "/daos" : "/daos?\(params.joined(separator: "&"))"
let response: DaosResponse = try await get(path: path)
return response.daos ?? []
}
/// Get DAO treasury.
public func getDaoTreasury(daoId: String) async throws -> DaoTreasury {
return try await get(path: "/daos/\(daoId.urlEncoded)/treasury")
}
/// Get DAO members.
public func getDaoMembers(daoId: String) async throws -> [String] {
let response: MembersResponse = try await get(path: "/daos/\(daoId.urlEncoded)/members")
return response.members ?? []
}
// MARK: - Vesting Operations
/// Create a vesting schedule.
public func createVestingSchedule(_ schedule: VestingSchedule) async throws -> VestingContract {
let body: [String: Any] = [
"beneficiary": schedule.beneficiary,
"totalAmount": schedule.totalAmount,
"startTime": schedule.startTime,
"cliffDuration": schedule.cliffDuration,
"vestingDuration": schedule.vestingDuration,
"revocable": schedule.revocable
]
return try await post(path: "/vesting", body: body)
}
/// Get vesting contract.
public func getVestingContract(contractId: String) async throws -> VestingContract {
return try await get(path: "/vesting/\(contractId.urlEncoded)")
}
/// List vesting contracts.
public func listVestingContracts(beneficiary: String? = nil) async throws -> [VestingContract] {
let path = beneficiary != nil ? "/vesting?beneficiary=\(beneficiary!.urlEncoded)" : "/vesting"
let response: VestingContractsResponse = try await get(path: path)
return response.contracts ?? []
}
/// Claim vested tokens.
public func claimVested(contractId: String) async throws -> ClaimReceipt {
return try await post(path: "/vesting/\(contractId.urlEncoded)/claim", body: [:] as [String: String])
}
/// Revoke vesting.
public func revokeVesting(contractId: String) async throws -> VestingContract {
return try await post(path: "/vesting/\(contractId.urlEncoded)/revoke", body: [:] as [String: String])
}
/// Get releasable amount.
public func getReleasableAmount(contractId: String) async throws -> String {
let response: [String: String] = try await get(path: "/vesting/\(contractId.urlEncoded)/releasable")
return response["amount"] ?? "0"
}
// MARK: - Lifecycle
public func close() { closed = true }
public var isClosed: Bool { closed }
public func healthCheck() async -> Bool {
do {
let response: [String: String] = try await get(path: "/health")
return response["status"] == "healthy"
} catch { return false }
}
// MARK: - Private Methods
private func get<T: Decodable>(path: String) async throws -> T {
return try await request(method: "GET", path: path)
}
private func post<T: Decodable>(path: String, body: Any) async throws -> T {
return try await request(method: "POST", path: path, body: body)
}
private func request<T: Decodable>(method: String, path: String, body: Any? = nil) async throws -> T {
guard !closed else { throw GovernanceError(message: "Client has been closed") }
var lastError: Error?
for attempt in 0..<config.retries {
do {
return try await doRequest(method: method, path: path, body: body)
} catch {
lastError = error
if attempt < config.retries - 1 {
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
}
}
}
throw lastError ?? GovernanceError(message: "Unknown error")
}
private func doRequest<T: Decodable>(method: String, path: String, body: Any?) async throws -> T {
guard let url = URL(string: config.endpoint + path) else { throw GovernanceError(message: "Invalid URL") }
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
if let body = body { request.httpBody = try JSONSerialization.data(withJSONObject: body) }
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else { throw GovernanceError(message: "Invalid response") }
if httpResponse.statusCode >= 400 {
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
throw GovernanceError(
message: errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)",
code: errorInfo?["code"] as? String,
statusCode: httpResponse.statusCode
)
}
return try decoder.decode(T.self, from: data)
}
}
extension String {
fileprivate var urlEncoded: String {
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
}
}