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 = [.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(path: String) async throws -> T { return try await request(method: "GET", path: path) } private func post(path: String, body: Any) async throws -> T { return try await request(method: "POST", path: path, body: body) } private func request(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..(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 } }