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).
250 lines
9.1 KiB
Dart
250 lines
9.1 KiB
Dart
/// 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<Proposal> createProposal(ProposalDraft proposal) async {
|
|
final resp = await _request('POST', '/proposals', proposal.toJson());
|
|
return Proposal.fromJson(resp);
|
|
}
|
|
|
|
Future<Proposal> getProposal(String proposalId) async {
|
|
final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}');
|
|
return Proposal.fromJson(resp);
|
|
}
|
|
|
|
Future<List<Proposal>> listProposals({ProposalFilter? filter}) async {
|
|
final params = <String>[];
|
|
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<Proposal> cancelProposal(String proposalId) async {
|
|
final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/cancel');
|
|
return Proposal.fromJson(resp);
|
|
}
|
|
|
|
Future<Proposal> executeProposal(String proposalId) async {
|
|
final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/execute');
|
|
return Proposal.fromJson(resp);
|
|
}
|
|
|
|
Future<Proposal> 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<VoteReceipt> vote(String proposalId, Vote vote, {String? weight}) async {
|
|
final body = <String, dynamic>{'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<List<VoteReceipt>> 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<VoteReceipt> getMyVote(String proposalId) async {
|
|
final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}/votes/me');
|
|
return VoteReceipt.fromJson(resp);
|
|
}
|
|
|
|
Future<DelegationReceipt> delegate(String delegatee, {String? amount}) async {
|
|
final body = <String, dynamic>{'delegatee': delegatee};
|
|
if (amount != null) body['amount'] = amount;
|
|
final resp = await _request('POST', '/voting/delegate', body);
|
|
return DelegationReceipt.fromJson(resp);
|
|
}
|
|
|
|
Future<DelegationReceipt> undelegate(String delegatee) async {
|
|
final resp = await _request('POST', '/voting/undelegate', {'delegatee': delegatee});
|
|
return DelegationReceipt.fromJson(resp);
|
|
}
|
|
|
|
Future<VotingPower> getVotingPower(String address) async {
|
|
final resp = await _request('GET', '/voting/power/${Uri.encodeComponent(address)}');
|
|
return VotingPower.fromJson(resp);
|
|
}
|
|
|
|
Future<List<DelegationReceipt>> 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<Dao> createDao(DaoConfig daoConfig) async {
|
|
final resp = await _request('POST', '/daos', daoConfig.toJson());
|
|
return Dao.fromJson(resp);
|
|
}
|
|
|
|
Future<Dao> getDao(String daoId) async {
|
|
final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}');
|
|
return Dao.fromJson(resp);
|
|
}
|
|
|
|
Future<List<Dao>> listDaos({int? limit, int? offset}) async {
|
|
final params = <String>[];
|
|
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<DaoTreasury> getDaoTreasury(String daoId) async {
|
|
final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}/treasury');
|
|
return DaoTreasury.fromJson(resp);
|
|
}
|
|
|
|
Future<List<String>> getDaoMembers(String daoId) async {
|
|
final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}/members');
|
|
return (resp['members'] as List).cast<String>();
|
|
}
|
|
|
|
// ==================== Vesting Operations ====================
|
|
|
|
Future<VestingContract> createVestingSchedule(VestingSchedule schedule) async {
|
|
final resp = await _request('POST', '/vesting', schedule.toJson());
|
|
return VestingContract.fromJson(resp);
|
|
}
|
|
|
|
Future<VestingContract> getVestingContract(String contractId) async {
|
|
final resp = await _request('GET', '/vesting/${Uri.encodeComponent(contractId)}');
|
|
return VestingContract.fromJson(resp);
|
|
}
|
|
|
|
Future<List<VestingContract>> 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<ClaimReceipt> claimVested(String contractId) async {
|
|
final resp = await _request('POST', '/vesting/${Uri.encodeComponent(contractId)}/claim');
|
|
return ClaimReceipt.fromJson(resp);
|
|
}
|
|
|
|
Future<VestingContract> revokeVesting(String contractId) async {
|
|
final resp = await _request('POST', '/vesting/${Uri.encodeComponent(contractId)}/revoke');
|
|
return VestingContract.fromJson(resp);
|
|
}
|
|
|
|
Future<String> 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<bool> healthCheck() async {
|
|
try {
|
|
final resp = await _request('GET', '/health');
|
|
return resp['status'] == 'healthy';
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== Private Methods ====================
|
|
|
|
Future<Map<String, dynamic>> _request(String method, String path, [Map<String, dynamic>? 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<Map<String, dynamic>> _doRequest(String method, String path, Map<String, dynamic>? 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<String, dynamic>;
|
|
if (response.statusCode >= 400) {
|
|
throw GovernanceException(
|
|
respBody['message'] as String? ?? 'HTTP ${response.statusCode}',
|
|
code: respBody['code'] as String?,
|
|
statusCode: response.statusCode,
|
|
);
|
|
}
|
|
return respBody;
|
|
}
|
|
}
|