/// Synor Governance SDK Client for Flutter library synor_governance; import 'dart:convert'; import 'package:http/http.dart' as http; import 'types.dart'; export 'types.dart'; /// Synor Governance Client class SynorGovernance { final GovernanceConfig config; final http.Client _client; bool _closed = false; static const _finalStatuses = { ProposalStatus.passed, ProposalStatus.rejected, ProposalStatus.executed, ProposalStatus.cancelled, }; SynorGovernance(this.config) : _client = http.Client(); // ==================== Proposal Operations ==================== Future createProposal(ProposalDraft proposal) async { final resp = await _request('POST', '/proposals', proposal.toJson()); return Proposal.fromJson(resp); } Future getProposal(String proposalId) async { final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}'); return Proposal.fromJson(resp); } Future> listProposals({ProposalFilter? filter}) async { final params = []; if (filter?.status != null) params.add('status=${filter!.status!.name}'); if (filter?.proposer != null) params.add('proposer=${Uri.encodeComponent(filter!.proposer!)}'); if (filter?.daoId != null) params.add('daoId=${Uri.encodeComponent(filter!.daoId!)}'); if (filter?.limit != null) params.add('limit=${filter!.limit}'); if (filter?.offset != null) params.add('offset=${filter!.offset}'); final path = params.isEmpty ? '/proposals' : '/proposals?${params.join('&')}'; final resp = await _request('GET', path); return (resp['proposals'] as List).map((e) => Proposal.fromJson(e)).toList(); } Future cancelProposal(String proposalId) async { final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/cancel'); return Proposal.fromJson(resp); } Future executeProposal(String proposalId) async { final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/execute'); return Proposal.fromJson(resp); } Future waitForProposal( String proposalId, { Duration pollInterval = const Duration(minutes: 1), Duration maxWait = const Duration(days: 7), }) async { final deadline = DateTime.now().add(maxWait); while (DateTime.now().isBefore(deadline)) { final proposal = await getProposal(proposalId); if (_finalStatuses.contains(proposal.status)) return proposal; await Future.delayed(pollInterval); } throw const GovernanceException('Timeout waiting for proposal completion'); } // ==================== Voting Operations ==================== Future vote(String proposalId, Vote vote, {String? weight}) async { final body = {'choice': vote.choice.name}; if (vote.reason != null) body['reason'] = vote.reason; if (weight != null) body['weight'] = weight; final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/vote', body); return VoteReceipt.fromJson(resp); } Future> getVotes(String proposalId) async { final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}/votes'); return (resp['votes'] as List).map((e) => VoteReceipt.fromJson(e)).toList(); } Future getMyVote(String proposalId) async { final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}/votes/me'); return VoteReceipt.fromJson(resp); } Future delegate(String delegatee, {String? amount}) async { final body = {'delegatee': delegatee}; if (amount != null) body['amount'] = amount; final resp = await _request('POST', '/voting/delegate', body); return DelegationReceipt.fromJson(resp); } Future undelegate(String delegatee) async { final resp = await _request('POST', '/voting/undelegate', {'delegatee': delegatee}); return DelegationReceipt.fromJson(resp); } Future getVotingPower(String address) async { final resp = await _request('GET', '/voting/power/${Uri.encodeComponent(address)}'); return VotingPower.fromJson(resp); } Future> getDelegations(String address) async { final resp = await _request('GET', '/voting/delegations/${Uri.encodeComponent(address)}'); return (resp['delegations'] as List).map((e) => DelegationReceipt.fromJson(e)).toList(); } // ==================== DAO Operations ==================== Future createDao(DaoConfig daoConfig) async { final resp = await _request('POST', '/daos', daoConfig.toJson()); return Dao.fromJson(resp); } Future getDao(String daoId) async { final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}'); return Dao.fromJson(resp); } Future> listDaos({int? limit, int? offset}) async { final params = []; if (limit != null) params.add('limit=$limit'); if (offset != null) params.add('offset=$offset'); final path = params.isEmpty ? '/daos' : '/daos?${params.join('&')}'; final resp = await _request('GET', path); return (resp['daos'] as List).map((e) => Dao.fromJson(e)).toList(); } Future getDaoTreasury(String daoId) async { final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}/treasury'); return DaoTreasury.fromJson(resp); } Future> getDaoMembers(String daoId) async { final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}/members'); return (resp['members'] as List).cast(); } // ==================== Vesting Operations ==================== Future createVestingSchedule(VestingSchedule schedule) async { final resp = await _request('POST', '/vesting', schedule.toJson()); return VestingContract.fromJson(resp); } Future getVestingContract(String contractId) async { final resp = await _request('GET', '/vesting/${Uri.encodeComponent(contractId)}'); return VestingContract.fromJson(resp); } Future> listVestingContracts({String? beneficiary}) async { final path = beneficiary != null ? '/vesting?beneficiary=${Uri.encodeComponent(beneficiary)}' : '/vesting'; final resp = await _request('GET', path); return (resp['contracts'] as List).map((e) => VestingContract.fromJson(e)).toList(); } Future claimVested(String contractId) async { final resp = await _request('POST', '/vesting/${Uri.encodeComponent(contractId)}/claim'); return ClaimReceipt.fromJson(resp); } Future revokeVesting(String contractId) async { final resp = await _request('POST', '/vesting/${Uri.encodeComponent(contractId)}/revoke'); return VestingContract.fromJson(resp); } Future getReleasableAmount(String contractId) async { final resp = await _request('GET', '/vesting/${Uri.encodeComponent(contractId)}/releasable'); return resp['amount'] as String; } // ==================== Lifecycle ==================== void close() { _closed = true; _client.close(); } bool get isClosed => _closed; Future healthCheck() async { try { final resp = await _request('GET', '/health'); return resp['status'] == 'healthy'; } catch (_) { return false; } } // ==================== Private Methods ==================== Future> _request(String method, String path, [Map? body]) async { if (_closed) throw const GovernanceException('Client has been closed'); Exception? lastError; for (var attempt = 0; attempt < config.retries; attempt++) { try { return await _doRequest(method, path, body); } catch (e) { lastError = e as Exception; if (attempt < config.retries - 1) { await Future.delayed(Duration(seconds: 1 << attempt)); } } } throw lastError ?? const GovernanceException('Unknown error'); } Future> _doRequest(String method, String path, Map? body) async { final uri = Uri.parse('${config.endpoint}$path'); final headers = { 'Authorization': 'Bearer ${config.apiKey}', 'Content-Type': 'application/json', 'X-SDK-Version': 'flutter/0.1.0', }; http.Response response; switch (method) { case 'GET': response = await _client.get(uri, headers: headers).timeout(config.timeout); break; case 'POST': response = await _client.post(uri, headers: headers, body: body != null ? jsonEncode(body) : null).timeout(config.timeout); break; case 'DELETE': response = await _client.delete(uri, headers: headers).timeout(config.timeout); break; default: throw GovernanceException('Unknown method: $method'); } final respBody = jsonDecode(response.body) as Map; if (response.statusCode >= 400) { throw GovernanceException( respBody['message'] as String? ?? 'HTTP ${response.statusCode}', code: respBody['code'] as String?, statusCode: response.statusCode, ); } return respBody; } }