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).
286 lines
12 KiB
Swift
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
|
|
}
|
|
}
|