From 6607223c9e8666f1797cab23c457447a1849aaa5 Mon Sep 17 00:00:00 2001 From: Gulshan Yadav Date: Wed, 28 Jan 2026 08:33:20 +0530 Subject: [PATCH] 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). --- sdk/c/include/synor/economics.h | 345 +++++++++++ sdk/c/include/synor/governance.h | 364 +++++++++++ sdk/c/include/synor/mining.h | 394 ++++++++++++ sdk/c/src/economics/economics.c | 424 +++++++++++++ sdk/c/src/governance/governance.c | 524 ++++++++++++++++ sdk/c/src/mining/mining.c | 563 ++++++++++++++++++ sdk/cpp/include/synor/economics.hpp | 282 +++++++++ sdk/cpp/include/synor/governance.hpp | 300 ++++++++++ sdk/cpp/include/synor/mining.hpp | 334 +++++++++++ .../Synor.Sdk/Economics/SynorEconomics.cs | 165 +++++ sdk/csharp/Synor.Sdk/Economics/Types.cs | 181 ++++++ .../Synor.Sdk/Governance/SynorGovernance.cs | 212 +++++++ sdk/csharp/Synor.Sdk/Governance/Types.cs | 195 ++++++ sdk/csharp/Synor.Sdk/Mining/SynorMining.cs | 215 +++++++ sdk/csharp/Synor.Sdk/Mining/Types.cs | 219 +++++++ sdk/flutter/lib/src/database/client.dart | 4 +- sdk/flutter/lib/src/economics/client.dart | 217 +++++++ sdk/flutter/lib/src/economics/types.dart | 532 +++++++++++++++++ sdk/flutter/lib/src/governance/client.dart | 250 ++++++++ sdk/flutter/lib/src/governance/types.dart | 497 ++++++++++++++++ sdk/flutter/lib/src/hosting/client.dart | 4 +- sdk/flutter/lib/src/mining/client.dart | 231 +++++++ sdk/flutter/lib/src/mining/types.dart | 555 +++++++++++++++++ .../io/synor/economics/SynorEconomics.java | 298 +++++++++ .../main/java/io/synor/economics/Types.java | 383 ++++++++++++ .../io/synor/governance/SynorGovernance.java | 369 ++++++++++++ .../main/java/io/synor/governance/Types.java | 407 +++++++++++++ .../java/io/synor/mining/SynorMining.java | 329 ++++++++++ .../src/main/java/io/synor/mining/Types.java | 425 +++++++++++++ .../io/synor/economics/SynorEconomics.kt | 415 +++++++++++++ .../io/synor/governance/SynorGovernance.kt | 465 +++++++++++++++ .../kotlin/io/synor/mining/SynorMining.kt | 464 +++++++++++++++ sdk/ruby/lib/synor_economics.rb | 20 + sdk/ruby/lib/synor_economics/client.rb | 366 ++++++++++++ sdk/ruby/lib/synor_economics/types.rb | 85 +++ sdk/ruby/lib/synor_economics/version.rb | 5 + sdk/ruby/lib/synor_governance.rb | 20 + sdk/ruby/lib/synor_governance/client.rb | 422 +++++++++++++ sdk/ruby/lib/synor_governance/types.rb | 74 +++ sdk/ruby/lib/synor_governance/version.rb | 5 + sdk/ruby/lib/synor_mining.rb | 20 + sdk/ruby/lib/synor_mining/client.rb | 437 ++++++++++++++ sdk/ruby/lib/synor_mining/types.rb | 89 +++ sdk/ruby/lib/synor_mining/version.rb | 5 + .../Synor/Economics/SynorEconomics.swift | 286 +++++++++ sdk/swift/Sources/Synor/Economics/Types.swift | 314 ++++++++++ .../Synor/Governance/SynorGovernance.swift | 286 +++++++++ .../Sources/Synor/Governance/Types.swift | 374 ++++++++++++ .../Sources/Synor/Mining/SynorMining.swift | 279 +++++++++ sdk/swift/Sources/Synor/Mining/Types.swift | 352 +++++++++++ 50 files changed, 13997 insertions(+), 4 deletions(-) create mode 100644 sdk/c/include/synor/economics.h create mode 100644 sdk/c/include/synor/governance.h create mode 100644 sdk/c/include/synor/mining.h create mode 100644 sdk/c/src/economics/economics.c create mode 100644 sdk/c/src/governance/governance.c create mode 100644 sdk/c/src/mining/mining.c create mode 100644 sdk/cpp/include/synor/economics.hpp create mode 100644 sdk/cpp/include/synor/governance.hpp create mode 100644 sdk/cpp/include/synor/mining.hpp create mode 100644 sdk/csharp/Synor.Sdk/Economics/SynorEconomics.cs create mode 100644 sdk/csharp/Synor.Sdk/Economics/Types.cs create mode 100644 sdk/csharp/Synor.Sdk/Governance/SynorGovernance.cs create mode 100644 sdk/csharp/Synor.Sdk/Governance/Types.cs create mode 100644 sdk/csharp/Synor.Sdk/Mining/SynorMining.cs create mode 100644 sdk/csharp/Synor.Sdk/Mining/Types.cs create mode 100644 sdk/flutter/lib/src/economics/client.dart create mode 100644 sdk/flutter/lib/src/economics/types.dart create mode 100644 sdk/flutter/lib/src/governance/client.dart create mode 100644 sdk/flutter/lib/src/governance/types.dart create mode 100644 sdk/flutter/lib/src/mining/client.dart create mode 100644 sdk/flutter/lib/src/mining/types.dart create mode 100644 sdk/java/src/main/java/io/synor/economics/SynorEconomics.java create mode 100644 sdk/java/src/main/java/io/synor/economics/Types.java create mode 100644 sdk/java/src/main/java/io/synor/governance/SynorGovernance.java create mode 100644 sdk/java/src/main/java/io/synor/governance/Types.java create mode 100644 sdk/java/src/main/java/io/synor/mining/SynorMining.java create mode 100644 sdk/java/src/main/java/io/synor/mining/Types.java create mode 100644 sdk/kotlin/src/main/kotlin/io/synor/economics/SynorEconomics.kt create mode 100644 sdk/kotlin/src/main/kotlin/io/synor/governance/SynorGovernance.kt create mode 100644 sdk/kotlin/src/main/kotlin/io/synor/mining/SynorMining.kt create mode 100644 sdk/ruby/lib/synor_economics.rb create mode 100644 sdk/ruby/lib/synor_economics/client.rb create mode 100644 sdk/ruby/lib/synor_economics/types.rb create mode 100644 sdk/ruby/lib/synor_economics/version.rb create mode 100644 sdk/ruby/lib/synor_governance.rb create mode 100644 sdk/ruby/lib/synor_governance/client.rb create mode 100644 sdk/ruby/lib/synor_governance/types.rb create mode 100644 sdk/ruby/lib/synor_governance/version.rb create mode 100644 sdk/ruby/lib/synor_mining.rb create mode 100644 sdk/ruby/lib/synor_mining/client.rb create mode 100644 sdk/ruby/lib/synor_mining/types.rb create mode 100644 sdk/ruby/lib/synor_mining/version.rb create mode 100644 sdk/swift/Sources/Synor/Economics/SynorEconomics.swift create mode 100644 sdk/swift/Sources/Synor/Economics/Types.swift create mode 100644 sdk/swift/Sources/Synor/Governance/SynorGovernance.swift create mode 100644 sdk/swift/Sources/Synor/Governance/Types.swift create mode 100644 sdk/swift/Sources/Synor/Mining/SynorMining.swift create mode 100644 sdk/swift/Sources/Synor/Mining/Types.swift diff --git a/sdk/c/include/synor/economics.h b/sdk/c/include/synor/economics.h new file mode 100644 index 0000000..96d0bd0 --- /dev/null +++ b/sdk/c/include/synor/economics.h @@ -0,0 +1,345 @@ +/** + * @file economics.h + * @brief Synor Economics SDK for C + * + * Pricing, billing, staking, and discount management. + */ + +#ifndef SYNOR_ECONOMICS_H +#define SYNOR_ECONOMICS_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ==================== Error Types ==================== */ + +typedef enum { + SYNOR_ECONOMICS_OK = 0, + SYNOR_ECONOMICS_ERROR_CLIENT_CLOSED, + SYNOR_ECONOMICS_ERROR_NETWORK, + SYNOR_ECONOMICS_ERROR_HTTP, + SYNOR_ECONOMICS_ERROR_ENCODING, + SYNOR_ECONOMICS_ERROR_DECODING, + SYNOR_ECONOMICS_ERROR_INVALID_RESPONSE, + SYNOR_ECONOMICS_ERROR_MEMORY, + SYNOR_ECONOMICS_ERROR_TIMEOUT, + SYNOR_ECONOMICS_ERROR_UNKNOWN +} synor_economics_error_t; + +/* ==================== Configuration ==================== */ + +typedef struct { + const char* api_key; + const char* endpoint; /* Default: https://economics.synor.io/v1 */ + uint32_t timeout_ms; /* Default: 30000 */ + uint32_t retries; /* Default: 3 */ + bool debug; +} synor_economics_config_t; + +/* ==================== Enums ==================== */ + +typedef enum { + SYNOR_SERVICE_COMPUTE, + SYNOR_SERVICE_STORAGE, + SYNOR_SERVICE_DATABASE, + SYNOR_SERVICE_HOSTING, + SYNOR_SERVICE_RPC, + SYNOR_SERVICE_BRIDGE +} synor_service_type_t; + +typedef enum { + SYNOR_BILLING_HOURLY, + SYNOR_BILLING_DAILY, + SYNOR_BILLING_WEEKLY, + SYNOR_BILLING_MONTHLY +} synor_billing_period_t; + +typedef enum { + SYNOR_STAKE_ACTIVE, + SYNOR_STAKE_UNSTAKING, + SYNOR_STAKE_WITHDRAWN +} synor_stake_status_t; + +typedef enum { + SYNOR_DISCOUNT_PERCENTAGE, + SYNOR_DISCOUNT_FIXED, + SYNOR_DISCOUNT_CREDITS +} synor_discount_type_t; + +typedef enum { + SYNOR_INVOICE_PENDING, + SYNOR_INVOICE_PAID, + SYNOR_INVOICE_OVERDUE, + SYNOR_INVOICE_CANCELLED +} synor_invoice_status_t; + +/* ==================== Pricing Types ==================== */ + +typedef struct { + synor_service_type_t service; + char* unit; + char* price_per_unit; + char* currency; + double minimum; + double maximum; +} synor_service_pricing_t; + +typedef struct { + synor_service_pricing_t* prices; + size_t count; +} synor_service_pricing_list_t; + +typedef struct { + synor_service_type_t service; + double compute_units; + double storage_bytes; + double bandwidth_bytes; + int64_t duration_seconds; + int32_t requests; +} synor_usage_metrics_t; + +typedef struct { + synor_service_type_t service; + char* amount; + char* currency; + synor_usage_metrics_t usage; +} synor_price_result_t; + +typedef struct { + synor_service_type_t service; + synor_usage_metrics_t projected_usage; + synor_billing_period_t period; +} synor_usage_plan_item_t; + +typedef struct { + synor_usage_plan_item_t* items; + size_t count; +} synor_usage_plan_t; + +typedef struct { + synor_service_type_t service; + char* amount; +} synor_cost_item_t; + +typedef struct { + char* total; + char* currency; + synor_cost_item_t* breakdown; + size_t breakdown_count; + char* discount_applied; + synor_billing_period_t period; +} synor_cost_estimate_t; + +/* ==================== Billing Types ==================== */ + +typedef struct { + synor_service_type_t service; + double compute_units; + double storage_bytes; + double bandwidth_bytes; + int32_t requests; + char* cost; +} synor_usage_item_t; + +typedef struct { + synor_billing_period_t period; + int64_t start_date; + int64_t end_date; + synor_usage_item_t* items; + size_t items_count; + char* total_cost; + char* currency; +} synor_usage_t; + +typedef struct { + char* service; + char* description; + int32_t quantity; + char* unit_price; + char* amount; +} synor_invoice_line_t; + +typedef struct { + char* id; + int64_t date; + int64_t due_date; + synor_invoice_status_t status; + synor_invoice_line_t* lines; + size_t lines_count; + char* subtotal; + char* discount; + char* tax; + char* total; + char* currency; + char* pdf_url; +} synor_invoice_t; + +typedef struct { + synor_invoice_t* invoices; + size_t count; +} synor_invoice_list_t; + +typedef struct { + char* available; + char* pending; + char* staked; + char* total; + char* currency; +} synor_account_balance_t; + +/* ==================== Staking Types ==================== */ + +typedef struct { + char* id; + char* amount; + int64_t staked_at; + int64_t unlock_at; + synor_stake_status_t status; + char* rewards_earned; + double apy; +} synor_stake_t; + +typedef struct { + synor_stake_t* stakes; + size_t count; +} synor_stake_list_t; + +typedef struct { + char* id; + char* amount; + char* tx_hash; + int64_t staked_at; + int64_t unlock_at; + double apy; +} synor_stake_receipt_t; + +typedef struct { + char* id; + char* amount; + char* tx_hash; + int64_t unstaked_at; + int64_t available_at; +} synor_unstake_receipt_t; + +typedef struct { + char* pending; + char* claimed; + char* total; + double current_apy; + int64_t last_claim; + int64_t next_distribution; +} synor_staking_rewards_t; + +/* ==================== Discount Types ==================== */ + +typedef struct { + char* code; + synor_discount_type_t type; + char* value; + char* description; + synor_service_type_t* applicable_services; + size_t applicable_services_count; + int64_t valid_from; + int64_t valid_until; + int32_t max_uses; + int32_t current_uses; + bool active; +} synor_discount_t; + +typedef struct { + synor_discount_t* discounts; + size_t count; +} synor_discount_list_t; + +typedef struct { + synor_discount_t discount; + char* savings_estimate; + int64_t applied_at; +} synor_discount_result_t; + +/* ==================== Client Handle ==================== */ + +typedef struct synor_economics synor_economics_t; + +/* ==================== Lifecycle Functions ==================== */ + +synor_economics_t* synor_economics_create(const synor_economics_config_t* config); +void synor_economics_destroy(synor_economics_t* economics); +bool synor_economics_is_closed(synor_economics_t* economics); +bool synor_economics_health_check(synor_economics_t* economics); + +/* ==================== Pricing Operations ==================== */ + +synor_economics_error_t synor_economics_get_pricing(synor_economics_t* economics, + synor_service_type_t* service, synor_service_pricing_list_t* result); +synor_economics_error_t synor_economics_get_price(synor_economics_t* economics, + synor_service_type_t service, const synor_usage_metrics_t* usage, + synor_price_result_t* result); +synor_economics_error_t synor_economics_estimate_cost(synor_economics_t* economics, + const synor_usage_plan_t* plan, synor_cost_estimate_t* result); + +void synor_service_pricing_free(synor_service_pricing_t* pricing); +void synor_service_pricing_list_free(synor_service_pricing_list_t* list); +void synor_price_result_free(synor_price_result_t* result); +void synor_cost_estimate_free(synor_cost_estimate_t* estimate); + +/* ==================== Billing Operations ==================== */ + +synor_economics_error_t synor_economics_get_usage(synor_economics_t* economics, + synor_billing_period_t* period, synor_usage_t* result); +synor_economics_error_t synor_economics_get_invoices(synor_economics_t* economics, + synor_invoice_list_t* result); +synor_economics_error_t synor_economics_get_invoice(synor_economics_t* economics, + const char* invoice_id, synor_invoice_t* result); +synor_economics_error_t synor_economics_get_balance(synor_economics_t* economics, + synor_account_balance_t* result); + +void synor_usage_free(synor_usage_t* usage); +void synor_invoice_free(synor_invoice_t* invoice); +void synor_invoice_list_free(synor_invoice_list_t* list); +void synor_account_balance_free(synor_account_balance_t* balance); + +/* ==================== Staking Operations ==================== */ + +synor_economics_error_t synor_economics_stake(synor_economics_t* economics, + const char* amount, int32_t duration_days, synor_stake_receipt_t* result); +synor_economics_error_t synor_economics_unstake(synor_economics_t* economics, + const char* stake_id, synor_unstake_receipt_t* result); +synor_economics_error_t synor_economics_get_stakes(synor_economics_t* economics, + synor_stake_list_t* result); +synor_economics_error_t synor_economics_get_stake(synor_economics_t* economics, + const char* stake_id, synor_stake_t* result); +synor_economics_error_t synor_economics_get_staking_rewards(synor_economics_t* economics, + synor_staking_rewards_t* result); +synor_economics_error_t synor_economics_claim_rewards(synor_economics_t* economics, + synor_stake_receipt_t* result); + +void synor_stake_free(synor_stake_t* stake); +void synor_stake_list_free(synor_stake_list_t* list); +void synor_stake_receipt_free(synor_stake_receipt_t* receipt); +void synor_unstake_receipt_free(synor_unstake_receipt_t* receipt); +void synor_staking_rewards_free(synor_staking_rewards_t* rewards); + +/* ==================== Discount Operations ==================== */ + +synor_economics_error_t synor_economics_apply_discount(synor_economics_t* economics, + const char* code, synor_discount_result_t* result); +synor_economics_error_t synor_economics_remove_discount(synor_economics_t* economics, + const char* code); +synor_economics_error_t synor_economics_get_discounts(synor_economics_t* economics, + synor_discount_list_t* result); + +void synor_discount_free(synor_discount_t* discount); +void synor_discount_list_free(synor_discount_list_t* list); +void synor_discount_result_free(synor_discount_result_t* result); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_ECONOMICS_H */ diff --git a/sdk/c/include/synor/governance.h b/sdk/c/include/synor/governance.h new file mode 100644 index 0000000..4eea6e0 --- /dev/null +++ b/sdk/c/include/synor/governance.h @@ -0,0 +1,364 @@ +/** + * @file governance.h + * @brief Synor Governance SDK for C + * + * Proposals, voting, DAOs, and vesting operations. + */ + +#ifndef SYNOR_GOVERNANCE_H +#define SYNOR_GOVERNANCE_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ==================== Error Types ==================== */ + +typedef enum { + SYNOR_GOVERNANCE_OK = 0, + SYNOR_GOVERNANCE_ERROR_CLIENT_CLOSED, + SYNOR_GOVERNANCE_ERROR_NETWORK, + SYNOR_GOVERNANCE_ERROR_HTTP, + SYNOR_GOVERNANCE_ERROR_ENCODING, + SYNOR_GOVERNANCE_ERROR_DECODING, + SYNOR_GOVERNANCE_ERROR_INVALID_RESPONSE, + SYNOR_GOVERNANCE_ERROR_MEMORY, + SYNOR_GOVERNANCE_ERROR_TIMEOUT, + SYNOR_GOVERNANCE_ERROR_UNKNOWN +} synor_governance_error_t; + +/* ==================== Configuration ==================== */ + +typedef struct { + const char* api_key; + const char* endpoint; /* Default: https://governance.synor.io/v1 */ + uint32_t timeout_ms; /* Default: 30000 */ + uint32_t retries; /* Default: 3 */ + bool debug; +} synor_governance_config_t; + +/* ==================== Enums ==================== */ + +typedef enum { + SYNOR_PROPOSAL_PENDING, + SYNOR_PROPOSAL_ACTIVE, + SYNOR_PROPOSAL_PASSED, + SYNOR_PROPOSAL_REJECTED, + SYNOR_PROPOSAL_EXECUTED, + SYNOR_PROPOSAL_CANCELLED +} synor_proposal_status_t; + +typedef enum { + SYNOR_VOTE_FOR, + SYNOR_VOTE_AGAINST, + SYNOR_VOTE_ABSTAIN +} synor_vote_choice_t; + +typedef enum { + SYNOR_DAO_TOKEN, + SYNOR_DAO_MULTISIG, + SYNOR_DAO_HYBRID +} synor_dao_type_t; + +typedef enum { + SYNOR_VESTING_ACTIVE, + SYNOR_VESTING_COMPLETED, + SYNOR_VESTING_REVOKED +} synor_vesting_status_t; + +/* ==================== Proposal Types ==================== */ + +typedef struct { + char* target; + char* method; + char* data; + char* value; +} synor_proposal_action_t; + +typedef struct { + char* title; + char* description; + char* discussion_url; + int64_t voting_start_time; + int64_t voting_end_time; + char* dao_id; + synor_proposal_action_t* actions; + size_t actions_count; +} synor_proposal_draft_t; + +typedef struct { + char* for_votes; + char* against_votes; + char* abstain_votes; + char* quorum; + char* quorum_required; + int32_t total_voters; +} synor_vote_tally_t; + +typedef struct { + char* id; + char* title; + char* description; + char* discussion_url; + char* proposer; + synor_proposal_status_t status; + int64_t created_at; + int64_t voting_start_time; + int64_t voting_end_time; + int64_t execution_time; + synor_vote_tally_t vote_tally; + synor_proposal_action_t* actions; + size_t actions_count; + char* dao_id; +} synor_proposal_t; + +typedef struct { + synor_proposal_t* proposals; + size_t count; +} synor_proposal_list_t; + +typedef struct { + synor_proposal_status_t* status; /* NULL for all */ + char* proposer; /* NULL for all */ + char* dao_id; /* NULL for all */ + int32_t limit; + int32_t offset; +} synor_proposal_filter_t; + +/* ==================== Voting Types ==================== */ + +typedef struct { + synor_vote_choice_t choice; + char* reason; +} synor_vote_t; + +typedef struct { + char* id; + char* proposal_id; + char* voter; + synor_vote_choice_t choice; + char* weight; + char* reason; + int64_t voted_at; + char* tx_hash; +} synor_vote_receipt_t; + +typedef struct { + synor_vote_receipt_t* votes; + size_t count; +} synor_vote_receipt_list_t; + +typedef struct { + char* address; + char* delegated_power; + char* own_power; + char* total_power; + char** delegators; + size_t delegators_count; +} synor_voting_power_t; + +typedef struct { + char* id; + char* from; + char* to; + char* amount; + int64_t delegated_at; + char* tx_hash; +} synor_delegation_receipt_t; + +typedef struct { + synor_delegation_receipt_t* delegations; + size_t count; +} synor_delegation_receipt_list_t; + +/* ==================== DAO Types ==================== */ + +typedef struct { + char* name; + char* description; + synor_dao_type_t type; + int32_t voting_period_days; + int32_t timelock_days; + char* token_address; + double quorum_percent; + char* proposal_threshold; + char** multisig_members; + size_t multisig_members_count; + int32_t multisig_threshold; +} synor_dao_config_t; + +typedef struct { + char* id; + char* name; + char* description; + synor_dao_type_t type; + char* token_address; + int32_t voting_period_days; + int32_t timelock_days; + double quorum_percent; + char* proposal_threshold; + int32_t total_proposals; + int32_t active_proposals; + char* treasury_value; + int32_t member_count; + int64_t created_at; +} synor_dao_t; + +typedef struct { + synor_dao_t* daos; + size_t count; +} synor_dao_list_t; + +typedef struct { + char* address; + char* balance; + char* name; + char* symbol; +} synor_treasury_token_t; + +typedef struct { + char* dao_id; + char* total_value; + synor_treasury_token_t* tokens; + size_t tokens_count; + int64_t last_updated; +} synor_dao_treasury_t; + +/* ==================== Vesting Types ==================== */ + +typedef struct { + char* beneficiary; + char* total_amount; + int64_t start_time; + int64_t cliff_duration; + int64_t vesting_duration; + bool revocable; +} synor_vesting_schedule_t; + +typedef struct { + char* id; + char* beneficiary; + char* grantor; + char* total_amount; + char* released_amount; + char* releasable_amount; + int64_t start_time; + int64_t cliff_time; + int64_t end_time; + synor_vesting_status_t status; + bool revocable; + int64_t created_at; +} synor_vesting_contract_t; + +typedef struct { + synor_vesting_contract_t* contracts; + size_t count; +} synor_vesting_contract_list_t; + +typedef struct { + char* id; + char* contract_id; + char* amount; + char* tx_hash; + int64_t claimed_at; +} synor_claim_receipt_t; + +/* ==================== Client Handle ==================== */ + +typedef struct synor_governance synor_governance_t; + +/* ==================== Lifecycle Functions ==================== */ + +synor_governance_t* synor_governance_create(const synor_governance_config_t* config); +void synor_governance_destroy(synor_governance_t* governance); +bool synor_governance_is_closed(synor_governance_t* governance); +bool synor_governance_health_check(synor_governance_t* governance); + +/* ==================== Proposal Operations ==================== */ + +synor_governance_error_t synor_governance_create_proposal(synor_governance_t* governance, + const synor_proposal_draft_t* draft, synor_proposal_t* result); +synor_governance_error_t synor_governance_get_proposal(synor_governance_t* governance, + const char* proposal_id, synor_proposal_t* result); +synor_governance_error_t synor_governance_list_proposals(synor_governance_t* governance, + const synor_proposal_filter_t* filter, synor_proposal_list_t* result); +synor_governance_error_t synor_governance_cancel_proposal(synor_governance_t* governance, + const char* proposal_id, synor_proposal_t* result); +synor_governance_error_t synor_governance_execute_proposal(synor_governance_t* governance, + const char* proposal_id, synor_proposal_t* result); +synor_governance_error_t synor_governance_wait_for_proposal(synor_governance_t* governance, + const char* proposal_id, uint32_t poll_interval_ms, uint32_t max_wait_ms, + synor_proposal_t* result); + +void synor_proposal_free(synor_proposal_t* proposal); +void synor_proposal_list_free(synor_proposal_list_t* list); + +/* ==================== Voting Operations ==================== */ + +synor_governance_error_t synor_governance_vote(synor_governance_t* governance, + const char* proposal_id, const synor_vote_t* vote, const char* weight, + synor_vote_receipt_t* result); +synor_governance_error_t synor_governance_get_votes(synor_governance_t* governance, + const char* proposal_id, synor_vote_receipt_list_t* result); +synor_governance_error_t synor_governance_get_my_vote(synor_governance_t* governance, + const char* proposal_id, synor_vote_receipt_t* result); +synor_governance_error_t synor_governance_delegate(synor_governance_t* governance, + const char* delegatee, const char* amount, synor_delegation_receipt_t* result); +synor_governance_error_t synor_governance_undelegate(synor_governance_t* governance, + const char* delegatee, synor_delegation_receipt_t* result); +synor_governance_error_t synor_governance_get_voting_power(synor_governance_t* governance, + const char* address, synor_voting_power_t* result); +synor_governance_error_t synor_governance_get_delegations(synor_governance_t* governance, + const char* address, synor_delegation_receipt_list_t* result); + +void synor_vote_receipt_free(synor_vote_receipt_t* receipt); +void synor_vote_receipt_list_free(synor_vote_receipt_list_t* list); +void synor_voting_power_free(synor_voting_power_t* power); +void synor_delegation_receipt_free(synor_delegation_receipt_t* receipt); +void synor_delegation_receipt_list_free(synor_delegation_receipt_list_t* list); + +/* ==================== DAO Operations ==================== */ + +synor_governance_error_t synor_governance_create_dao(synor_governance_t* governance, + const synor_dao_config_t* config, synor_dao_t* result); +synor_governance_error_t synor_governance_get_dao(synor_governance_t* governance, + const char* dao_id, synor_dao_t* result); +synor_governance_error_t synor_governance_list_daos(synor_governance_t* governance, + int32_t limit, int32_t offset, synor_dao_list_t* result); +synor_governance_error_t synor_governance_get_dao_treasury(synor_governance_t* governance, + const char* dao_id, synor_dao_treasury_t* result); +synor_governance_error_t synor_governance_get_dao_members(synor_governance_t* governance, + const char* dao_id, char*** members, size_t* count); + +void synor_dao_free(synor_dao_t* dao); +void synor_dao_list_free(synor_dao_list_t* list); +void synor_dao_treasury_free(synor_dao_treasury_t* treasury); + +/* ==================== Vesting Operations ==================== */ + +synor_governance_error_t synor_governance_create_vesting(synor_governance_t* governance, + const synor_vesting_schedule_t* schedule, synor_vesting_contract_t* result); +synor_governance_error_t synor_governance_get_vesting(synor_governance_t* governance, + const char* contract_id, synor_vesting_contract_t* result); +synor_governance_error_t synor_governance_list_vesting(synor_governance_t* governance, + const char* beneficiary, synor_vesting_contract_list_t* result); +synor_governance_error_t synor_governance_claim_vested(synor_governance_t* governance, + const char* contract_id, synor_claim_receipt_t* result); +synor_governance_error_t synor_governance_revoke_vesting(synor_governance_t* governance, + const char* contract_id, synor_vesting_contract_t* result); +synor_governance_error_t synor_governance_get_releasable(synor_governance_t* governance, + const char* contract_id, char** amount); + +void synor_vesting_contract_free(synor_vesting_contract_t* contract); +void synor_vesting_contract_list_free(synor_vesting_contract_list_t* list); +void synor_claim_receipt_free(synor_claim_receipt_t* receipt); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_GOVERNANCE_H */ diff --git a/sdk/c/include/synor/mining.h b/sdk/c/include/synor/mining.h new file mode 100644 index 0000000..ae871a2 --- /dev/null +++ b/sdk/c/include/synor/mining.h @@ -0,0 +1,394 @@ +/** + * @file mining.h + * @brief Synor Mining SDK for C + * + * Pool connections, block templates, hashrate stats, and GPU management. + */ + +#ifndef SYNOR_MINING_H +#define SYNOR_MINING_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ==================== Error Types ==================== */ + +typedef enum { + SYNOR_MINING_OK = 0, + SYNOR_MINING_ERROR_CLIENT_CLOSED, + SYNOR_MINING_ERROR_NETWORK, + SYNOR_MINING_ERROR_HTTP, + SYNOR_MINING_ERROR_ENCODING, + SYNOR_MINING_ERROR_DECODING, + SYNOR_MINING_ERROR_INVALID_RESPONSE, + SYNOR_MINING_ERROR_MEMORY, + SYNOR_MINING_ERROR_TIMEOUT, + SYNOR_MINING_ERROR_NOT_CONNECTED, + SYNOR_MINING_ERROR_UNKNOWN +} synor_mining_error_t; + +/* ==================== Configuration ==================== */ + +typedef struct { + const char* api_key; + const char* endpoint; /* Default: https://mining.synor.io/v1 */ + uint32_t timeout_ms; /* Default: 60000 */ + uint32_t retries; /* Default: 3 */ + bool debug; +} synor_mining_config_t; + +/* ==================== Enums ==================== */ + +typedef enum { + SYNOR_DEVICE_CPU, + SYNOR_DEVICE_GPU_NVIDIA, + SYNOR_DEVICE_GPU_AMD, + SYNOR_DEVICE_ASIC +} synor_device_type_t; + +typedef enum { + SYNOR_DEVICE_IDLE, + SYNOR_DEVICE_MINING, + SYNOR_DEVICE_ERROR, + SYNOR_DEVICE_OFFLINE +} synor_device_status_t; + +typedef enum { + SYNOR_CONN_DISCONNECTED, + SYNOR_CONN_CONNECTING, + SYNOR_CONN_CONNECTED, + SYNOR_CONN_RECONNECTING +} synor_connection_status_t; + +typedef enum { + SYNOR_PERIOD_HOUR, + SYNOR_PERIOD_DAY, + SYNOR_PERIOD_WEEK, + SYNOR_PERIOD_MONTH, + SYNOR_PERIOD_ALL +} synor_time_period_t; + +typedef enum { + SYNOR_SUBMIT_ACCEPTED, + SYNOR_SUBMIT_REJECTED, + SYNOR_SUBMIT_STALE +} synor_submit_status_t; + +/* ==================== Pool Types ==================== */ + +typedef struct { + const char* url; + const char* user; + const char* password; + const char* algorithm; + double difficulty; +} synor_pool_config_t; + +typedef struct { + char* id; + char* pool; + synor_connection_status_t status; + char* algorithm; + double difficulty; + int64_t connected_at; + int32_t accepted_shares; + int32_t rejected_shares; + int32_t stale_shares; + int64_t last_share_at; +} synor_stratum_connection_t; + +typedef struct { + char* url; + int32_t workers; + double hashrate; + double difficulty; + int64_t last_block; + int32_t blocks_found_24h; + double luck; +} synor_pool_stats_t; + +/* ==================== Block Template Types ==================== */ + +typedef struct { + char* txid; + char* data; + char* fee; + int32_t weight; +} synor_template_transaction_t; + +typedef struct { + char* id; + char* previous_block_hash; + char* merkle_root; + int64_t timestamp; + char* bits; + int64_t height; + char* coinbase_value; + synor_template_transaction_t* transactions; + size_t transactions_count; + char* target; + char* algorithm; + char* extra_nonce; +} synor_block_template_t; + +typedef struct { + char* template_id; + char* nonce; + char* extra_nonce; + int64_t timestamp; + char* hash; +} synor_mined_work_t; + +typedef struct { + char* hash; + double difficulty; + int64_t timestamp; + bool accepted; +} synor_share_info_t; + +typedef struct { + synor_submit_status_t status; + synor_share_info_t share; + bool block_found; + char* reason; + char* block_hash; + char* reward; +} synor_submit_result_t; + +/* ==================== Stats Types ==================== */ + +typedef struct { + double current; + double average_1h; + double average_24h; + double peak; + char* unit; +} synor_hashrate_t; + +typedef struct { + int32_t accepted; + int32_t rejected; + int32_t stale; + int32_t total; + double accept_rate; +} synor_share_stats_t; + +typedef struct { + double current; + double max; + bool throttling; +} synor_device_temperature_t; + +typedef struct { + char* today; + char* yesterday; + char* this_week; + char* this_month; + char* total; + char* currency; +} synor_earnings_snapshot_t; + +typedef struct { + synor_hashrate_t hashrate; + synor_share_stats_t shares; + int64_t uptime; + double efficiency; + synor_earnings_snapshot_t earnings; + double power_consumption; + synor_device_temperature_t temperature; +} synor_mining_stats_t; + +typedef struct { + int64_t date; + char* amount; + int32_t blocks; + int32_t shares; + double hashrate; +} synor_earnings_breakdown_t; + +typedef struct { + synor_time_period_t period; + int64_t start_date; + int64_t end_date; + char* amount; + int32_t blocks; + int32_t shares; + double average_hashrate; + char* currency; + synor_earnings_breakdown_t* breakdown; + size_t breakdown_count; +} synor_earnings_t; + +/* ==================== Device Types ==================== */ + +typedef struct { + char* id; + char* name; + synor_device_type_t type; + synor_device_status_t status; + double hashrate; + double temperature; + double fan_speed; + double power_draw; + int64_t memory_used; + int64_t memory_total; + char* driver; + char* firmware; +} synor_mining_device_t; + +typedef struct { + synor_mining_device_t* devices; + size_t count; +} synor_mining_device_list_t; + +typedef struct { + bool enabled; + int32_t intensity; + int32_t power_limit; + int32_t core_clock_offset; + int32_t memory_clock_offset; + int32_t fan_speed; +} synor_device_config_t; + +/* ==================== Worker Types ==================== */ + +typedef struct { + char* id; + char* name; + synor_connection_status_t status; + synor_hashrate_t hashrate; + synor_share_stats_t shares; + synor_mining_device_t* devices; + size_t devices_count; + int64_t last_seen; + int64_t uptime; +} synor_worker_info_t; + +typedef struct { + synor_worker_info_t* workers; + size_t count; +} synor_worker_info_list_t; + +/* ==================== Algorithm Types ==================== */ + +typedef struct { + char* name; + char* display_name; + char* hash_unit; + char* profitability; + double difficulty; + char* block_reward; + int32_t block_time; +} synor_mining_algorithm_t; + +typedef struct { + synor_mining_algorithm_t* algorithms; + size_t count; +} synor_mining_algorithm_list_t; + +/* ==================== Client Handle ==================== */ + +typedef struct synor_mining synor_mining_t; + +/* ==================== Lifecycle Functions ==================== */ + +synor_mining_t* synor_mining_create(const synor_mining_config_t* config); +void synor_mining_destroy(synor_mining_t* mining); +bool synor_mining_is_closed(synor_mining_t* mining); +bool synor_mining_health_check(synor_mining_t* mining); + +/* ==================== Pool Operations ==================== */ + +synor_mining_error_t synor_mining_connect(synor_mining_t* mining, + const synor_pool_config_t* pool, synor_stratum_connection_t* result); +synor_mining_error_t synor_mining_disconnect(synor_mining_t* mining); +synor_mining_error_t synor_mining_get_connection(synor_mining_t* mining, + synor_stratum_connection_t* result); +synor_mining_error_t synor_mining_get_pool_stats(synor_mining_t* mining, + synor_pool_stats_t* result); +bool synor_mining_is_connected(synor_mining_t* mining); + +void synor_stratum_connection_free(synor_stratum_connection_t* conn); +void synor_pool_stats_free(synor_pool_stats_t* stats); + +/* ==================== Mining Operations ==================== */ + +synor_mining_error_t synor_mining_get_block_template(synor_mining_t* mining, + synor_block_template_t* result); +synor_mining_error_t synor_mining_submit_work(synor_mining_t* mining, + const synor_mined_work_t* work, synor_submit_result_t* result); +synor_mining_error_t synor_mining_start(synor_mining_t* mining, const char* algorithm); +synor_mining_error_t synor_mining_stop(synor_mining_t* mining); +synor_mining_error_t synor_mining_is_mining(synor_mining_t* mining, bool* result); + +void synor_block_template_free(synor_block_template_t* tmpl); +void synor_submit_result_free(synor_submit_result_t* result); + +/* ==================== Stats Operations ==================== */ + +synor_mining_error_t synor_mining_get_hashrate(synor_mining_t* mining, + synor_hashrate_t* result); +synor_mining_error_t synor_mining_get_stats(synor_mining_t* mining, + synor_mining_stats_t* result); +synor_mining_error_t synor_mining_get_earnings(synor_mining_t* mining, + synor_time_period_t* period, synor_earnings_t* result); +synor_mining_error_t synor_mining_get_share_stats(synor_mining_t* mining, + synor_share_stats_t* result); + +void synor_hashrate_free(synor_hashrate_t* hashrate); +void synor_mining_stats_free(synor_mining_stats_t* stats); +void synor_earnings_free(synor_earnings_t* earnings); + +/* ==================== Device Operations ==================== */ + +synor_mining_error_t synor_mining_list_devices(synor_mining_t* mining, + synor_mining_device_list_t* result); +synor_mining_error_t synor_mining_get_device(synor_mining_t* mining, + const char* device_id, synor_mining_device_t* result); +synor_mining_error_t synor_mining_set_device_config(synor_mining_t* mining, + const char* device_id, const synor_device_config_t* config, + synor_mining_device_t* result); +synor_mining_error_t synor_mining_enable_device(synor_mining_t* mining, + const char* device_id); +synor_mining_error_t synor_mining_disable_device(synor_mining_t* mining, + const char* device_id); + +void synor_mining_device_free(synor_mining_device_t* device); +void synor_mining_device_list_free(synor_mining_device_list_t* list); + +/* ==================== Worker Operations ==================== */ + +synor_mining_error_t synor_mining_list_workers(synor_mining_t* mining, + synor_worker_info_list_t* result); +synor_mining_error_t synor_mining_get_worker(synor_mining_t* mining, + const char* worker_id, synor_worker_info_t* result); +synor_mining_error_t synor_mining_remove_worker(synor_mining_t* mining, + const char* worker_id); + +void synor_worker_info_free(synor_worker_info_t* worker); +void synor_worker_info_list_free(synor_worker_info_list_t* list); + +/* ==================== Algorithm Operations ==================== */ + +synor_mining_error_t synor_mining_list_algorithms(synor_mining_t* mining, + synor_mining_algorithm_list_t* result); +synor_mining_error_t synor_mining_get_algorithm(synor_mining_t* mining, + const char* name, synor_mining_algorithm_t* result); +synor_mining_error_t synor_mining_switch_algorithm(synor_mining_t* mining, + const char* algorithm); +synor_mining_error_t synor_mining_get_most_profitable(synor_mining_t* mining, + synor_mining_algorithm_t* result); + +void synor_mining_algorithm_free(synor_mining_algorithm_t* algorithm); +void synor_mining_algorithm_list_free(synor_mining_algorithm_list_t* list); + +#ifdef __cplusplus +} +#endif + +#endif /* SYNOR_MINING_H */ diff --git a/sdk/c/src/economics/economics.c b/sdk/c/src/economics/economics.c new file mode 100644 index 0000000..6c7abe3 --- /dev/null +++ b/sdk/c/src/economics/economics.c @@ -0,0 +1,424 @@ +/** + * @file economics.c + * @brief Synor Economics SDK implementation + */ + +#include +#include +#include +#include "synor/economics.h" + +/* Forward declarations from common.c */ +typedef struct synor_http_client synor_http_client_t; +synor_http_client_t* synor_http_client_create( + const char* api_key, const char* endpoint, + uint32_t timeout_ms, uint32_t retries, bool debug); +void synor_http_client_destroy(synor_http_client_t* client); +int synor_http_get(synor_http_client_t* client, const char* path, + char** response, size_t* response_size); +int synor_http_post(synor_http_client_t* client, const char* path, + const char* body, char** response, size_t* response_size); +char* synor_strdup(const char* s); +char* synor_url_encode(const char* s); + +/* Internal economics structure */ +struct synor_economics { + synor_http_client_t* http; + bool closed; + bool debug; +}; + +static const char* service_type_to_string(synor_service_type_t type) { + switch (type) { + case SYNOR_SERVICE_COMPUTE: return "compute"; + case SYNOR_SERVICE_STORAGE: return "storage"; + case SYNOR_SERVICE_DATABASE: return "database"; + case SYNOR_SERVICE_HOSTING: return "hosting"; + case SYNOR_SERVICE_RPC: return "rpc"; + case SYNOR_SERVICE_BRIDGE: return "bridge"; + default: return "unknown"; + } +} + +static const char* billing_period_to_string(synor_billing_period_t period) { + switch (period) { + case SYNOR_BILLING_HOURLY: return "hourly"; + case SYNOR_BILLING_DAILY: return "daily"; + case SYNOR_BILLING_WEEKLY: return "weekly"; + case SYNOR_BILLING_MONTHLY: return "monthly"; + default: return "monthly"; + } +} + +synor_economics_t* synor_economics_create(const synor_economics_config_t* config) { + if (!config || !config->api_key) { + return NULL; + } + + synor_economics_t* economics = calloc(1, sizeof(synor_economics_t)); + if (!economics) return NULL; + + const char* endpoint = config->endpoint ? + config->endpoint : "https://economics.synor.io/v1"; + uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 30000; + uint32_t retries = config->retries > 0 ? config->retries : 3; + + economics->http = synor_http_client_create( + config->api_key, endpoint, timeout, retries, config->debug); + + if (!economics->http) { + free(economics); + return NULL; + } + + economics->closed = false; + economics->debug = config->debug; + return economics; +} + +void synor_economics_destroy(synor_economics_t* economics) { + if (!economics) return; + economics->closed = true; + synor_http_client_destroy(economics->http); + free(economics); +} + +bool synor_economics_is_closed(synor_economics_t* economics) { + return economics ? economics->closed : true; +} + +bool synor_economics_health_check(synor_economics_t* economics) { + if (!economics || economics->closed) return false; + + char* response = NULL; + size_t response_size = 0; + + if (synor_http_get(economics->http, "/health", &response, &response_size) != 0) { + return false; + } + + bool healthy = response && strstr(response, "\"healthy\"") != NULL; + free(response); + return healthy; +} + +/* ==================== Pricing Operations ==================== */ + +synor_economics_error_t synor_economics_get_pricing(synor_economics_t* economics, + synor_service_type_t* service, synor_service_pricing_list_t* result) { + if (!economics || economics->closed) return SYNOR_ECONOMICS_ERROR_CLIENT_CLOSED; + (void)service; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_price(synor_economics_t* economics, + synor_service_type_t service, const synor_usage_metrics_t* usage, + synor_price_result_t* result) { + if (!economics || economics->closed) return SYNOR_ECONOMICS_ERROR_CLIENT_CLOSED; + if (!usage || !result) return SYNOR_ECONOMICS_ERROR_UNKNOWN; + + char body[1024]; + snprintf(body, sizeof(body), + "{\"service\":\"%s\",\"computeUnits\":%.2f,\"storageBytes\":%.2f,\"bandwidthBytes\":%.2f,\"durationSeconds\":%lld,\"requests\":%d}", + service_type_to_string(service), + usage->compute_units, usage->storage_bytes, usage->bandwidth_bytes, + (long long)usage->duration_seconds, usage->requests); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(economics->http, "/pricing/calculate", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_ECONOMICS_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_estimate_cost(synor_economics_t* economics, + const synor_usage_plan_t* plan, synor_cost_estimate_t* result) { + (void)economics; (void)plan; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +void synor_service_pricing_free(synor_service_pricing_t* pricing) { + if (!pricing) return; + free(pricing->unit); + free(pricing->price_per_unit); + free(pricing->currency); +} + +void synor_service_pricing_list_free(synor_service_pricing_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_service_pricing_free(&list->prices[i]); + } + free(list->prices); + list->prices = NULL; + list->count = 0; +} + +void synor_price_result_free(synor_price_result_t* result) { + if (!result) return; + free(result->amount); + free(result->currency); +} + +void synor_cost_estimate_free(synor_cost_estimate_t* estimate) { + if (!estimate) return; + free(estimate->total); + free(estimate->currency); + for (size_t i = 0; i < estimate->breakdown_count; i++) { + free(estimate->breakdown[i].amount); + } + free(estimate->breakdown); + free(estimate->discount_applied); +} + +/* ==================== Billing Operations ==================== */ + +synor_economics_error_t synor_economics_get_usage(synor_economics_t* economics, + synor_billing_period_t* period, synor_usage_t* result) { + if (!economics || economics->closed) return SYNOR_ECONOMICS_ERROR_CLIENT_CLOSED; + + char path[256]; + if (period) { + snprintf(path, sizeof(path), "/billing/usage?period=%s", billing_period_to_string(*period)); + } else { + snprintf(path, sizeof(path), "/billing/usage"); + } + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_get(economics->http, path, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_ECONOMICS_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + (void)result; + free(response); + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_invoices(synor_economics_t* economics, + synor_invoice_list_t* result) { + (void)economics; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_invoice(synor_economics_t* economics, + const char* invoice_id, synor_invoice_t* result) { + (void)economics; (void)invoice_id; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_balance(synor_economics_t* economics, + synor_account_balance_t* result) { + (void)economics; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +void synor_usage_free(synor_usage_t* usage) { + if (!usage) return; + for (size_t i = 0; i < usage->items_count; i++) { + free(usage->items[i].cost); + } + free(usage->items); + free(usage->total_cost); + free(usage->currency); +} + +void synor_invoice_free(synor_invoice_t* invoice) { + if (!invoice) return; + free(invoice->id); + for (size_t i = 0; i < invoice->lines_count; i++) { + free(invoice->lines[i].service); + free(invoice->lines[i].description); + free(invoice->lines[i].unit_price); + free(invoice->lines[i].amount); + } + free(invoice->lines); + free(invoice->subtotal); + free(invoice->discount); + free(invoice->tax); + free(invoice->total); + free(invoice->currency); + free(invoice->pdf_url); +} + +void synor_invoice_list_free(synor_invoice_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_invoice_free(&list->invoices[i]); + } + free(list->invoices); + list->invoices = NULL; + list->count = 0; +} + +void synor_account_balance_free(synor_account_balance_t* balance) { + if (!balance) return; + free(balance->available); + free(balance->pending); + free(balance->staked); + free(balance->total); + free(balance->currency); +} + +/* ==================== Staking Operations ==================== */ + +synor_economics_error_t synor_economics_stake(synor_economics_t* economics, + const char* amount, int32_t duration_days, synor_stake_receipt_t* result) { + if (!economics || economics->closed) return SYNOR_ECONOMICS_ERROR_CLIENT_CLOSED; + if (!amount || !result) return SYNOR_ECONOMICS_ERROR_UNKNOWN; + + char body[256]; + snprintf(body, sizeof(body), "{\"amount\":\"%s\",\"durationDays\":%d}", amount, duration_days); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(economics->http, "/staking/stake", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_ECONOMICS_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_unstake(synor_economics_t* economics, + const char* stake_id, synor_unstake_receipt_t* result) { + (void)economics; (void)stake_id; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_stakes(synor_economics_t* economics, + synor_stake_list_t* result) { + (void)economics; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_stake(synor_economics_t* economics, + const char* stake_id, synor_stake_t* result) { + (void)economics; (void)stake_id; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_staking_rewards(synor_economics_t* economics, + synor_staking_rewards_t* result) { + (void)economics; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_claim_rewards(synor_economics_t* economics, + synor_stake_receipt_t* result) { + (void)economics; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +void synor_stake_free(synor_stake_t* stake) { + if (!stake) return; + free(stake->id); + free(stake->amount); + free(stake->rewards_earned); +} + +void synor_stake_list_free(synor_stake_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_stake_free(&list->stakes[i]); + } + free(list->stakes); + list->stakes = NULL; + list->count = 0; +} + +void synor_stake_receipt_free(synor_stake_receipt_t* receipt) { + if (!receipt) return; + free(receipt->id); + free(receipt->amount); + free(receipt->tx_hash); +} + +void synor_unstake_receipt_free(synor_unstake_receipt_t* receipt) { + if (!receipt) return; + free(receipt->id); + free(receipt->amount); + free(receipt->tx_hash); +} + +void synor_staking_rewards_free(synor_staking_rewards_t* rewards) { + if (!rewards) return; + free(rewards->pending); + free(rewards->claimed); + free(rewards->total); +} + +/* ==================== Discount Operations ==================== */ + +synor_economics_error_t synor_economics_apply_discount(synor_economics_t* economics, + const char* code, synor_discount_result_t* result) { + if (!economics || economics->closed) return SYNOR_ECONOMICS_ERROR_CLIENT_CLOSED; + if (!code || !result) return SYNOR_ECONOMICS_ERROR_UNKNOWN; + + char body[256]; + snprintf(body, sizeof(body), "{\"code\":\"%s\"}", code); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(economics->http, "/discounts/apply", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_ECONOMICS_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_remove_discount(synor_economics_t* economics, + const char* code) { + (void)economics; (void)code; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +synor_economics_error_t synor_economics_get_discounts(synor_economics_t* economics, + synor_discount_list_t* result) { + (void)economics; (void)result; + return SYNOR_ECONOMICS_ERROR_UNKNOWN; +} + +void synor_discount_free(synor_discount_t* discount) { + if (!discount) return; + free(discount->code); + free(discount->value); + free(discount->description); + free(discount->applicable_services); +} + +void synor_discount_list_free(synor_discount_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_discount_free(&list->discounts[i]); + } + free(list->discounts); + list->discounts = NULL; + list->count = 0; +} + +void synor_discount_result_free(synor_discount_result_t* result) { + if (!result) return; + synor_discount_free(&result->discount); + free(result->savings_estimate); +} diff --git a/sdk/c/src/governance/governance.c b/sdk/c/src/governance/governance.c new file mode 100644 index 0000000..4f2b6fe --- /dev/null +++ b/sdk/c/src/governance/governance.c @@ -0,0 +1,524 @@ +/** + * @file governance.c + * @brief Synor Governance SDK implementation + */ + +#include +#include +#include +#include "synor/governance.h" + +/* Forward declarations from common.c */ +typedef struct synor_http_client synor_http_client_t; +synor_http_client_t* synor_http_client_create( + const char* api_key, const char* endpoint, + uint32_t timeout_ms, uint32_t retries, bool debug); +void synor_http_client_destroy(synor_http_client_t* client); +int synor_http_get(synor_http_client_t* client, const char* path, + char** response, size_t* response_size); +int synor_http_post(synor_http_client_t* client, const char* path, + const char* body, char** response, size_t* response_size); +char* synor_strdup(const char* s); +char* synor_url_encode(const char* s); + +/* Internal governance structure */ +struct synor_governance { + synor_http_client_t* http; + bool closed; + bool debug; +}; + +static const synor_proposal_status_t FINAL_STATUSES[] = { + SYNOR_PROPOSAL_PASSED, + SYNOR_PROPOSAL_REJECTED, + SYNOR_PROPOSAL_EXECUTED, + SYNOR_PROPOSAL_CANCELLED +}; +static const size_t FINAL_STATUSES_COUNT = sizeof(FINAL_STATUSES) / sizeof(FINAL_STATUSES[0]); + +static bool is_final_status(synor_proposal_status_t status) { + for (size_t i = 0; i < FINAL_STATUSES_COUNT; i++) { + if (FINAL_STATUSES[i] == status) return true; + } + return false; +} + +static const char* vote_choice_to_string(synor_vote_choice_t choice) { + switch (choice) { + case SYNOR_VOTE_FOR: return "for"; + case SYNOR_VOTE_AGAINST: return "against"; + case SYNOR_VOTE_ABSTAIN: return "abstain"; + default: return "abstain"; + } +} + +static const char* dao_type_to_string(synor_dao_type_t type) { + switch (type) { + case SYNOR_DAO_TOKEN: return "token"; + case SYNOR_DAO_MULTISIG: return "multisig"; + case SYNOR_DAO_HYBRID: return "hybrid"; + default: return "token"; + } +} + +synor_governance_t* synor_governance_create(const synor_governance_config_t* config) { + if (!config || !config->api_key) { + return NULL; + } + + synor_governance_t* governance = calloc(1, sizeof(synor_governance_t)); + if (!governance) return NULL; + + const char* endpoint = config->endpoint ? + config->endpoint : "https://governance.synor.io/v1"; + uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 30000; + uint32_t retries = config->retries > 0 ? config->retries : 3; + + governance->http = synor_http_client_create( + config->api_key, endpoint, timeout, retries, config->debug); + + if (!governance->http) { + free(governance); + return NULL; + } + + governance->closed = false; + governance->debug = config->debug; + return governance; +} + +void synor_governance_destroy(synor_governance_t* governance) { + if (!governance) return; + governance->closed = true; + synor_http_client_destroy(governance->http); + free(governance); +} + +bool synor_governance_is_closed(synor_governance_t* governance) { + return governance ? governance->closed : true; +} + +bool synor_governance_health_check(synor_governance_t* governance) { + if (!governance || governance->closed) return false; + + char* response = NULL; + size_t response_size = 0; + + if (synor_http_get(governance->http, "/health", &response, &response_size) != 0) { + return false; + } + + bool healthy = response && strstr(response, "\"healthy\"") != NULL; + free(response); + return healthy; +} + +/* ==================== Proposal Operations ==================== */ + +synor_governance_error_t synor_governance_create_proposal(synor_governance_t* governance, + const synor_proposal_draft_t* draft, synor_proposal_t* result) { + if (!governance || governance->closed) return SYNOR_GOVERNANCE_ERROR_CLIENT_CLOSED; + if (!draft || !result) return SYNOR_GOVERNANCE_ERROR_UNKNOWN; + + char body[4096]; + snprintf(body, sizeof(body), + "{\"title\":\"%s\",\"description\":\"%s\"}", + draft->title, draft->description); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(governance->http, "/proposals", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_GOVERNANCE_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_proposal(synor_governance_t* governance, + const char* proposal_id, synor_proposal_t* result) { + (void)governance; (void)proposal_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_list_proposals(synor_governance_t* governance, + const synor_proposal_filter_t* filter, synor_proposal_list_t* result) { + (void)governance; (void)filter; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_cancel_proposal(synor_governance_t* governance, + const char* proposal_id, synor_proposal_t* result) { + (void)governance; (void)proposal_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_execute_proposal(synor_governance_t* governance, + const char* proposal_id, synor_proposal_t* result) { + (void)governance; (void)proposal_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_wait_for_proposal(synor_governance_t* governance, + const char* proposal_id, uint32_t poll_interval_ms, uint32_t max_wait_ms, + synor_proposal_t* result) { + (void)governance; (void)proposal_id; (void)poll_interval_ms; + (void)max_wait_ms; (void)result; + (void)is_final_status; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +void synor_proposal_free(synor_proposal_t* proposal) { + if (!proposal) return; + free(proposal->id); + free(proposal->title); + free(proposal->description); + free(proposal->discussion_url); + free(proposal->proposer); + free(proposal->vote_tally.for_votes); + free(proposal->vote_tally.against_votes); + free(proposal->vote_tally.abstain_votes); + free(proposal->vote_tally.quorum); + free(proposal->vote_tally.quorum_required); + for (size_t i = 0; i < proposal->actions_count; i++) { + free(proposal->actions[i].target); + free(proposal->actions[i].method); + free(proposal->actions[i].data); + free(proposal->actions[i].value); + } + free(proposal->actions); + free(proposal->dao_id); +} + +void synor_proposal_list_free(synor_proposal_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_proposal_free(&list->proposals[i]); + } + free(list->proposals); + list->proposals = NULL; + list->count = 0; +} + +/* ==================== Voting Operations ==================== */ + +synor_governance_error_t synor_governance_vote(synor_governance_t* governance, + const char* proposal_id, const synor_vote_t* vote, const char* weight, + synor_vote_receipt_t* result) { + if (!governance || governance->closed) return SYNOR_GOVERNANCE_ERROR_CLIENT_CLOSED; + if (!proposal_id || !vote || !result) return SYNOR_GOVERNANCE_ERROR_UNKNOWN; + + char body[512]; + if (vote->reason && weight) { + snprintf(body, sizeof(body), + "{\"choice\":\"%s\",\"reason\":\"%s\",\"weight\":\"%s\"}", + vote_choice_to_string(vote->choice), vote->reason, weight); + } else if (vote->reason) { + snprintf(body, sizeof(body), + "{\"choice\":\"%s\",\"reason\":\"%s\"}", + vote_choice_to_string(vote->choice), vote->reason); + } else if (weight) { + snprintf(body, sizeof(body), + "{\"choice\":\"%s\",\"weight\":\"%s\"}", + vote_choice_to_string(vote->choice), weight); + } else { + snprintf(body, sizeof(body), "{\"choice\":\"%s\"}", vote_choice_to_string(vote->choice)); + } + + char path[256]; + snprintf(path, sizeof(path), "/proposals/%s/vote", proposal_id); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(governance->http, path, body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_GOVERNANCE_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_votes(synor_governance_t* governance, + const char* proposal_id, synor_vote_receipt_list_t* result) { + (void)governance; (void)proposal_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_my_vote(synor_governance_t* governance, + const char* proposal_id, synor_vote_receipt_t* result) { + (void)governance; (void)proposal_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_delegate(synor_governance_t* governance, + const char* delegatee, const char* amount, synor_delegation_receipt_t* result) { + if (!governance || governance->closed) return SYNOR_GOVERNANCE_ERROR_CLIENT_CLOSED; + if (!delegatee || !result) return SYNOR_GOVERNANCE_ERROR_UNKNOWN; + + char body[256]; + if (amount) { + snprintf(body, sizeof(body), "{\"delegatee\":\"%s\",\"amount\":\"%s\"}", delegatee, amount); + } else { + snprintf(body, sizeof(body), "{\"delegatee\":\"%s\"}", delegatee); + } + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(governance->http, "/voting/delegate", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_GOVERNANCE_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_undelegate(synor_governance_t* governance, + const char* delegatee, synor_delegation_receipt_t* result) { + (void)governance; (void)delegatee; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_voting_power(synor_governance_t* governance, + const char* address, synor_voting_power_t* result) { + (void)governance; (void)address; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_delegations(synor_governance_t* governance, + const char* address, synor_delegation_receipt_list_t* result) { + (void)governance; (void)address; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +void synor_vote_receipt_free(synor_vote_receipt_t* receipt) { + if (!receipt) return; + free(receipt->id); + free(receipt->proposal_id); + free(receipt->voter); + free(receipt->weight); + free(receipt->reason); + free(receipt->tx_hash); +} + +void synor_vote_receipt_list_free(synor_vote_receipt_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_vote_receipt_free(&list->votes[i]); + } + free(list->votes); + list->votes = NULL; + list->count = 0; +} + +void synor_voting_power_free(synor_voting_power_t* power) { + if (!power) return; + free(power->address); + free(power->delegated_power); + free(power->own_power); + free(power->total_power); + for (size_t i = 0; i < power->delegators_count; i++) { + free(power->delegators[i]); + } + free(power->delegators); +} + +void synor_delegation_receipt_free(synor_delegation_receipt_t* receipt) { + if (!receipt) return; + free(receipt->id); + free(receipt->from); + free(receipt->to); + free(receipt->amount); + free(receipt->tx_hash); +} + +void synor_delegation_receipt_list_free(synor_delegation_receipt_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_delegation_receipt_free(&list->delegations[i]); + } + free(list->delegations); + list->delegations = NULL; + list->count = 0; +} + +/* ==================== DAO Operations ==================== */ + +synor_governance_error_t synor_governance_create_dao(synor_governance_t* governance, + const synor_dao_config_t* config, synor_dao_t* result) { + if (!governance || governance->closed) return SYNOR_GOVERNANCE_ERROR_CLIENT_CLOSED; + if (!config || !result) return SYNOR_GOVERNANCE_ERROR_UNKNOWN; + + char body[2048]; + snprintf(body, sizeof(body), + "{\"name\":\"%s\",\"description\":\"%s\",\"type\":\"%s\",\"votingPeriodDays\":%d,\"timelockDays\":%d}", + config->name, config->description, dao_type_to_string(config->type), + config->voting_period_days, config->timelock_days); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(governance->http, "/daos", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_GOVERNANCE_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_dao(synor_governance_t* governance, + const char* dao_id, synor_dao_t* result) { + (void)governance; (void)dao_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_list_daos(synor_governance_t* governance, + int32_t limit, int32_t offset, synor_dao_list_t* result) { + (void)governance; (void)limit; (void)offset; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_dao_treasury(synor_governance_t* governance, + const char* dao_id, synor_dao_treasury_t* result) { + (void)governance; (void)dao_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_dao_members(synor_governance_t* governance, + const char* dao_id, char*** members, size_t* count) { + (void)governance; (void)dao_id; (void)members; (void)count; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +void synor_dao_free(synor_dao_t* dao) { + if (!dao) return; + free(dao->id); + free(dao->name); + free(dao->description); + free(dao->token_address); + free(dao->proposal_threshold); + free(dao->treasury_value); +} + +void synor_dao_list_free(synor_dao_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_dao_free(&list->daos[i]); + } + free(list->daos); + list->daos = NULL; + list->count = 0; +} + +void synor_dao_treasury_free(synor_dao_treasury_t* treasury) { + if (!treasury) return; + free(treasury->dao_id); + free(treasury->total_value); + for (size_t i = 0; i < treasury->tokens_count; i++) { + free(treasury->tokens[i].address); + free(treasury->tokens[i].balance); + free(treasury->tokens[i].name); + free(treasury->tokens[i].symbol); + } + free(treasury->tokens); +} + +/* ==================== Vesting Operations ==================== */ + +synor_governance_error_t synor_governance_create_vesting(synor_governance_t* governance, + const synor_vesting_schedule_t* schedule, synor_vesting_contract_t* result) { + if (!governance || governance->closed) return SYNOR_GOVERNANCE_ERROR_CLIENT_CLOSED; + if (!schedule || !result) return SYNOR_GOVERNANCE_ERROR_UNKNOWN; + + char body[1024]; + snprintf(body, sizeof(body), + "{\"beneficiary\":\"%s\",\"totalAmount\":\"%s\",\"startTime\":%lld,\"cliffDuration\":%lld,\"vestingDuration\":%lld,\"revocable\":%s}", + schedule->beneficiary, schedule->total_amount, + (long long)schedule->start_time, (long long)schedule->cliff_duration, + (long long)schedule->vesting_duration, schedule->revocable ? "true" : "false"); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(governance->http, "/vesting", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_GOVERNANCE_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_vesting(synor_governance_t* governance, + const char* contract_id, synor_vesting_contract_t* result) { + (void)governance; (void)contract_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_list_vesting(synor_governance_t* governance, + const char* beneficiary, synor_vesting_contract_list_t* result) { + (void)governance; (void)beneficiary; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_claim_vested(synor_governance_t* governance, + const char* contract_id, synor_claim_receipt_t* result) { + (void)governance; (void)contract_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_revoke_vesting(synor_governance_t* governance, + const char* contract_id, synor_vesting_contract_t* result) { + (void)governance; (void)contract_id; (void)result; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +synor_governance_error_t synor_governance_get_releasable(synor_governance_t* governance, + const char* contract_id, char** amount) { + (void)governance; (void)contract_id; (void)amount; + return SYNOR_GOVERNANCE_ERROR_UNKNOWN; +} + +void synor_vesting_contract_free(synor_vesting_contract_t* contract) { + if (!contract) return; + free(contract->id); + free(contract->beneficiary); + free(contract->grantor); + free(contract->total_amount); + free(contract->released_amount); + free(contract->releasable_amount); +} + +void synor_vesting_contract_list_free(synor_vesting_contract_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_vesting_contract_free(&list->contracts[i]); + } + free(list->contracts); + list->contracts = NULL; + list->count = 0; +} + +void synor_claim_receipt_free(synor_claim_receipt_t* receipt) { + if (!receipt) return; + free(receipt->id); + free(receipt->contract_id); + free(receipt->amount); + free(receipt->tx_hash); +} diff --git a/sdk/c/src/mining/mining.c b/sdk/c/src/mining/mining.c new file mode 100644 index 0000000..dd79c0a --- /dev/null +++ b/sdk/c/src/mining/mining.c @@ -0,0 +1,563 @@ +/** + * @file mining.c + * @brief Synor Mining SDK implementation + */ + +#include +#include +#include +#include "synor/mining.h" + +/* Forward declarations from common.c */ +typedef struct synor_http_client synor_http_client_t; +synor_http_client_t* synor_http_client_create( + const char* api_key, const char* endpoint, + uint32_t timeout_ms, uint32_t retries, bool debug); +void synor_http_client_destroy(synor_http_client_t* client); +int synor_http_get(synor_http_client_t* client, const char* path, + char** response, size_t* response_size); +int synor_http_post(synor_http_client_t* client, const char* path, + const char* body, char** response, size_t* response_size); +int synor_http_put(synor_http_client_t* client, const char* path, + const char* body, char** response, size_t* response_size); +int synor_http_delete(synor_http_client_t* client, const char* path, + char** response, size_t* response_size); +char* synor_strdup(const char* s); +char* synor_url_encode(const char* s); + +/* Internal mining structure */ +struct synor_mining { + synor_http_client_t* http; + bool closed; + bool debug; + synor_stratum_connection_t* active_connection; +}; + +static const char* time_period_to_string(synor_time_period_t period) { + switch (period) { + case SYNOR_PERIOD_HOUR: return "hour"; + case SYNOR_PERIOD_DAY: return "day"; + case SYNOR_PERIOD_WEEK: return "week"; + case SYNOR_PERIOD_MONTH: return "month"; + case SYNOR_PERIOD_ALL: return "all"; + default: return "day"; + } +} + +synor_mining_t* synor_mining_create(const synor_mining_config_t* config) { + if (!config || !config->api_key) { + return NULL; + } + + synor_mining_t* mining = calloc(1, sizeof(synor_mining_t)); + if (!mining) return NULL; + + const char* endpoint = config->endpoint ? + config->endpoint : "https://mining.synor.io/v1"; + uint32_t timeout = config->timeout_ms > 0 ? config->timeout_ms : 60000; + uint32_t retries = config->retries > 0 ? config->retries : 3; + + mining->http = synor_http_client_create( + config->api_key, endpoint, timeout, retries, config->debug); + + if (!mining->http) { + free(mining); + return NULL; + } + + mining->closed = false; + mining->debug = config->debug; + mining->active_connection = NULL; + return mining; +} + +void synor_mining_destroy(synor_mining_t* mining) { + if (!mining) return; + mining->closed = true; + if (mining->active_connection) { + synor_stratum_connection_free(mining->active_connection); + free(mining->active_connection); + } + synor_http_client_destroy(mining->http); + free(mining); +} + +bool synor_mining_is_closed(synor_mining_t* mining) { + return mining ? mining->closed : true; +} + +bool synor_mining_health_check(synor_mining_t* mining) { + if (!mining || mining->closed) return false; + + char* response = NULL; + size_t response_size = 0; + + if (synor_http_get(mining->http, "/health", &response, &response_size) != 0) { + return false; + } + + bool healthy = response && strstr(response, "\"healthy\"") != NULL; + free(response); + return healthy; +} + +/* ==================== Pool Operations ==================== */ + +synor_mining_error_t synor_mining_connect(synor_mining_t* mining, + const synor_pool_config_t* pool, synor_stratum_connection_t* result) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!pool || !result) return SYNOR_MINING_ERROR_UNKNOWN; + + char body[1024]; + snprintf(body, sizeof(body), "{\"url\":\"%s\",\"user\":\"%s\"", pool->url, pool->user); + if (pool->password) { + char temp[256]; + snprintf(temp, sizeof(temp), ",\"password\":\"%s\"", pool->password); + strncat(body, temp, sizeof(body) - strlen(body) - 1); + } + if (pool->algorithm) { + char temp[128]; + snprintf(temp, sizeof(temp), ",\"algorithm\":\"%s\"", pool->algorithm); + strncat(body, temp, sizeof(body) - strlen(body) - 1); + } + if (pool->difficulty > 0) { + char temp[64]; + snprintf(temp, sizeof(temp), ",\"difficulty\":%.2f", pool->difficulty); + strncat(body, temp, sizeof(body) - strlen(body) - 1); + } + strncat(body, "}", sizeof(body) - strlen(body) - 1); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(mining->http, "/pool/connect", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_MINING_ERROR_HTTP; + } + + /* TODO: Parse JSON response and update active_connection */ + free(response); + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_disconnect(synor_mining_t* mining) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!mining->active_connection) return SYNOR_MINING_OK; + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(mining->http, "/pool/disconnect", "{}", &response, &response_size); + free(response); + + if (err == 0) { + if (mining->active_connection) { + synor_stratum_connection_free(mining->active_connection); + free(mining->active_connection); + mining->active_connection = NULL; + } + return SYNOR_MINING_OK; + } + return SYNOR_MINING_ERROR_HTTP; +} + +synor_mining_error_t synor_mining_get_connection(synor_mining_t* mining, + synor_stratum_connection_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_pool_stats(synor_mining_t* mining, + synor_pool_stats_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +bool synor_mining_is_connected(synor_mining_t* mining) { + if (!mining || !mining->active_connection) return false; + return mining->active_connection->status == SYNOR_CONN_CONNECTED; +} + +void synor_stratum_connection_free(synor_stratum_connection_t* conn) { + if (!conn) return; + free(conn->id); + free(conn->pool); + free(conn->algorithm); +} + +void synor_pool_stats_free(synor_pool_stats_t* stats) { + if (!stats) return; + free(stats->url); +} + +/* ==================== Mining Operations ==================== */ + +synor_mining_error_t synor_mining_get_block_template(synor_mining_t* mining, + synor_block_template_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_submit_work(synor_mining_t* mining, + const synor_mined_work_t* work, synor_submit_result_t* result) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!work || !result) return SYNOR_MINING_ERROR_UNKNOWN; + + char body[1024]; + snprintf(body, sizeof(body), + "{\"templateId\":\"%s\",\"nonce\":\"%s\",\"extraNonce\":\"%s\",\"timestamp\":%lld,\"hash\":\"%s\"}", + work->template_id, work->nonce, work->extra_nonce, + (long long)work->timestamp, work->hash); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(mining->http, "/mining/submit", body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_MINING_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_start(synor_mining_t* mining, const char* algorithm) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + + char body[256]; + if (algorithm) { + snprintf(body, sizeof(body), "{\"algorithm\":\"%s\"}", algorithm); + } else { + snprintf(body, sizeof(body), "{}"); + } + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(mining->http, "/mining/start", body, &response, &response_size); + free(response); + return err == 0 ? SYNOR_MINING_OK : SYNOR_MINING_ERROR_HTTP; +} + +synor_mining_error_t synor_mining_stop(synor_mining_t* mining) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(mining->http, "/mining/stop", "{}", &response, &response_size); + free(response); + return err == 0 ? SYNOR_MINING_OK : SYNOR_MINING_ERROR_HTTP; +} + +synor_mining_error_t synor_mining_is_mining(synor_mining_t* mining, bool* result) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!result) return SYNOR_MINING_ERROR_UNKNOWN; + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_get(mining->http, "/mining/status", &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_MINING_ERROR_HTTP; + } + + *result = response && strstr(response, "\"mining\":true") != NULL; + free(response); + return SYNOR_MINING_OK; +} + +void synor_block_template_free(synor_block_template_t* tmpl) { + if (!tmpl) return; + free(tmpl->id); + free(tmpl->previous_block_hash); + free(tmpl->merkle_root); + free(tmpl->bits); + free(tmpl->coinbase_value); + for (size_t i = 0; i < tmpl->transactions_count; i++) { + free(tmpl->transactions[i].txid); + free(tmpl->transactions[i].data); + free(tmpl->transactions[i].fee); + } + free(tmpl->transactions); + free(tmpl->target); + free(tmpl->algorithm); + free(tmpl->extra_nonce); +} + +void synor_submit_result_free(synor_submit_result_t* result) { + if (!result) return; + free(result->share.hash); + free(result->reason); + free(result->block_hash); + free(result->reward); +} + +/* ==================== Stats Operations ==================== */ + +synor_mining_error_t synor_mining_get_hashrate(synor_mining_t* mining, + synor_hashrate_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_stats(synor_mining_t* mining, + synor_mining_stats_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_earnings(synor_mining_t* mining, + synor_time_period_t* period, synor_earnings_t* result) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + + char path[128]; + if (period) { + snprintf(path, sizeof(path), "/stats/earnings?period=%s", time_period_to_string(*period)); + } else { + snprintf(path, sizeof(path), "/stats/earnings"); + } + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_get(mining->http, path, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_MINING_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + (void)result; + free(response); + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_share_stats(synor_mining_t* mining, + synor_share_stats_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +void synor_hashrate_free(synor_hashrate_t* hashrate) { + if (!hashrate) return; + free(hashrate->unit); +} + +void synor_mining_stats_free(synor_mining_stats_t* stats) { + if (!stats) return; + synor_hashrate_free(&stats->hashrate); + free(stats->earnings.today); + free(stats->earnings.yesterday); + free(stats->earnings.this_week); + free(stats->earnings.this_month); + free(stats->earnings.total); + free(stats->earnings.currency); +} + +void synor_earnings_free(synor_earnings_t* earnings) { + if (!earnings) return; + free(earnings->amount); + free(earnings->currency); + for (size_t i = 0; i < earnings->breakdown_count; i++) { + free(earnings->breakdown[i].amount); + } + free(earnings->breakdown); +} + +/* ==================== Device Operations ==================== */ + +synor_mining_error_t synor_mining_list_devices(synor_mining_t* mining, + synor_mining_device_list_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_device(synor_mining_t* mining, + const char* device_id, synor_mining_device_t* result) { + (void)mining; (void)device_id; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_set_device_config(synor_mining_t* mining, + const char* device_id, const synor_device_config_t* config, + synor_mining_device_t* result) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!device_id || !config || !result) return SYNOR_MINING_ERROR_UNKNOWN; + + char body[512]; + snprintf(body, sizeof(body), "{\"enabled\":%s", config->enabled ? "true" : "false"); + if (config->intensity > 0) { + char temp[32]; + snprintf(temp, sizeof(temp), ",\"intensity\":%d", config->intensity); + strncat(body, temp, sizeof(body) - strlen(body) - 1); + } + if (config->power_limit > 0) { + char temp[32]; + snprintf(temp, sizeof(temp), ",\"powerLimit\":%d", config->power_limit); + strncat(body, temp, sizeof(body) - strlen(body) - 1); + } + strncat(body, "}", sizeof(body) - strlen(body) - 1); + + char path[256]; + char* encoded = synor_url_encode(device_id); + snprintf(path, sizeof(path), "/devices/%s/config", encoded); + free(encoded); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_put(mining->http, path, body, &response, &response_size); + if (err != 0) { + free(response); + return SYNOR_MINING_ERROR_HTTP; + } + + /* TODO: Parse JSON response */ + free(response); + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_enable_device(synor_mining_t* mining, + const char* device_id) { + synor_device_config_t config = { .enabled = true }; + synor_mining_device_t result; + return synor_mining_set_device_config(mining, device_id, &config, &result); +} + +synor_mining_error_t synor_mining_disable_device(synor_mining_t* mining, + const char* device_id) { + synor_device_config_t config = { .enabled = false }; + synor_mining_device_t result; + return synor_mining_set_device_config(mining, device_id, &config, &result); +} + +void synor_mining_device_free(synor_mining_device_t* device) { + if (!device) return; + free(device->id); + free(device->name); + free(device->driver); + free(device->firmware); +} + +void synor_mining_device_list_free(synor_mining_device_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_mining_device_free(&list->devices[i]); + } + free(list->devices); + list->devices = NULL; + list->count = 0; +} + +/* ==================== Worker Operations ==================== */ + +synor_mining_error_t synor_mining_list_workers(synor_mining_t* mining, + synor_worker_info_list_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_worker(synor_mining_t* mining, + const char* worker_id, synor_worker_info_t* result) { + (void)mining; (void)worker_id; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_remove_worker(synor_mining_t* mining, + const char* worker_id) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!worker_id) return SYNOR_MINING_ERROR_UNKNOWN; + + char path[256]; + char* encoded = synor_url_encode(worker_id); + snprintf(path, sizeof(path), "/workers/%s", encoded); + free(encoded); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_delete(mining->http, path, &response, &response_size); + free(response); + return err == 0 ? SYNOR_MINING_OK : SYNOR_MINING_ERROR_HTTP; +} + +void synor_worker_info_free(synor_worker_info_t* worker) { + if (!worker) return; + free(worker->id); + free(worker->name); + synor_hashrate_free(&worker->hashrate); + for (size_t i = 0; i < worker->devices_count; i++) { + synor_mining_device_free(&worker->devices[i]); + } + free(worker->devices); +} + +void synor_worker_info_list_free(synor_worker_info_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_worker_info_free(&list->workers[i]); + } + free(list->workers); + list->workers = NULL; + list->count = 0; +} + +/* ==================== Algorithm Operations ==================== */ + +synor_mining_error_t synor_mining_list_algorithms(synor_mining_t* mining, + synor_mining_algorithm_list_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_get_algorithm(synor_mining_t* mining, + const char* name, synor_mining_algorithm_t* result) { + (void)mining; (void)name; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +synor_mining_error_t synor_mining_switch_algorithm(synor_mining_t* mining, + const char* algorithm) { + if (!mining || mining->closed) return SYNOR_MINING_ERROR_CLIENT_CLOSED; + if (!algorithm) return SYNOR_MINING_ERROR_UNKNOWN; + + char body[128]; + snprintf(body, sizeof(body), "{\"algorithm\":\"%s\"}", algorithm); + + char* response = NULL; + size_t response_size = 0; + + int err = synor_http_post(mining->http, "/algorithms/switch", body, &response, &response_size); + free(response); + return err == 0 ? SYNOR_MINING_OK : SYNOR_MINING_ERROR_HTTP; +} + +synor_mining_error_t synor_mining_get_most_profitable(synor_mining_t* mining, + synor_mining_algorithm_t* result) { + (void)mining; (void)result; + return SYNOR_MINING_ERROR_UNKNOWN; +} + +void synor_mining_algorithm_free(synor_mining_algorithm_t* algorithm) { + if (!algorithm) return; + free(algorithm->name); + free(algorithm->display_name); + free(algorithm->hash_unit); + free(algorithm->profitability); + free(algorithm->block_reward); +} + +void synor_mining_algorithm_list_free(synor_mining_algorithm_list_t* list) { + if (!list) return; + for (size_t i = 0; i < list->count; i++) { + synor_mining_algorithm_free(&list->algorithms[i]); + } + free(list->algorithms); + list->algorithms = NULL; + list->count = 0; +} diff --git a/sdk/cpp/include/synor/economics.hpp b/sdk/cpp/include/synor/economics.hpp new file mode 100644 index 0000000..2d20f7e --- /dev/null +++ b/sdk/cpp/include/synor/economics.hpp @@ -0,0 +1,282 @@ +/** + * @file economics.hpp + * @brief Synor Economics SDK for C++ + * + * Modern C++17 SDK for pricing, billing, staking, and discount management. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "wallet.hpp" // For SynorException + +namespace synor { +namespace economics { + +/// Service type +enum class ServiceType { + Compute, + Storage, + Database, + Hosting, + Rpc, + Bridge +}; + +/// Billing period +enum class BillingPeriod { + Hourly, + Daily, + Weekly, + Monthly +}; + +/// Stake status +enum class StakeStatus { + Active, + Unstaking, + Withdrawn +}; + +/// Discount type +enum class DiscountType { + Percentage, + Fixed, + Credits +}; + +/// Invoice status +enum class InvoiceStatus { + Pending, + Paid, + Overdue, + Cancelled +}; + +/// Economics configuration +struct Config { + std::string api_key; + std::string endpoint = "https://economics.synor.io/v1"; + uint32_t timeout_ms = 30000; + uint32_t retries = 3; + bool debug = false; +}; + +/// Service pricing +struct ServicePricing { + ServiceType service; + std::string unit; + std::string price_per_unit; + std::string currency; + double minimum = 0.0; + double maximum = 0.0; +}; + +/// Usage metrics for pricing calculation +struct UsageMetrics { + ServiceType service; + double compute_units = 0.0; + double storage_bytes = 0.0; + double bandwidth_bytes = 0.0; + int64_t duration_seconds = 0; + int32_t requests = 0; +}; + +/// Price calculation result +struct PriceResult { + ServiceType service; + std::string amount; + std::string currency; + UsageMetrics usage; +}; + +/// Usage plan item for cost estimation +struct UsagePlanItem { + ServiceType service; + UsageMetrics projected_usage; + BillingPeriod period; +}; + +/// Cost breakdown item +struct CostItem { + ServiceType service; + std::string amount; +}; + +/// Cost estimate result +struct CostEstimate { + std::string total; + std::string currency; + std::vector breakdown; + std::optional discount_applied; + BillingPeriod period; +}; + +/// Usage item +struct UsageItem { + ServiceType service; + double compute_units; + double storage_bytes; + double bandwidth_bytes; + int32_t requests; + std::string cost; +}; + +/// Usage report +struct Usage { + BillingPeriod period; + int64_t start_date; + int64_t end_date; + std::vector items; + std::string total_cost; + std::string currency; +}; + +/// Invoice line item +struct InvoiceLine { + std::string service; + std::string description; + int32_t quantity; + std::string unit_price; + std::string amount; +}; + +/// Invoice +struct Invoice { + std::string id; + int64_t date; + int64_t due_date; + InvoiceStatus status; + std::vector lines; + std::string subtotal; + std::string discount; + std::string tax; + std::string total; + std::string currency; + std::optional pdf_url; +}; + +/// Account balance +struct AccountBalance { + std::string available; + std::string pending; + std::string staked; + std::string total; + std::string currency; +}; + +/// Stake information +struct Stake { + std::string id; + std::string amount; + int64_t staked_at; + int64_t unlock_at; + StakeStatus status; + std::string rewards_earned; + double apy; +}; + +/// Stake receipt +struct StakeReceipt { + std::string id; + std::string amount; + std::string tx_hash; + int64_t staked_at; + int64_t unlock_at; + double apy; +}; + +/// Unstake receipt +struct UnstakeReceipt { + std::string id; + std::string amount; + std::string tx_hash; + int64_t unstaked_at; + int64_t available_at; +}; + +/// Staking rewards +struct StakingRewards { + std::string pending; + std::string claimed; + std::string total; + double current_apy; + int64_t last_claim; + int64_t next_distribution; +}; + +/// Discount information +struct Discount { + std::string code; + DiscountType type; + std::string value; + std::string description; + std::vector applicable_services; + int64_t valid_from; + int64_t valid_until; + int32_t max_uses; + int32_t current_uses; + bool active; +}; + +/// Discount application result +struct DiscountResult { + Discount discount; + std::string savings_estimate; + int64_t applied_at; +}; + +// Forward declaration +class SynorEconomicsImpl; + +/// Synor Economics client +class SynorEconomics { +public: + explicit SynorEconomics(const Config& config); + ~SynorEconomics(); + + SynorEconomics(const SynorEconomics&) = delete; + SynorEconomics& operator=(const SynorEconomics&) = delete; + SynorEconomics(SynorEconomics&&) noexcept; + SynorEconomics& operator=(SynorEconomics&&) noexcept; + + // Pricing operations + std::future> get_pricing(std::optional service = std::nullopt); + std::future get_price(ServiceType service, const UsageMetrics& usage); + std::future estimate_cost(const std::vector& plan); + + // Billing operations + std::future get_usage(std::optional period = std::nullopt); + std::future> get_invoices(); + std::future get_invoice(const std::string& invoice_id); + std::future get_balance(); + + // Staking operations + std::future stake(const std::string& amount, int32_t duration_days = 0); + std::future unstake(const std::string& stake_id); + std::future> get_stakes(); + std::future get_stake(const std::string& stake_id); + std::future get_staking_rewards(); + std::future claim_rewards(); + + // Discount operations + std::future apply_discount(const std::string& code); + std::future remove_discount(const std::string& code); + std::future> get_discounts(); + + // Lifecycle + void close(); + bool is_closed() const; + std::future health_check(); + +private: + std::unique_ptr impl_; +}; + +} // namespace economics +} // namespace synor diff --git a/sdk/cpp/include/synor/governance.hpp b/sdk/cpp/include/synor/governance.hpp new file mode 100644 index 0000000..4bc244f --- /dev/null +++ b/sdk/cpp/include/synor/governance.hpp @@ -0,0 +1,300 @@ +/** + * @file governance.hpp + * @brief Synor Governance SDK for C++ + * + * Modern C++17 SDK for proposals, voting, DAOs, and vesting operations. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "wallet.hpp" // For SynorException + +namespace synor { +namespace governance { + +/// Proposal status +enum class ProposalStatus { + Pending, + Active, + Passed, + Rejected, + Executed, + Cancelled +}; + +/// Vote choice +enum class VoteChoice { + For, + Against, + Abstain +}; + +/// DAO type +enum class DaoType { + Token, + Multisig, + Hybrid +}; + +/// Vesting status +enum class VestingStatus { + Active, + Completed, + Revoked +}; + +/// Governance configuration +struct Config { + std::string api_key; + std::string endpoint = "https://governance.synor.io/v1"; + uint32_t timeout_ms = 30000; + uint32_t retries = 3; + bool debug = false; +}; + +/// Proposal action +struct ProposalAction { + std::string target; + std::string method; + std::string data; + std::optional value; +}; + +/// Proposal draft +struct ProposalDraft { + std::string title; + std::string description; + std::optional discussion_url; + std::optional voting_start_time; + std::optional voting_end_time; + std::optional dao_id; + std::vector actions; +}; + +/// Vote tally +struct VoteTally { + std::string for_votes; + std::string against_votes; + std::string abstain_votes; + std::string quorum; + std::string quorum_required; + int32_t total_voters; +}; + +/// Proposal +struct Proposal { + std::string id; + std::string title; + std::string description; + std::optional discussion_url; + std::string proposer; + ProposalStatus status; + int64_t created_at; + int64_t voting_start_time; + int64_t voting_end_time; + std::optional execution_time; + VoteTally vote_tally; + std::vector actions; + std::optional dao_id; +}; + +/// Proposal filter +struct ProposalFilter { + std::optional status; + std::optional proposer; + std::optional dao_id; + std::optional limit; + std::optional offset; +}; + +/// Vote +struct Vote { + VoteChoice choice; + std::optional reason; +}; + +/// Vote receipt +struct VoteReceipt { + std::string id; + std::string proposal_id; + std::string voter; + VoteChoice choice; + std::string weight; + std::optional reason; + int64_t voted_at; + std::string tx_hash; +}; + +/// Voting power +struct VotingPower { + std::string address; + std::string delegated_power; + std::string own_power; + std::string total_power; + std::vector delegators; +}; + +/// Delegation receipt +struct DelegationReceipt { + std::string id; + std::string from; + std::string to; + std::string amount; + int64_t delegated_at; + std::string tx_hash; +}; + +/// DAO configuration +struct DaoConfig { + std::string name; + std::string description; + DaoType type; + int32_t voting_period_days; + int32_t timelock_days; + std::optional token_address; + std::optional quorum_percent; + std::optional proposal_threshold; + std::vector multisig_members; + std::optional multisig_threshold; +}; + +/// DAO +struct Dao { + std::string id; + std::string name; + std::string description; + DaoType type; + std::optional token_address; + int32_t voting_period_days; + int32_t timelock_days; + double quorum_percent; + std::string proposal_threshold; + int32_t total_proposals; + int32_t active_proposals; + std::string treasury_value; + int32_t member_count; + int64_t created_at; +}; + +/// Treasury token +struct TreasuryToken { + std::string address; + std::string balance; + std::string name; + std::string symbol; +}; + +/// DAO treasury +struct DaoTreasury { + std::string dao_id; + std::string total_value; + std::vector tokens; + int64_t last_updated; +}; + +/// Vesting schedule +struct VestingSchedule { + std::string beneficiary; + std::string total_amount; + int64_t start_time; + int64_t cliff_duration; + int64_t vesting_duration; + bool revocable; +}; + +/// Vesting contract +struct VestingContract { + std::string id; + std::string beneficiary; + std::string grantor; + std::string total_amount; + std::string released_amount; + std::string releasable_amount; + int64_t start_time; + int64_t cliff_time; + int64_t end_time; + VestingStatus status; + bool revocable; + int64_t created_at; +}; + +/// Claim receipt +struct ClaimReceipt { + std::string id; + std::string contract_id; + std::string amount; + std::string tx_hash; + int64_t claimed_at; +}; + +// Forward declaration +class SynorGovernanceImpl; + +/// Synor Governance client +class SynorGovernance { +public: + explicit SynorGovernance(const Config& config); + ~SynorGovernance(); + + SynorGovernance(const SynorGovernance&) = delete; + SynorGovernance& operator=(const SynorGovernance&) = delete; + SynorGovernance(SynorGovernance&&) noexcept; + SynorGovernance& operator=(SynorGovernance&&) noexcept; + + // Proposal operations + std::future create_proposal(const ProposalDraft& draft); + std::future get_proposal(const std::string& proposal_id); + std::future> list_proposals(const ProposalFilter& filter = {}); + std::future cancel_proposal(const std::string& proposal_id); + std::future execute_proposal(const std::string& proposal_id); + std::future wait_for_proposal( + const std::string& proposal_id, + std::chrono::milliseconds poll_interval = std::chrono::minutes(1), + std::chrono::milliseconds max_wait = std::chrono::hours(168)); + + // Voting operations + std::future vote(const std::string& proposal_id, const Vote& vote, + std::optional weight = std::nullopt); + std::future> get_votes(const std::string& proposal_id); + std::future get_my_vote(const std::string& proposal_id); + std::future delegate(const std::string& delegatee, + std::optional amount = std::nullopt); + std::future undelegate(const std::string& delegatee); + std::future get_voting_power(const std::string& address); + std::future> get_delegations(const std::string& address); + + // DAO operations + std::future create_dao(const DaoConfig& config); + std::future get_dao(const std::string& dao_id); + std::future> list_daos(std::optional limit = std::nullopt, + std::optional offset = std::nullopt); + std::future get_dao_treasury(const std::string& dao_id); + std::future> get_dao_members(const std::string& dao_id); + + // Vesting operations + std::future create_vesting_schedule(const VestingSchedule& schedule); + std::future get_vesting_contract(const std::string& contract_id); + std::future> list_vesting_contracts( + std::optional beneficiary = std::nullopt); + std::future claim_vested(const std::string& contract_id); + std::future revoke_vesting(const std::string& contract_id); + std::future get_releasable_amount(const std::string& contract_id); + + // Lifecycle + void close(); + bool is_closed() const; + std::future health_check(); + +private: + std::unique_ptr impl_; +}; + +} // namespace governance +} // namespace synor diff --git a/sdk/cpp/include/synor/mining.hpp b/sdk/cpp/include/synor/mining.hpp new file mode 100644 index 0000000..b0d0b10 --- /dev/null +++ b/sdk/cpp/include/synor/mining.hpp @@ -0,0 +1,334 @@ +/** + * @file mining.hpp + * @brief Synor Mining SDK for C++ + * + * Modern C++17 SDK for pool connections, block templates, hashrate stats, and GPU management. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "wallet.hpp" // For SynorException + +namespace synor { +namespace mining { + +/// Device type +enum class DeviceType { + Cpu, + GpuNvidia, + GpuAmd, + Asic +}; + +/// Device status +enum class DeviceStatus { + Idle, + Mining, + Error, + Offline +}; + +/// Connection status +enum class ConnectionStatus { + Disconnected, + Connecting, + Connected, + Reconnecting +}; + +/// Time period for stats +enum class TimePeriod { + Hour, + Day, + Week, + Month, + All +}; + +/// Submit result status +enum class SubmitStatus { + Accepted, + Rejected, + Stale +}; + +/// Mining configuration +struct Config { + std::string api_key; + std::string endpoint = "https://mining.synor.io/v1"; + uint32_t timeout_ms = 60000; + uint32_t retries = 3; + bool debug = false; +}; + +/// Pool configuration +struct PoolConfig { + std::string url; + std::string user; + std::optional password; + std::optional algorithm; + std::optional difficulty; +}; + +/// Stratum connection +struct StratumConnection { + std::string id; + std::string pool; + ConnectionStatus status; + std::string algorithm; + double difficulty; + int64_t connected_at; + int32_t accepted_shares; + int32_t rejected_shares; + int32_t stale_shares; + std::optional last_share_at; +}; + +/// Pool stats +struct PoolStats { + std::string url; + int32_t workers; + double hashrate; + double difficulty; + int64_t last_block; + int32_t blocks_found_24h; + double luck; +}; + +/// Template transaction +struct TemplateTransaction { + std::string txid; + std::string data; + std::string fee; + int32_t weight; +}; + +/// Block template +struct BlockTemplate { + std::string id; + std::string previous_block_hash; + std::string merkle_root; + int64_t timestamp; + std::string bits; + int64_t height; + std::string coinbase_value; + std::vector transactions; + std::string target; + std::string algorithm; + std::string extra_nonce; +}; + +/// Mined work +struct MinedWork { + std::string template_id; + std::string nonce; + std::string extra_nonce; + int64_t timestamp; + std::string hash; +}; + +/// Share info +struct ShareInfo { + std::string hash; + double difficulty; + int64_t timestamp; + bool accepted; +}; + +/// Submit result +struct SubmitResult { + SubmitStatus status; + ShareInfo share; + bool block_found; + std::optional reason; + std::optional block_hash; + std::optional reward; +}; + +/// Hashrate +struct Hashrate { + double current; + double average_1h; + double average_24h; + double peak; + std::string unit; +}; + +/// Share stats +struct ShareStats { + int32_t accepted; + int32_t rejected; + int32_t stale; + int32_t total; + double accept_rate; +}; + +/// Device temperature +struct DeviceTemperature { + double current; + double max; + bool throttling; +}; + +/// Earnings snapshot +struct EarningsSnapshot { + std::string today; + std::string yesterday; + std::string this_week; + std::string this_month; + std::string total; + std::string currency; +}; + +/// Mining stats +struct MiningStats { + Hashrate hashrate; + ShareStats shares; + int64_t uptime; + double efficiency; + EarningsSnapshot earnings; + std::optional power_consumption; + std::optional temperature; +}; + +/// Earnings breakdown +struct EarningsBreakdown { + int64_t date; + std::string amount; + int32_t blocks; + int32_t shares; + double hashrate; +}; + +/// Earnings report +struct Earnings { + TimePeriod period; + int64_t start_date; + int64_t end_date; + std::string amount; + int32_t blocks; + int32_t shares; + double average_hashrate; + std::string currency; + std::vector breakdown; +}; + +/// Mining device +struct MiningDevice { + std::string id; + std::string name; + DeviceType type; + DeviceStatus status; + double hashrate; + double temperature; + double fan_speed; + double power_draw; + int64_t memory_used; + int64_t memory_total; + std::optional driver; + std::optional firmware; +}; + +/// Device configuration +struct DeviceConfig { + bool enabled; + std::optional intensity; + std::optional power_limit; + std::optional core_clock_offset; + std::optional memory_clock_offset; + std::optional fan_speed; +}; + +/// Worker info +struct WorkerInfo { + std::string id; + std::string name; + ConnectionStatus status; + Hashrate hashrate; + ShareStats shares; + std::vector devices; + int64_t last_seen; + int64_t uptime; +}; + +/// Mining algorithm +struct MiningAlgorithm { + std::string name; + std::string display_name; + std::string hash_unit; + std::string profitability; + double difficulty; + std::string block_reward; + int32_t block_time; +}; + +// Forward declaration +class SynorMiningImpl; + +/// Synor Mining client +class SynorMining { +public: + explicit SynorMining(const Config& config); + ~SynorMining(); + + SynorMining(const SynorMining&) = delete; + SynorMining& operator=(const SynorMining&) = delete; + SynorMining(SynorMining&&) noexcept; + SynorMining& operator=(SynorMining&&) noexcept; + + // Pool operations + std::future connect(const PoolConfig& pool); + std::future disconnect(); + std::future get_connection(); + std::future get_pool_stats(); + bool is_connected() const; + + // Mining operations + std::future get_block_template(); + std::future submit_work(const MinedWork& work); + std::future start_mining(std::optional algorithm = std::nullopt); + std::future stop_mining(); + std::future is_mining(); + + // Stats operations + std::future get_hashrate(); + std::future get_stats(); + std::future get_earnings(std::optional period = std::nullopt); + std::future get_share_stats(); + + // Device operations + std::future> list_devices(); + std::future get_device(const std::string& device_id); + std::future set_device_config(const std::string& device_id, + const DeviceConfig& config); + std::future enable_device(const std::string& device_id); + std::future disable_device(const std::string& device_id); + + // Worker operations + std::future> list_workers(); + std::future get_worker(const std::string& worker_id); + std::future remove_worker(const std::string& worker_id); + + // Algorithm operations + std::future> list_algorithms(); + std::future get_algorithm(const std::string& name); + std::future switch_algorithm(const std::string& algorithm); + std::future get_most_profitable(); + + // Lifecycle + void close(); + bool is_closed() const; + std::future health_check(); + +private: + std::unique_ptr impl_; +}; + +} // namespace mining +} // namespace synor diff --git a/sdk/csharp/Synor.Sdk/Economics/SynorEconomics.cs b/sdk/csharp/Synor.Sdk/Economics/SynorEconomics.cs new file mode 100644 index 0000000..b93c451 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Economics/SynorEconomics.cs @@ -0,0 +1,165 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace Synor.Sdk.Economics; + +/// +/// Synor Economics SDK client for C#. +/// Pricing, billing, staking, and discount management. +/// +public class SynorEconomics : IDisposable +{ + private readonly EconomicsConfig _config; + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions; + private bool _disposed; + + public SynorEconomics(EconomicsConfig config) + { + _config = config; + _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout }; + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); + _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; + } + + // ==================== Pricing Operations ==================== + + public async Task> GetPricingAsync(ServiceType? service = null, CancellationToken ct = default) + { + var path = service.HasValue ? $"/pricing?service={service.Value.ToString().ToLower()}" : "/pricing"; + var r = await GetAsync(path, ct); + return r.Pricing ?? new List(); + } + + public async Task GetPriceAsync(ServiceType service, UsageMetrics usage, CancellationToken ct = default) + { + var body = new { service = service.ToString().ToLower(), usage }; + return await PostAsync("/pricing/calculate", body, ct); + } + + public async Task EstimateCostAsync(List plan, CancellationToken ct = default) + => await PostAsync("/pricing/estimate", new { plan }, ct); + + // ==================== Billing Operations ==================== + + public async Task GetUsageAsync(BillingPeriod? period = null, CancellationToken ct = default) + { + var path = period.HasValue ? $"/billing/usage?period={period.Value.ToString().ToLower()}" : "/billing/usage"; + return await GetAsync(path, ct); + } + + public async Task> GetInvoicesAsync(CancellationToken ct = default) + { + var r = await GetAsync("/billing/invoices", ct); + return r.Invoices ?? new List(); + } + + public async Task GetInvoiceAsync(string invoiceId, CancellationToken ct = default) + => await GetAsync($"/billing/invoices/{Uri.EscapeDataString(invoiceId)}", ct); + + public async Task GetBalanceAsync(CancellationToken ct = default) + => await GetAsync("/billing/balance", ct); + + // ==================== Staking Operations ==================== + + public async Task StakeAsync(string amount, int durationDays = 0, CancellationToken ct = default) + { + var body = new Dictionary { ["amount"] = amount }; + if (durationDays > 0) body["durationDays"] = durationDays; + return await PostAsync("/staking/stake", body, ct); + } + + public async Task UnstakeAsync(string stakeId, CancellationToken ct = default) + => await PostAsync($"/staking/{Uri.EscapeDataString(stakeId)}/unstake", new { }, ct); + + public async Task> GetStakesAsync(CancellationToken ct = default) + { + var r = await GetAsync("/staking", ct); + return r.Stakes ?? new List(); + } + + public async Task GetStakeAsync(string stakeId, CancellationToken ct = default) + => await GetAsync($"/staking/{Uri.EscapeDataString(stakeId)}", ct); + + public async Task GetStakingRewardsAsync(CancellationToken ct = default) + => await GetAsync("/staking/rewards", ct); + + public async Task ClaimRewardsAsync(CancellationToken ct = default) + => await PostAsync("/staking/rewards/claim", new { }, ct); + + // ==================== Discount Operations ==================== + + public async Task ApplyDiscountAsync(string code, CancellationToken ct = default) + => await PostAsync("/discounts/apply", new { code }, ct); + + public async Task RemoveDiscountAsync(string code, CancellationToken ct = default) + => await DeleteAsync($"/discounts/{Uri.EscapeDataString(code)}", ct); + + public async Task> GetDiscountsAsync(CancellationToken ct = default) + { + var r = await GetAsync("/discounts", ct); + return r.Discounts ?? new List(); + } + + // ==================== Lifecycle ==================== + + public async Task HealthCheckAsync(CancellationToken ct = default) + { + try { var r = await GetAsync("/health", ct); return r.Status == "healthy"; } + catch { return false; } + } + + public bool IsClosed => _disposed; + + public void Dispose() + { + if (!_disposed) { _httpClient.Dispose(); _disposed = true; } + GC.SuppressFinalize(this); + } + + // ==================== Private HTTP Methods ==================== + + private async Task GetAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.GetAsync(path, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task PostAsync(string path, object body, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task DeleteAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.DeleteAsync(path, ct); + await EnsureSuccessAsync(r); + return true; + }); + + private async Task ExecuteAsync(Func> op) + { + Exception? err = null; + for (int i = 0; i < _config.Retries; i++) + { + try { return await op(); } + catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); } + } + throw err!; + } + + private async Task EnsureSuccessAsync(HttpResponseMessage r) + { + if (!r.IsSuccessStatusCode) + { + var c = await r.Content.ReadAsStringAsync(); + var e = JsonSerializer.Deserialize>(c, _jsonOptions); + throw new EconomicsException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", + e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); + } + } +} diff --git a/sdk/csharp/Synor.Sdk/Economics/Types.cs b/sdk/csharp/Synor.Sdk/Economics/Types.cs new file mode 100644 index 0000000..488bc4a --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Economics/Types.cs @@ -0,0 +1,181 @@ +namespace Synor.Sdk.Economics; + +public enum ServiceType { Compute, Storage, Database, Hosting, Rpc, Bridge } +public enum BillingPeriod { Hourly, Daily, Weekly, Monthly } +public enum StakeStatus { Active, Unstaking, Withdrawn } +public enum DiscountType { Percentage, Fixed, Credits } +public enum InvoiceStatus { Pending, Paid, Overdue, Cancelled } + +public record EconomicsConfig( + string ApiKey, + string Endpoint = "https://economics.synor.io/v1", + TimeSpan? Timeout = null, + int Retries = 3, + bool Debug = false +) { + public TimeSpan Timeout { get; init; } = Timeout ?? TimeSpan.FromSeconds(30); +} + +public record ServicePricing( + ServiceType Service, + string Unit, + string PricePerUnit, + string Currency, + double Minimum = 0.0, + double Maximum = 0.0 +); + +public record UsageMetrics( + ServiceType Service, + double ComputeUnits = 0.0, + double StorageBytes = 0.0, + double BandwidthBytes = 0.0, + long DurationSeconds = 0, + int Requests = 0 +); + +public record PriceResult( + ServiceType Service, + string Amount, + string Currency, + UsageMetrics Usage +); + +public record UsagePlanItem( + ServiceType Service, + UsageMetrics ProjectedUsage, + BillingPeriod Period +); + +public record CostItem(ServiceType Service, string Amount); + +public record CostEstimate( + string Total, + string Currency, + List Breakdown, + string? DiscountApplied, + BillingPeriod Period +); + +public record UsageItem( + ServiceType Service, + double ComputeUnits, + double StorageBytes, + double BandwidthBytes, + int Requests, + string Cost +); + +public record Usage( + BillingPeriod Period, + long StartDate, + long EndDate, + List Items, + string TotalCost, + string Currency +); + +public record InvoiceLine( + string Service, + string Description, + int Quantity, + string UnitPrice, + string Amount +); + +public record Invoice( + string Id, + long Date, + long DueDate, + InvoiceStatus Status, + List Lines, + string Subtotal, + string Discount, + string Tax, + string Total, + string Currency, + string? PdfUrl = null +); + +public record AccountBalance( + string Available, + string Pending, + string Staked, + string Total, + string Currency +); + +public record Stake( + string Id, + string Amount, + long StakedAt, + long UnlockAt, + StakeStatus Status, + string RewardsEarned, + double Apy +); + +public record StakeReceipt( + string Id, + string Amount, + string TxHash, + long StakedAt, + long UnlockAt, + double Apy +); + +public record UnstakeReceipt( + string Id, + string Amount, + string TxHash, + long UnstakedAt, + long AvailableAt +); + +public record StakingRewards( + string Pending, + string Claimed, + string Total, + double CurrentApy, + long LastClaim, + long NextDistribution +); + +public record Discount( + string Code, + DiscountType Type, + string Value, + string Description, + List ApplicableServices, + long ValidFrom, + long ValidUntil, + int MaxUses, + int CurrentUses, + bool Active +); + +public record DiscountResult( + Discount Discount, + string SavingsEstimate, + long AppliedAt +); + +// Response wrappers +internal record PricingResponse(List? Pricing); +internal record InvoicesResponse(List? Invoices); +internal record StakesResponse(List? Stakes); +internal record DiscountsResponse(List? Discounts); +internal record HealthResponse(string Status); + +public class EconomicsException : Exception +{ + public string? Code { get; } + public int StatusCode { get; } + + public EconomicsException(string message, string? code = null, int statusCode = 0) + : base(message) + { + Code = code; + StatusCode = statusCode; + } +} diff --git a/sdk/csharp/Synor.Sdk/Governance/SynorGovernance.cs b/sdk/csharp/Synor.Sdk/Governance/SynorGovernance.cs new file mode 100644 index 0000000..5adabb4 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Governance/SynorGovernance.cs @@ -0,0 +1,212 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace Synor.Sdk.Governance; + +/// +/// Synor Governance SDK client for C#. +/// Proposals, voting, DAOs, and vesting operations. +/// +public class SynorGovernance : IDisposable +{ + private readonly GovernanceConfig _config; + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions; + private bool _disposed; + private static readonly HashSet FinalStatuses = new() { ProposalStatus.Passed, ProposalStatus.Rejected, ProposalStatus.Executed, ProposalStatus.Cancelled }; + + public SynorGovernance(GovernanceConfig config) + { + _config = config; + _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout }; + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); + _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; + } + + // ==================== Proposal Operations ==================== + + public async Task CreateProposalAsync(ProposalDraft draft, CancellationToken ct = default) + => await PostAsync("/proposals", draft, ct); + + public async Task GetProposalAsync(string proposalId, CancellationToken ct = default) + => await GetAsync($"/proposals/{Uri.EscapeDataString(proposalId)}", ct); + + public async Task> ListProposalsAsync(ProposalFilter? filter = null, CancellationToken ct = default) + { + var q = new List(); + if (filter?.Status != null) q.Add($"status={filter.Status.ToString()!.ToLower()}"); + if (filter?.Proposer != null) q.Add($"proposer={Uri.EscapeDataString(filter.Proposer)}"); + if (filter?.DaoId != null) q.Add($"daoId={Uri.EscapeDataString(filter.DaoId)}"); + if (filter?.Limit != null) q.Add($"limit={filter.Limit}"); + if (filter?.Offset != null) q.Add($"offset={filter.Offset}"); + var path = q.Count > 0 ? $"/proposals?{string.Join("&", q)}" : "/proposals"; + var r = await GetAsync(path, ct); + return r.Proposals ?? new List(); + } + + public async Task CancelProposalAsync(string proposalId, CancellationToken ct = default) + => await PostAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/cancel", new { }, ct); + + public async Task ExecuteProposalAsync(string proposalId, CancellationToken ct = default) + => await PostAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/execute", new { }, ct); + + public async Task WaitForProposalAsync(string proposalId, TimeSpan? pollInterval = null, TimeSpan? maxWait = null, CancellationToken ct = default) + { + var interval = pollInterval ?? TimeSpan.FromMinutes(1); + var deadline = DateTime.UtcNow + (maxWait ?? TimeSpan.FromDays(7)); + while (DateTime.UtcNow < deadline) + { + var p = await GetProposalAsync(proposalId, ct); + if (FinalStatuses.Contains(p.Status)) return p; + await Task.Delay(interval, ct); + } + throw new GovernanceException("Timeout waiting for proposal completion"); + } + + // ==================== Voting Operations ==================== + + public async Task VoteAsync(string proposalId, Vote vote, string? weight = null, CancellationToken ct = default) + { + var body = new Dictionary { ["choice"] = vote.Choice.ToString().ToLower() }; + if (vote.Reason != null) body["reason"] = vote.Reason; + if (weight != null) body["weight"] = weight; + return await PostAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/vote", body, ct); + } + + public async Task> GetVotesAsync(string proposalId, CancellationToken ct = default) + { + var r = await GetAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/votes", ct); + return r.Votes ?? new List(); + } + + public async Task GetMyVoteAsync(string proposalId, CancellationToken ct = default) + => await GetAsync($"/proposals/{Uri.EscapeDataString(proposalId)}/votes/me", ct); + + public async Task DelegateAsync(string delegatee, string? amount = null, CancellationToken ct = default) + { + var body = new Dictionary { ["delegatee"] = delegatee }; + if (amount != null) body["amount"] = amount; + return await PostAsync("/voting/delegate", body, ct); + } + + public async Task UndelegateAsync(string delegatee, CancellationToken ct = default) + => await PostAsync("/voting/undelegate", new { delegatee }, ct); + + public async Task GetVotingPowerAsync(string address, CancellationToken ct = default) + => await GetAsync($"/voting/power/{Uri.EscapeDataString(address)}", ct); + + public async Task> GetDelegationsAsync(string address, CancellationToken ct = default) + { + var r = await GetAsync($"/voting/delegations/{Uri.EscapeDataString(address)}", ct); + return r.Delegations ?? new List(); + } + + // ==================== DAO Operations ==================== + + public async Task CreateDaoAsync(DaoConfig config, CancellationToken ct = default) + => await PostAsync("/daos", config, ct); + + public async Task GetDaoAsync(string daoId, CancellationToken ct = default) + => await GetAsync($"/daos/{Uri.EscapeDataString(daoId)}", ct); + + public async Task> ListDaosAsync(int? limit = null, int? offset = null, CancellationToken ct = default) + { + var q = new List(); + if (limit != null) q.Add($"limit={limit}"); + if (offset != null) q.Add($"offset={offset}"); + var path = q.Count > 0 ? $"/daos?{string.Join("&", q)}" : "/daos"; + var r = await GetAsync(path, ct); + return r.Daos ?? new List(); + } + + public async Task GetDaoTreasuryAsync(string daoId, CancellationToken ct = default) + => await GetAsync($"/daos/{Uri.EscapeDataString(daoId)}/treasury", ct); + + public async Task> GetDaoMembersAsync(string daoId, CancellationToken ct = default) + { + var r = await GetAsync($"/daos/{Uri.EscapeDataString(daoId)}/members", ct); + return r.Members ?? new List(); + } + + // ==================== Vesting Operations ==================== + + public async Task CreateVestingScheduleAsync(VestingSchedule schedule, CancellationToken ct = default) + => await PostAsync("/vesting", schedule, ct); + + public async Task GetVestingContractAsync(string contractId, CancellationToken ct = default) + => await GetAsync($"/vesting/{Uri.EscapeDataString(contractId)}", ct); + + public async Task> ListVestingContractsAsync(string? beneficiary = null, CancellationToken ct = default) + { + var path = beneficiary != null ? $"/vesting?beneficiary={Uri.EscapeDataString(beneficiary)}" : "/vesting"; + var r = await GetAsync(path, ct); + return r.Contracts ?? new List(); + } + + public async Task ClaimVestedAsync(string contractId, CancellationToken ct = default) + => await PostAsync($"/vesting/{Uri.EscapeDataString(contractId)}/claim", new { }, ct); + + public async Task RevokeVestingAsync(string contractId, CancellationToken ct = default) + => await PostAsync($"/vesting/{Uri.EscapeDataString(contractId)}/revoke", new { }, ct); + + public async Task GetReleasableAmountAsync(string contractId, CancellationToken ct = default) + { + var r = await GetAsync($"/vesting/{Uri.EscapeDataString(contractId)}/releasable", ct); + return r.Amount; + } + + // ==================== Lifecycle ==================== + + public async Task HealthCheckAsync(CancellationToken ct = default) + { + try { var r = await GetAsync("/health", ct); return r.Status == "healthy"; } + catch { return false; } + } + + public bool IsClosed => _disposed; + + public void Dispose() + { + if (!_disposed) { _httpClient.Dispose(); _disposed = true; } + GC.SuppressFinalize(this); + } + + // ==================== Private HTTP Methods ==================== + + private async Task GetAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.GetAsync(path, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task PostAsync(string path, object body, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task ExecuteAsync(Func> op) + { + Exception? err = null; + for (int i = 0; i < _config.Retries; i++) + { + try { return await op(); } + catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); } + } + throw err!; + } + + private async Task EnsureSuccessAsync(HttpResponseMessage r) + { + if (!r.IsSuccessStatusCode) + { + var c = await r.Content.ReadAsStringAsync(); + var e = JsonSerializer.Deserialize>(c, _jsonOptions); + throw new GovernanceException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", + e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); + } + } +} diff --git a/sdk/csharp/Synor.Sdk/Governance/Types.cs b/sdk/csharp/Synor.Sdk/Governance/Types.cs new file mode 100644 index 0000000..308cea6 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Governance/Types.cs @@ -0,0 +1,195 @@ +namespace Synor.Sdk.Governance; + +public enum ProposalStatus { Pending, Active, Passed, Rejected, Executed, Cancelled } +public enum VoteChoice { For, Against, Abstain } +public enum DaoType { Token, Multisig, Hybrid } +public enum VestingStatus { Active, Completed, Revoked } + +public record GovernanceConfig( + string ApiKey, + string Endpoint = "https://governance.synor.io/v1", + TimeSpan? Timeout = null, + int Retries = 3, + bool Debug = false +) { + public TimeSpan Timeout { get; init; } = Timeout ?? TimeSpan.FromSeconds(30); +} + +public record ProposalAction( + string Target, + string Method, + string Data, + string? Value = null +); + +public record ProposalDraft( + string Title, + string Description, + string? DiscussionUrl = null, + long? VotingStartTime = null, + long? VotingEndTime = null, + string? DaoId = null, + List? Actions = null +); + +public record VoteTally( + string ForVotes, + string AgainstVotes, + string AbstainVotes, + string Quorum, + string QuorumRequired, + int TotalVoters +); + +public record Proposal( + string Id, + string Title, + string Description, + string? DiscussionUrl, + string Proposer, + ProposalStatus Status, + long CreatedAt, + long VotingStartTime, + long VotingEndTime, + long? ExecutionTime, + VoteTally VoteTally, + List Actions, + string? DaoId = null +); + +public record ProposalFilter( + ProposalStatus? Status = null, + string? Proposer = null, + string? DaoId = null, + int? Limit = null, + int? Offset = null +); + +public record Vote(VoteChoice Choice, string? Reason = null); + +public record VoteReceipt( + string Id, + string ProposalId, + string Voter, + VoteChoice Choice, + string Weight, + string? Reason, + long VotedAt, + string TxHash +); + +public record VotingPower( + string Address, + string DelegatedPower, + string OwnPower, + string TotalPower, + List Delegators +); + +public record DelegationReceipt( + string Id, + string From, + string To, + string Amount, + long DelegatedAt, + string TxHash +); + +public record DaoConfig( + string Name, + string Description, + DaoType Type, + int VotingPeriodDays, + int TimelockDays, + string? TokenAddress = null, + double? QuorumPercent = null, + string? ProposalThreshold = null, + List? MultisigMembers = null, + int? MultisigThreshold = null +); + +public record Dao( + string Id, + string Name, + string Description, + DaoType Type, + string? TokenAddress, + int VotingPeriodDays, + int TimelockDays, + double QuorumPercent, + string ProposalThreshold, + int TotalProposals, + int ActiveProposals, + string TreasuryValue, + int MemberCount, + long CreatedAt +); + +public record TreasuryToken( + string Address, + string Balance, + string Name, + string Symbol +); + +public record DaoTreasury( + string DaoId, + string TotalValue, + List Tokens, + long LastUpdated +); + +public record VestingSchedule( + string Beneficiary, + string TotalAmount, + long StartTime, + long CliffDuration, + long VestingDuration, + bool Revocable +); + +public record VestingContract( + string Id, + string Beneficiary, + string Grantor, + string TotalAmount, + string ReleasedAmount, + string ReleasableAmount, + long StartTime, + long CliffTime, + long EndTime, + VestingStatus Status, + bool Revocable, + long CreatedAt +); + +public record ClaimReceipt( + string Id, + string ContractId, + string Amount, + string TxHash, + long ClaimedAt +); + +// Response wrappers +internal record ProposalsResponse(List? Proposals); +internal record VotesResponse(List? Votes); +internal record DaosResponse(List? Daos); +internal record MembersResponse(List? Members); +internal record DelegationsResponse(List? Delegations); +internal record VestingContractsResponse(List? Contracts); +internal record ReleasableResponse(string Amount); +internal record GovernanceHealthResponse(string Status); + +public class GovernanceException : Exception +{ + public string? Code { get; } + public int StatusCode { get; } + + public GovernanceException(string message, string? code = null, int statusCode = 0) + : base(message) + { + Code = code; + StatusCode = statusCode; + } +} diff --git a/sdk/csharp/Synor.Sdk/Mining/SynorMining.cs b/sdk/csharp/Synor.Sdk/Mining/SynorMining.cs new file mode 100644 index 0000000..39fd071 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Mining/SynorMining.cs @@ -0,0 +1,215 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace Synor.Sdk.Mining; + +/// +/// Synor Mining SDK client for C#. +/// Pool connections, block templates, hashrate stats, and GPU management. +/// +public class SynorMining : IDisposable +{ + private readonly MiningConfig _config; + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions; + private bool _disposed; + private StratumConnection? _activeConnection; + + public SynorMining(MiningConfig config) + { + _config = config; + _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout }; + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); + _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true }; + } + + // ==================== Pool Operations ==================== + + public async Task ConnectAsync(PoolConfig pool, CancellationToken ct = default) + { + var body = new Dictionary { ["url"] = pool.Url, ["user"] = pool.User }; + if (pool.Password != null) body["password"] = pool.Password; + if (pool.Algorithm != null) body["algorithm"] = pool.Algorithm; + if (pool.Difficulty != null) body["difficulty"] = pool.Difficulty; + _activeConnection = await PostAsync("/pool/connect", body, ct); + return _activeConnection; + } + + public async Task DisconnectAsync(CancellationToken ct = default) + { + if (_activeConnection == null) return; + await PostAsync("/pool/disconnect", new { }, ct); + _activeConnection = null; + } + + public async Task GetConnectionAsync(CancellationToken ct = default) + { + _activeConnection = await GetAsync("/pool/connection", ct); + return _activeConnection; + } + + public async Task GetPoolStatsAsync(CancellationToken ct = default) + => await GetAsync("/pool/stats", ct); + + public bool IsConnected => _activeConnection?.Status == ConnectionStatus.Connected; + + // ==================== Mining Operations ==================== + + public async Task GetBlockTemplateAsync(CancellationToken ct = default) + => await GetAsync("/mining/template", ct); + + public async Task SubmitWorkAsync(MinedWork work, CancellationToken ct = default) + => await PostAsync("/mining/submit", work, ct); + + public async Task StartMiningAsync(string? algorithm = null, CancellationToken ct = default) + { + var body = algorithm != null ? new { algorithm } : (object)new { }; + await PostAsync("/mining/start", body, ct); + } + + public async Task StopMiningAsync(CancellationToken ct = default) + => await PostAsync("/mining/stop", new { }, ct); + + public async Task IsMiningAsync(CancellationToken ct = default) + { + var r = await GetAsync("/mining/status", ct); + return r.Mining; + } + + // ==================== Stats Operations ==================== + + public async Task GetHashrateAsync(CancellationToken ct = default) + => await GetAsync("/stats/hashrate", ct); + + public async Task GetStatsAsync(CancellationToken ct = default) + => await GetAsync("/stats", ct); + + public async Task GetEarningsAsync(TimePeriod? period = null, CancellationToken ct = default) + { + var path = period.HasValue ? $"/stats/earnings?period={period.Value.ToString().ToLower()}" : "/stats/earnings"; + return await GetAsync(path, ct); + } + + public async Task GetShareStatsAsync(CancellationToken ct = default) + => await GetAsync("/stats/shares", ct); + + // ==================== Device Operations ==================== + + public async Task> ListDevicesAsync(CancellationToken ct = default) + { + var r = await GetAsync("/devices", ct); + return r.Devices ?? new List(); + } + + public async Task GetDeviceAsync(string deviceId, CancellationToken ct = default) + => await GetAsync($"/devices/{Uri.EscapeDataString(deviceId)}", ct); + + public async Task SetDeviceConfigAsync(string deviceId, DeviceConfig config, CancellationToken ct = default) + => await PutAsync($"/devices/{Uri.EscapeDataString(deviceId)}/config", config, ct); + + public async Task EnableDeviceAsync(string deviceId, CancellationToken ct = default) + => await SetDeviceConfigAsync(deviceId, new DeviceConfig(true), ct); + + public async Task DisableDeviceAsync(string deviceId, CancellationToken ct = default) + => await SetDeviceConfigAsync(deviceId, new DeviceConfig(false), ct); + + // ==================== Worker Operations ==================== + + public async Task> ListWorkersAsync(CancellationToken ct = default) + { + var r = await GetAsync("/workers", ct); + return r.Workers ?? new List(); + } + + public async Task GetWorkerAsync(string workerId, CancellationToken ct = default) + => await GetAsync($"/workers/{Uri.EscapeDataString(workerId)}", ct); + + public async Task RemoveWorkerAsync(string workerId, CancellationToken ct = default) + => await DeleteAsync($"/workers/{Uri.EscapeDataString(workerId)}", ct); + + // ==================== Algorithm Operations ==================== + + public async Task> ListAlgorithmsAsync(CancellationToken ct = default) + { + var r = await GetAsync("/algorithms", ct); + return r.Algorithms ?? new List(); + } + + public async Task GetAlgorithmAsync(string name, CancellationToken ct = default) + => await GetAsync($"/algorithms/{Uri.EscapeDataString(name)}", ct); + + public async Task SwitchAlgorithmAsync(string algorithm, CancellationToken ct = default) + => await PostAsync("/algorithms/switch", new { algorithm }, ct); + + public async Task GetMostProfitableAsync(CancellationToken ct = default) + => await GetAsync("/algorithms/profitable", ct); + + // ==================== Lifecycle ==================== + + public async Task HealthCheckAsync(CancellationToken ct = default) + { + try { var r = await GetAsync("/health", ct); return r.Status == "healthy"; } + catch { return false; } + } + + public bool IsClosed => _disposed; + + public void Dispose() + { + if (!_disposed) { _httpClient.Dispose(); _activeConnection = null; _disposed = true; } + GC.SuppressFinalize(this); + } + + // ==================== Private HTTP Methods ==================== + + private async Task GetAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.GetAsync(path, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task PostAsync(string path, object body, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task PutAsync(string path, object body, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct); + await EnsureSuccessAsync(r); + return await r.Content.ReadFromJsonAsync(_jsonOptions, ct)!; + }); + + private async Task DeleteAsync(string path, CancellationToken ct) + => await ExecuteAsync(async () => { + var r = await _httpClient.DeleteAsync(path, ct); + await EnsureSuccessAsync(r); + return true; + }); + + private async Task ExecuteAsync(Func> op) + { + Exception? err = null; + for (int i = 0; i < _config.Retries; i++) + { + try { return await op(); } + catch (Exception ex) { err = ex; if (i < _config.Retries - 1) await Task.Delay(1000 << i); } + } + throw err!; + } + + private async Task EnsureSuccessAsync(HttpResponseMessage r) + { + if (!r.IsSuccessStatusCode) + { + var c = await r.Content.ReadAsStringAsync(); + var e = JsonSerializer.Deserialize>(c, _jsonOptions); + throw new MiningException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}", + e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode); + } + } +} diff --git a/sdk/csharp/Synor.Sdk/Mining/Types.cs b/sdk/csharp/Synor.Sdk/Mining/Types.cs new file mode 100644 index 0000000..e77ba91 --- /dev/null +++ b/sdk/csharp/Synor.Sdk/Mining/Types.cs @@ -0,0 +1,219 @@ +namespace Synor.Sdk.Mining; + +public enum DeviceType { Cpu, GpuNvidia, GpuAmd, Asic } +public enum DeviceStatus { Idle, Mining, Error, Offline } +public enum ConnectionStatus { Disconnected, Connecting, Connected, Reconnecting } +public enum TimePeriod { Hour, Day, Week, Month, All } +public enum SubmitStatus { Accepted, Rejected, Stale } + +public record MiningConfig( + string ApiKey, + string Endpoint = "https://mining.synor.io/v1", + TimeSpan? Timeout = null, + int Retries = 3, + bool Debug = false +) { + public TimeSpan Timeout { get; init; } = Timeout ?? TimeSpan.FromSeconds(60); +} + +public record PoolConfig( + string Url, + string User, + string? Password = null, + string? Algorithm = null, + double? Difficulty = null +); + +public record StratumConnection( + string Id, + string Pool, + ConnectionStatus Status, + string Algorithm, + double Difficulty, + long ConnectedAt, + int AcceptedShares, + int RejectedShares, + int StaleShares, + long? LastShareAt = null +); + +public record PoolStats( + string Url, + int Workers, + double Hashrate, + double Difficulty, + long LastBlock, + int BlocksFound24h, + double Luck +); + +public record TemplateTransaction( + string Txid, + string Data, + string Fee, + int Weight +); + +public record BlockTemplate( + string Id, + string PreviousBlockHash, + string MerkleRoot, + long Timestamp, + string Bits, + long Height, + string CoinbaseValue, + List Transactions, + string Target, + string Algorithm, + string ExtraNonce +); + +public record MinedWork( + string TemplateId, + string Nonce, + string ExtraNonce, + long Timestamp, + string Hash +); + +public record ShareInfo( + string Hash, + double Difficulty, + long Timestamp, + bool Accepted +); + +public record SubmitResult( + SubmitStatus Status, + ShareInfo Share, + bool BlockFound, + string? Reason = null, + string? BlockHash = null, + string? Reward = null +); + +public record Hashrate( + double Current, + double Average1h, + double Average24h, + double Peak, + string Unit +); + +public record ShareStats( + int Accepted, + int Rejected, + int Stale, + int Total, + double AcceptRate +); + +public record DeviceTemperature( + double Current, + double Max, + bool Throttling +); + +public record EarningsSnapshot( + string Today, + string Yesterday, + string ThisWeek, + string ThisMonth, + string Total, + string Currency +); + +public record MiningStats( + Hashrate Hashrate, + ShareStats Shares, + long Uptime, + double Efficiency, + EarningsSnapshot Earnings, + double? PowerConsumption = null, + DeviceTemperature? Temperature = null +); + +public record EarningsBreakdown( + long Date, + string Amount, + int Blocks, + int Shares, + double Hashrate +); + +public record Earnings( + TimePeriod Period, + long StartDate, + long EndDate, + string Amount, + int Blocks, + int Shares, + double AverageHashrate, + string Currency, + List Breakdown +); + +public record MiningDevice( + string Id, + string Name, + DeviceType Type, + DeviceStatus Status, + double Hashrate, + double Temperature, + double FanSpeed, + double PowerDraw, + long MemoryUsed, + long MemoryTotal, + string? Driver = null, + string? Firmware = null +); + +public record DeviceConfig( + bool Enabled, + int? Intensity = null, + int? PowerLimit = null, + int? CoreClockOffset = null, + int? MemoryClockOffset = null, + int? FanSpeed = null +); + +public record WorkerInfo( + string Id, + string Name, + ConnectionStatus Status, + Hashrate Hashrate, + ShareStats Shares, + List Devices, + long LastSeen, + long Uptime +); + +public record MiningAlgorithm( + string Name, + string DisplayName, + string HashUnit, + string Profitability, + double Difficulty, + string BlockReward, + int BlockTime +); + +// Response wrappers +internal record DevicesResponse(List? Devices); +internal record WorkersResponse(List? Workers); +internal record AlgorithmsResponse(List? Algorithms); +internal record MiningStatusResponse(bool Mining); +internal record MiningHealthResponse(string Status); + +public class MiningException : Exception +{ + public string? Code { get; } + public int StatusCode { get; } + + public MiningException(string message, string? code = null, int statusCode = 0) + : base(message) + { + Code = code; + StatusCode = statusCode; + } +} diff --git a/sdk/flutter/lib/src/database/client.dart b/sdk/flutter/lib/src/database/client.dart index 6eaea49..cbc539f 100644 --- a/sdk/flutter/lib/src/database/client.dart +++ b/sdk/flutter/lib/src/database/client.dart @@ -51,7 +51,7 @@ class SynorDatabase { String path, [ Map? body, ]) async { - if (_closed) throw DatabaseException('Client has been closed'); + if (_closed) throw const DatabaseException('Client has been closed'); Exception? lastError; for (var attempt = 0; attempt < config.retries; attempt++) { @@ -64,7 +64,7 @@ class SynorDatabase { } } } - throw lastError ?? DatabaseException('Unknown error'); + throw lastError ?? const DatabaseException('Unknown error'); } Future> _doRequest( diff --git a/sdk/flutter/lib/src/economics/client.dart b/sdk/flutter/lib/src/economics/client.dart new file mode 100644 index 0000000..154f66a --- /dev/null +++ b/sdk/flutter/lib/src/economics/client.dart @@ -0,0 +1,217 @@ +/// Synor Economics SDK Client for Flutter +library synor_economics; + +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:http/http.dart' as http; +import 'types.dart'; + +export 'types.dart'; + +/// Synor Economics Client +class SynorEconomics { + final EconomicsConfig config; + final http.Client _client; + bool _closed = false; + + SynorEconomics(this.config) : _client = http.Client(); + + // ==================== Pricing Operations ==================== + + Future getPrice(ServiceType service, UsageMetrics usage) async { + final resp = await _request('POST', '/pricing/calculate', { + 'service': service.name, + 'usage': usage.toJson(), + }); + return Price.fromJson(resp); + } + + Future estimateCost(UsagePlan plan) async { + final resp = await _request('POST', '/pricing/estimate', plan.toJson()); + return CostEstimate.fromJson(resp); + } + + Future> getPricingTiers(ServiceType service) async { + final resp = await _request('GET', '/pricing/tiers/${service.name}'); + return (resp['tiers'] as List).map((e) => PricingTier.fromJson(e)).toList(); + } + + // ==================== Billing Operations ==================== + + Future getUsage({BillingPeriod? period}) async { + final path = period != null ? '/billing/usage?period=${period.name}' : '/billing/usage'; + final resp = await _request('GET', path); + return Usage.fromJson(resp); + } + + Future getUsageByDateRange(int startDate, int endDate) async { + final resp = await _request('GET', '/billing/usage?startDate=$startDate&endDate=$endDate'); + return Usage.fromJson(resp); + } + + Future> getInvoices() async { + final resp = await _request('GET', '/billing/invoices'); + return (resp['invoices'] as List).map((e) => Invoice.fromJson(e)).toList(); + } + + Future getInvoice(String invoiceId) async { + final resp = await _request('GET', '/billing/invoices/${Uri.encodeComponent(invoiceId)}'); + return Invoice.fromJson(resp); + } + + Future downloadInvoice(String invoiceId) async { + final resp = await _request('GET', '/billing/invoices/${Uri.encodeComponent(invoiceId)}/pdf'); + return base64Decode(resp['data'] as String); + } + + Future getBalance() async { + final resp = await _request('GET', '/billing/balance'); + return AccountBalance.fromJson(resp); + } + + Future addCredits(String amount, String paymentMethod) async { + await _request('POST', '/billing/credits', { + 'amount': amount, + 'paymentMethod': paymentMethod, + }); + } + + // ==================== Staking Operations ==================== + + Future stake(String amount, {int? durationDays}) async { + final body = {'amount': amount}; + if (durationDays != null) body['durationDays'] = durationDays; + final resp = await _request('POST', '/staking/stake', body); + return StakeReceipt.fromJson(resp); + } + + Future unstake(String stakeId) async { + final resp = await _request('POST', '/staking/unstake/${Uri.encodeComponent(stakeId)}'); + return UnstakeReceipt.fromJson(resp); + } + + Future> getStakes() async { + final resp = await _request('GET', '/staking/stakes'); + return (resp['stakes'] as List).map((e) => StakeInfo.fromJson(e)).toList(); + } + + Future getStake(String stakeId) async { + final resp = await _request('GET', '/staking/stakes/${Uri.encodeComponent(stakeId)}'); + return StakeInfo.fromJson(resp); + } + + Future getStakingRewards() async { + final resp = await _request('GET', '/staking/rewards'); + return Rewards.fromJson(resp); + } + + Future claimRewards() async { + final resp = await _request('POST', '/staking/rewards/claim'); + return resp['txHash'] as String; + } + + Future getCurrentApy() async { + final resp = await _request('GET', '/staking/apy'); + return (resp['apy'] as num).toDouble(); + } + + // ==================== Discount Operations ==================== + + Future applyDiscount(String code) async { + final resp = await _request('POST', '/discounts/apply', {'code': code}); + return AppliedDiscount.fromJson(resp); + } + + Future> getAvailableDiscounts() async { + final resp = await _request('GET', '/discounts/available'); + return (resp['discounts'] as List).map((e) => Discount.fromJson(e)).toList(); + } + + Future validateDiscount(String code) async { + final resp = await _request('GET', '/discounts/validate/${Uri.encodeComponent(code)}'); + return Discount.fromJson(resp); + } + + Future> getAppliedDiscounts() async { + final resp = await _request('GET', '/discounts/applied'); + return (resp['discounts'] as List).map((e) => AppliedDiscount.fromJson(e)).toList(); + } + + Future removeDiscount(String discountId) async { + await _request('DELETE', '/discounts/${Uri.encodeComponent(discountId)}'); + } + + // ==================== 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 EconomicsException('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 EconomicsException('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 'PUT': + response = await _client.put(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 EconomicsException('Unknown method: $method'); + } + + final respBody = jsonDecode(response.body) as Map; + if (response.statusCode >= 400) { + throw EconomicsException( + respBody['message'] as String? ?? 'HTTP ${response.statusCode}', + code: respBody['code'] as String?, + statusCode: response.statusCode, + ); + } + return respBody; + } +} diff --git a/sdk/flutter/lib/src/economics/types.dart b/sdk/flutter/lib/src/economics/types.dart new file mode 100644 index 0000000..ff255b3 --- /dev/null +++ b/sdk/flutter/lib/src/economics/types.dart @@ -0,0 +1,532 @@ +/// Synor Economics SDK Types for Flutter +library synor_economics_types; + +// ==================== Enums ==================== + +enum ServiceType { compute, storage, database, hosting, bridge, mining, rpc } +enum BillingPeriod { daily, weekly, monthly, yearly } +enum StakeStatus { active, unstaking, unlocked, slashed } +enum DiscountType { percentage, fixed, volume } + +// ==================== Pricing Types ==================== + +class UsageMetrics { + final double computeHours; + final double storageGb; + final int requests; + final double bandwidthGb; + final double gpuHours; + + const UsageMetrics({ + this.computeHours = 0, + this.storageGb = 0, + this.requests = 0, + this.bandwidthGb = 0, + this.gpuHours = 0, + }); + + Map toJson() => { + 'computeHours': computeHours, + 'storageGb': storageGb, + 'requests': requests, + 'bandwidthGb': bandwidthGb, + 'gpuHours': gpuHours, + }; +} + +class PricingTier { + final String name; + final double upTo; + final double pricePerUnit; + final String unit; + + const PricingTier({ + required this.name, + required this.upTo, + required this.pricePerUnit, + required this.unit, + }); + + factory PricingTier.fromJson(Map json) => PricingTier( + name: json['name'] as String, + upTo: (json['upTo'] as num).toDouble(), + pricePerUnit: (json['pricePerUnit'] as num).toDouble(), + unit: json['unit'] as String, + ); +} + +class Price { + final ServiceType service; + final String amount; + final String currency; + final String amountUsd; + final List? breakdown; + final String? discount; + final String finalAmount; + + const Price({ + required this.service, + required this.amount, + required this.currency, + required this.amountUsd, + this.breakdown, + this.discount, + required this.finalAmount, + }); + + factory Price.fromJson(Map json) => Price( + service: ServiceType.values.firstWhere((e) => e.name == json['service']), + amount: json['amount'] as String, + currency: json['currency'] as String, + amountUsd: json['amountUsd'] as String, + breakdown: (json['breakdown'] as List?)?.map((e) => PricingTier.fromJson(e)).toList(), + discount: json['discount'] as String?, + finalAmount: json['finalAmount'] as String, + ); +} + +class UsagePlan { + final ServiceType service; + final UsageMetrics estimatedUsage; + final BillingPeriod period; + + const UsagePlan({required this.service, required this.estimatedUsage, required this.period}); + + Map toJson() => { + 'service': service.name, + 'estimatedUsage': estimatedUsage.toJson(), + 'period': period.name, + }; +} + +class CostBreakdown { + final String category; + final String amount; + final double percentage; + + const CostBreakdown({required this.category, required this.amount, required this.percentage}); + + factory CostBreakdown.fromJson(Map json) => CostBreakdown( + category: json['category'] as String, + amount: json['amount'] as String, + percentage: (json['percentage'] as num).toDouble(), + ); +} + +class CostEstimate { + final ServiceType service; + final BillingPeriod period; + final String estimatedCost; + final String estimatedCostUsd; + final String currency; + final String confidenceLevel; + final List? breakdown; + final String? savingsFromStaking; + + const CostEstimate({ + required this.service, + required this.period, + required this.estimatedCost, + required this.estimatedCostUsd, + required this.currency, + required this.confidenceLevel, + this.breakdown, + this.savingsFromStaking, + }); + + factory CostEstimate.fromJson(Map json) => CostEstimate( + service: ServiceType.values.firstWhere((e) => e.name == json['service']), + period: BillingPeriod.values.firstWhere((e) => e.name == json['period']), + estimatedCost: json['estimatedCost'] as String, + estimatedCostUsd: json['estimatedCostUsd'] as String, + currency: json['currency'] as String, + confidenceLevel: json['confidenceLevel'] as String, + breakdown: (json['breakdown'] as List?)?.map((e) => CostBreakdown.fromJson(e)).toList(), + savingsFromStaking: json['savingsFromStaking'] as String?, + ); +} + +// ==================== Billing Types ==================== + +class UsageRecord { + final ServiceType service; + final String resource; + final double quantity; + final String unit; + final int timestamp; + final String cost; + + const UsageRecord({ + required this.service, + required this.resource, + required this.quantity, + required this.unit, + required this.timestamp, + required this.cost, + }); + + factory UsageRecord.fromJson(Map json) => UsageRecord( + service: ServiceType.values.firstWhere((e) => e.name == json['service']), + resource: json['resource'] as String, + quantity: (json['quantity'] as num).toDouble(), + unit: json['unit'] as String, + timestamp: json['timestamp'] as int, + cost: json['cost'] as String, + ); +} + +class Usage { + final BillingPeriod period; + final int startDate; + final int endDate; + final List records; + final String totalCost; + final String currency; + + const Usage({ + required this.period, + required this.startDate, + required this.endDate, + required this.records, + required this.totalCost, + required this.currency, + }); + + factory Usage.fromJson(Map json) => Usage( + period: BillingPeriod.values.firstWhere((e) => e.name == json['period']), + startDate: json['startDate'] as int, + endDate: json['endDate'] as int, + records: (json['records'] as List).map((e) => UsageRecord.fromJson(e)).toList(), + totalCost: json['totalCost'] as String, + currency: json['currency'] as String, + ); +} + +class InvoiceLineItem { + final String description; + final double quantity; + final String unit; + final String unitPrice; + final String amount; + + const InvoiceLineItem({ + required this.description, + required this.quantity, + required this.unit, + required this.unitPrice, + required this.amount, + }); + + factory InvoiceLineItem.fromJson(Map json) => InvoiceLineItem( + description: json['description'] as String, + quantity: (json['quantity'] as num).toDouble(), + unit: json['unit'] as String, + unitPrice: json['unitPrice'] as String, + amount: json['amount'] as String, + ); +} + +class Invoice { + final String id; + final String number; + final int periodStart; + final int periodEnd; + final String subtotal; + final String tax; + final String discount; + final String total; + final String currency; + final String status; + final int? dueDate; + final int? paidAt; + final List lineItems; + final String? pdfUrl; + + const Invoice({ + required this.id, + required this.number, + required this.periodStart, + required this.periodEnd, + required this.subtotal, + required this.tax, + required this.discount, + required this.total, + required this.currency, + required this.status, + this.dueDate, + this.paidAt, + required this.lineItems, + this.pdfUrl, + }); + + factory Invoice.fromJson(Map json) => Invoice( + id: json['id'] as String, + number: json['number'] as String, + periodStart: json['periodStart'] as int, + periodEnd: json['periodEnd'] as int, + subtotal: json['subtotal'] as String, + tax: json['tax'] as String, + discount: json['discount'] as String, + total: json['total'] as String, + currency: json['currency'] as String, + status: json['status'] as String, + dueDate: json['dueDate'] as int?, + paidAt: json['paidAt'] as int?, + lineItems: (json['lineItems'] as List).map((e) => InvoiceLineItem.fromJson(e)).toList(), + pdfUrl: json['pdfUrl'] as String?, + ); +} + +class AccountBalance { + final String available; + final String pending; + final String reserved; + final String staked; + final String currency; + final int lastUpdated; + + const AccountBalance({ + required this.available, + required this.pending, + required this.reserved, + required this.staked, + required this.currency, + required this.lastUpdated, + }); + + factory AccountBalance.fromJson(Map json) => AccountBalance( + available: json['available'] as String, + pending: json['pending'] as String, + reserved: json['reserved'] as String, + staked: json['staked'] as String, + currency: json['currency'] as String, + lastUpdated: json['lastUpdated'] as int, + ); +} + +// ==================== Staking Types ==================== + +class StakeReceipt { + final String id; + final String txHash; + final String amount; + final int startDate; + final int endDate; + final double apy; + final StakeStatus status; + + const StakeReceipt({ + required this.id, + required this.txHash, + required this.amount, + required this.startDate, + required this.endDate, + required this.apy, + required this.status, + }); + + factory StakeReceipt.fromJson(Map json) => StakeReceipt( + id: json['id'] as String, + txHash: json['txHash'] as String, + amount: json['amount'] as String, + startDate: json['startDate'] as int, + endDate: json['endDate'] as int, + apy: (json['apy'] as num).toDouble(), + status: StakeStatus.values.firstWhere((e) => e.name == json['status']), + ); +} + +class StakeInfo { + final String id; + final String amount; + final StakeStatus status; + final int startDate; + final int endDate; + final double apy; + final String earnedRewards; + final String pendingRewards; + final int? unlockDate; + final int discountPercent; + + const StakeInfo({ + required this.id, + required this.amount, + required this.status, + required this.startDate, + required this.endDate, + required this.apy, + required this.earnedRewards, + required this.pendingRewards, + this.unlockDate, + this.discountPercent = 0, + }); + + factory StakeInfo.fromJson(Map json) => StakeInfo( + id: json['id'] as String, + amount: json['amount'] as String, + status: StakeStatus.values.firstWhere((e) => e.name == json['status']), + startDate: json['startDate'] as int, + endDate: json['endDate'] as int, + apy: (json['apy'] as num).toDouble(), + earnedRewards: json['earnedRewards'] as String, + pendingRewards: json['pendingRewards'] as String, + unlockDate: json['unlockDate'] as int?, + discountPercent: json['discountPercent'] as int? ?? 0, + ); +} + +class UnstakeReceipt { + final String id; + final String txHash; + final String amount; + final int initiatedAt; + final int availableAt; + final String? penalty; + + const UnstakeReceipt({ + required this.id, + required this.txHash, + required this.amount, + required this.initiatedAt, + required this.availableAt, + this.penalty, + }); + + factory UnstakeReceipt.fromJson(Map json) => UnstakeReceipt( + id: json['id'] as String, + txHash: json['txHash'] as String, + amount: json['amount'] as String, + initiatedAt: json['initiatedAt'] as int, + availableAt: json['availableAt'] as int, + penalty: json['penalty'] as String?, + ); +} + +class RewardRecord { + final int date; + final String amount; + final String type; + final String? txHash; + + const RewardRecord({required this.date, required this.amount, required this.type, this.txHash}); + + factory RewardRecord.fromJson(Map json) => RewardRecord( + date: json['date'] as int, + amount: json['amount'] as String, + type: json['type'] as String, + txHash: json['txHash'] as String?, + ); +} + +class Rewards { + final String totalEarned; + final String pendingClaim; + final String? lastClaimDate; + final double currentApy; + final List history; + + const Rewards({ + required this.totalEarned, + required this.pendingClaim, + this.lastClaimDate, + required this.currentApy, + required this.history, + }); + + factory Rewards.fromJson(Map json) => Rewards( + totalEarned: json['totalEarned'] as String, + pendingClaim: json['pendingClaim'] as String, + lastClaimDate: json['lastClaimDate'] as String?, + currentApy: (json['currentApy'] as num).toDouble(), + history: (json['history'] as List).map((e) => RewardRecord.fromJson(e)).toList(), + ); +} + +// ==================== Discount Types ==================== + +class Discount { + final String id; + final String code; + final DiscountType type; + final String value; + final int? validFrom; + final int? validUntil; + final int? usageLimit; + final int usageCount; + final List? applicableServices; + final String? minimumSpend; + final bool active; + + const Discount({ + required this.id, + required this.code, + required this.type, + required this.value, + this.validFrom, + this.validUntil, + this.usageLimit, + this.usageCount = 0, + this.applicableServices, + this.minimumSpend, + this.active = true, + }); + + factory Discount.fromJson(Map json) => Discount( + id: json['id'] as String, + code: json['code'] as String, + type: DiscountType.values.firstWhere((e) => e.name == json['type']), + value: json['value'] as String, + validFrom: json['validFrom'] as int?, + validUntil: json['validUntil'] as int?, + usageLimit: json['usageLimit'] as int?, + usageCount: json['usageCount'] as int? ?? 0, + applicableServices: (json['applicableServices'] as List?) + ?.map((e) => ServiceType.values.firstWhere((s) => s.name == e)) + .toList(), + minimumSpend: json['minimumSpend'] as String?, + active: json['active'] as bool? ?? true, + ); +} + +class AppliedDiscount { + final Discount discount; + final String savedAmount; + final int appliedAt; + + const AppliedDiscount({required this.discount, required this.savedAmount, required this.appliedAt}); + + factory AppliedDiscount.fromJson(Map json) => AppliedDiscount( + discount: Discount.fromJson(json['discount']), + savedAmount: json['savedAmount'] as String, + appliedAt: json['appliedAt'] as int, + ); +} + +// ==================== Config & Error ==================== + +class EconomicsConfig { + final String apiKey; + final String endpoint; + final Duration timeout; + final int retries; + final bool debug; + + const EconomicsConfig({ + required this.apiKey, + this.endpoint = 'https://economics.synor.io/v1', + this.timeout = const Duration(seconds: 60), + this.retries = 3, + this.debug = false, + }); +} + +class EconomicsException implements Exception { + final String message; + final String? code; + final int statusCode; + + const EconomicsException(this.message, {this.code, this.statusCode = 0}); + + @override + String toString() => 'EconomicsException: $message'; +} diff --git a/sdk/flutter/lib/src/governance/client.dart b/sdk/flutter/lib/src/governance/client.dart new file mode 100644 index 0000000..7e99dc4 --- /dev/null +++ b/sdk/flutter/lib/src/governance/client.dart @@ -0,0 +1,250 @@ +/// 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; + } +} diff --git a/sdk/flutter/lib/src/governance/types.dart b/sdk/flutter/lib/src/governance/types.dart new file mode 100644 index 0000000..08b0f17 --- /dev/null +++ b/sdk/flutter/lib/src/governance/types.dart @@ -0,0 +1,497 @@ +/// Synor Governance SDK Types for Flutter +library synor_governance_types; + +// ==================== Enums ==================== + +enum ProposalStatus { draft, active, passed, rejected, executed, cancelled } +enum VoteChoice { yes, no, abstain } +enum DaoType { token, multisig, hybrid } +enum VestingStatus { pending, active, paused, completed, revoked } + +// ==================== Proposal Types ==================== + +class ProposalAction { + final String target; + final String method; + final String data; + final String? value; + + const ProposalAction({required this.target, required this.method, required this.data, this.value}); + + Map toJson() => {'target': target, 'method': method, 'data': data, if (value != null) 'value': value}; + + factory ProposalAction.fromJson(Map json) => ProposalAction( + target: json['target'] as String, + method: json['method'] as String, + data: json['data'] as String, + value: json['value'] as String?, + ); +} + +class ProposalDraft { + final String title; + final String description; + final String? discussionUrl; + final int? votingStartTime; + final int? votingEndTime; + final List? actions; + final String? daoId; + + const ProposalDraft({ + required this.title, + required this.description, + this.discussionUrl, + this.votingStartTime, + this.votingEndTime, + this.actions, + this.daoId, + }); + + Map toJson() => { + 'title': title, + 'description': description, + if (discussionUrl != null) 'discussionUrl': discussionUrl, + if (votingStartTime != null) 'votingStartTime': votingStartTime, + if (votingEndTime != null) 'votingEndTime': votingEndTime, + if (actions != null) 'actions': actions!.map((e) => e.toJson()).toList(), + if (daoId != null) 'daoId': daoId, + }; +} + +class VoteBreakdown { + final String yes; + final String no; + final String abstain; + final String quorum; + final double quorumPercent; + final bool quorumReached; + + const VoteBreakdown({ + required this.yes, + required this.no, + required this.abstain, + required this.quorum, + required this.quorumPercent, + required this.quorumReached, + }); + + factory VoteBreakdown.fromJson(Map json) => VoteBreakdown( + yes: json['yes'] as String, + no: json['no'] as String, + abstain: json['abstain'] as String, + quorum: json['quorum'] as String, + quorumPercent: (json['quorumPercent'] as num).toDouble(), + quorumReached: json['quorumReached'] as bool, + ); +} + +class Proposal { + final String id; + final String title; + final String description; + final String proposer; + final ProposalStatus status; + final int createdAt; + final int votingStartTime; + final int votingEndTime; + final VoteBreakdown votes; + final String? discussionUrl; + final List? actions; + final String? daoId; + final String? snapshotBlock; + final int? executedAt; + final String? executedTxHash; + + const Proposal({ + required this.id, + required this.title, + required this.description, + required this.proposer, + required this.status, + required this.createdAt, + required this.votingStartTime, + required this.votingEndTime, + required this.votes, + this.discussionUrl, + this.actions, + this.daoId, + this.snapshotBlock, + this.executedAt, + this.executedTxHash, + }); + + factory Proposal.fromJson(Map json) => Proposal( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String, + proposer: json['proposer'] as String, + status: ProposalStatus.values.firstWhere((e) => e.name == json['status']), + createdAt: json['createdAt'] as int, + votingStartTime: json['votingStartTime'] as int, + votingEndTime: json['votingEndTime'] as int, + votes: VoteBreakdown.fromJson(json['votes']), + discussionUrl: json['discussionUrl'] as String?, + actions: (json['actions'] as List?)?.map((e) => ProposalAction.fromJson(e)).toList(), + daoId: json['daoId'] as String?, + snapshotBlock: json['snapshotBlock'] as String?, + executedAt: json['executedAt'] as int?, + executedTxHash: json['executedTxHash'] as String?, + ); +} + +class ProposalFilter { + final ProposalStatus? status; + final String? proposer; + final String? daoId; + final int? limit; + final int? offset; + + const ProposalFilter({this.status, this.proposer, this.daoId, this.limit, this.offset}); +} + +// ==================== Voting Types ==================== + +class Vote { + final VoteChoice choice; + final String? reason; + + const Vote({required this.choice, this.reason}); +} + +class VoteReceipt { + final String id; + final String proposalId; + final String voter; + final VoteChoice choice; + final String weight; + final int timestamp; + final String txHash; + final String? reason; + + const VoteReceipt({ + required this.id, + required this.proposalId, + required this.voter, + required this.choice, + required this.weight, + required this.timestamp, + required this.txHash, + this.reason, + }); + + factory VoteReceipt.fromJson(Map json) => VoteReceipt( + id: json['id'] as String, + proposalId: json['proposalId'] as String, + voter: json['voter'] as String, + choice: VoteChoice.values.firstWhere((e) => e.name == json['choice']), + weight: json['weight'] as String, + timestamp: json['timestamp'] as int, + txHash: json['txHash'] as String, + reason: json['reason'] as String?, + ); +} + +class VotingPower { + final String address; + final String balance; + final String? delegatedTo; + final String delegatedFrom; + final String totalPower; + final int blockNumber; + + const VotingPower({ + required this.address, + required this.balance, + this.delegatedTo, + required this.delegatedFrom, + required this.totalPower, + required this.blockNumber, + }); + + factory VotingPower.fromJson(Map json) => VotingPower( + address: json['address'] as String, + balance: json['balance'] as String, + delegatedTo: json['delegatedTo'] as String?, + delegatedFrom: json['delegatedFrom'] as String, + totalPower: json['totalPower'] as String, + blockNumber: json['blockNumber'] as int, + ); +} + +class DelegationReceipt { + final String id; + final String delegator; + final String delegatee; + final String amount; + final int timestamp; + final String txHash; + + const DelegationReceipt({ + required this.id, + required this.delegator, + required this.delegatee, + required this.amount, + required this.timestamp, + required this.txHash, + }); + + factory DelegationReceipt.fromJson(Map json) => DelegationReceipt( + id: json['id'] as String, + delegator: json['delegator'] as String, + delegatee: json['delegatee'] as String, + amount: json['amount'] as String, + timestamp: json['timestamp'] as int, + txHash: json['txHash'] as String, + ); +} + +// ==================== DAO Types ==================== + +class DaoConfig { + final String name; + final String description; + final DaoType type; + final String? tokenAddress; + final String? quorumPercent; + final int votingPeriodDays; + final int timelockDays; + final int? proposalThreshold; + final List? multisigMembers; + final int? multisigThreshold; + + const DaoConfig({ + required this.name, + required this.description, + required this.type, + this.tokenAddress, + this.quorumPercent, + this.votingPeriodDays = 7, + this.timelockDays = 2, + this.proposalThreshold, + this.multisigMembers, + this.multisigThreshold, + }); + + Map toJson() => { + 'name': name, + 'description': description, + 'type': type.name, + if (tokenAddress != null) 'tokenAddress': tokenAddress, + if (quorumPercent != null) 'quorumPercent': quorumPercent, + 'votingPeriodDays': votingPeriodDays, + 'timelockDays': timelockDays, + if (proposalThreshold != null) 'proposalThreshold': proposalThreshold, + if (multisigMembers != null) 'multisigMembers': multisigMembers, + if (multisigThreshold != null) 'multisigThreshold': multisigThreshold, + }; +} + +class TreasuryAsset { + final String address; + final String symbol; + final String balance; + final String valueUsd; + + const TreasuryAsset({required this.address, required this.symbol, required this.balance, required this.valueUsd}); + + factory TreasuryAsset.fromJson(Map json) => TreasuryAsset( + address: json['address'] as String, + symbol: json['symbol'] as String, + balance: json['balance'] as String, + valueUsd: json['valueUsd'] as String, + ); +} + +class DaoTreasury { + final String address; + final String balance; + final String currency; + final List assets; + + const DaoTreasury({required this.address, required this.balance, required this.currency, required this.assets}); + + factory DaoTreasury.fromJson(Map json) => DaoTreasury( + address: json['address'] as String, + balance: json['balance'] as String, + currency: json['currency'] as String, + assets: (json['assets'] as List).map((e) => TreasuryAsset.fromJson(e)).toList(), + ); +} + +class Dao { + final String id; + final String name; + final String description; + final DaoType type; + final String? tokenAddress; + final String governorAddress; + final String timelockAddress; + final DaoTreasury treasury; + final String quorumPercent; + final int votingPeriodDays; + final int timelockDays; + final int proposalCount; + final int memberCount; + final int createdAt; + + const Dao({ + required this.id, + required this.name, + required this.description, + required this.type, + this.tokenAddress, + required this.governorAddress, + required this.timelockAddress, + required this.treasury, + required this.quorumPercent, + required this.votingPeriodDays, + required this.timelockDays, + required this.proposalCount, + required this.memberCount, + required this.createdAt, + }); + + factory Dao.fromJson(Map json) => Dao( + id: json['id'] as String, + name: json['name'] as String, + description: json['description'] as String, + type: DaoType.values.firstWhere((e) => e.name == json['type']), + tokenAddress: json['tokenAddress'] as String?, + governorAddress: json['governorAddress'] as String, + timelockAddress: json['timelockAddress'] as String, + treasury: DaoTreasury.fromJson(json['treasury']), + quorumPercent: json['quorumPercent'] as String, + votingPeriodDays: json['votingPeriodDays'] as int, + timelockDays: json['timelockDays'] as int, + proposalCount: json['proposalCount'] as int, + memberCount: json['memberCount'] as int, + createdAt: json['createdAt'] as int, + ); +} + +// ==================== Vesting Types ==================== + +class VestingSchedule { + final String beneficiary; + final String totalAmount; + final int startTime; + final int cliffDuration; + final int vestingDuration; + final bool revocable; + + const VestingSchedule({ + required this.beneficiary, + required this.totalAmount, + required this.startTime, + required this.cliffDuration, + required this.vestingDuration, + this.revocable = false, + }); + + Map toJson() => { + 'beneficiary': beneficiary, + 'totalAmount': totalAmount, + 'startTime': startTime, + 'cliffDuration': cliffDuration, + 'vestingDuration': vestingDuration, + 'revocable': revocable, + }; +} + +class VestingContract { + final String id; + final String contractAddress; + final String beneficiary; + final String totalAmount; + final String releasedAmount; + final String releasableAmount; + final int startTime; + final int cliffEnd; + final int vestingEnd; + final VestingStatus status; + final int createdAt; + final String txHash; + + const VestingContract({ + required this.id, + required this.contractAddress, + required this.beneficiary, + required this.totalAmount, + required this.releasedAmount, + required this.releasableAmount, + required this.startTime, + required this.cliffEnd, + required this.vestingEnd, + required this.status, + required this.createdAt, + required this.txHash, + }); + + factory VestingContract.fromJson(Map json) => VestingContract( + id: json['id'] as String, + contractAddress: json['contractAddress'] as String, + beneficiary: json['beneficiary'] as String, + totalAmount: json['totalAmount'] as String, + releasedAmount: json['releasedAmount'] as String, + releasableAmount: json['releasableAmount'] as String, + startTime: json['startTime'] as int, + cliffEnd: json['cliffEnd'] as int, + vestingEnd: json['vestingEnd'] as int, + status: VestingStatus.values.firstWhere((e) => e.name == json['status']), + createdAt: json['createdAt'] as int, + txHash: json['txHash'] as String, + ); +} + +class ClaimReceipt { + final String vestingContractId; + final String amount; + final String txHash; + final int timestamp; + final String remainingAmount; + + const ClaimReceipt({ + required this.vestingContractId, + required this.amount, + required this.txHash, + required this.timestamp, + required this.remainingAmount, + }); + + factory ClaimReceipt.fromJson(Map json) => ClaimReceipt( + vestingContractId: json['vestingContractId'] as String, + amount: json['amount'] as String, + txHash: json['txHash'] as String, + timestamp: json['timestamp'] as int, + remainingAmount: json['remainingAmount'] as String, + ); +} + +// ==================== Config & Error ==================== + +class GovernanceConfig { + final String apiKey; + final String endpoint; + final Duration timeout; + final int retries; + final bool debug; + + const GovernanceConfig({ + required this.apiKey, + this.endpoint = 'https://governance.synor.io/v1', + this.timeout = const Duration(seconds: 60), + this.retries = 3, + this.debug = false, + }); +} + +class GovernanceException implements Exception { + final String message; + final String? code; + final int statusCode; + + const GovernanceException(this.message, {this.code, this.statusCode = 0}); + + @override + String toString() => 'GovernanceException: $message'; +} diff --git a/sdk/flutter/lib/src/hosting/client.dart b/sdk/flutter/lib/src/hosting/client.dart index 8004fc7..05de920 100644 --- a/sdk/flutter/lib/src/hosting/client.dart +++ b/sdk/flutter/lib/src/hosting/client.dart @@ -189,7 +189,7 @@ class SynorHosting { String path, [ Map? body, ]) async { - if (_closed) throw HostingException('Client has been closed'); + if (_closed) throw const HostingException('Client has been closed'); Exception? lastError; for (var attempt = 0; attempt < config.retries; attempt++) { @@ -202,7 +202,7 @@ class SynorHosting { } } } - throw lastError ?? HostingException('Unknown error'); + throw lastError ?? const HostingException('Unknown error'); } Future> _doRequest( diff --git a/sdk/flutter/lib/src/mining/client.dart b/sdk/flutter/lib/src/mining/client.dart new file mode 100644 index 0000000..e9d7112 --- /dev/null +++ b/sdk/flutter/lib/src/mining/client.dart @@ -0,0 +1,231 @@ +/// Synor Mining SDK Client for Flutter +library synor_mining; + +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'types.dart'; + +export 'types.dart'; + +/// Synor Mining Client +class SynorMining { + final MiningConfig config; + final http.Client _client; + bool _closed = false; + StratumConnection? _activeConnection; + + SynorMining(this.config) : _client = http.Client(); + + // ==================== Pool Operations ==================== + + Future connect(PoolConfig pool) async { + final resp = await _request('POST', '/pool/connect', pool.toJson()); + _activeConnection = StratumConnection.fromJson(resp); + return _activeConnection!; + } + + Future disconnect() async { + if (_activeConnection == null) return; + await _request('POST', '/pool/disconnect'); + _activeConnection = null; + } + + Future getConnection() async { + final resp = await _request('GET', '/pool/connection'); + _activeConnection = StratumConnection.fromJson(resp); + return _activeConnection!; + } + + Future getPoolStats() async { + final resp = await _request('GET', '/pool/stats'); + return PoolStats.fromJson(resp); + } + + bool get isConnected => _activeConnection?.status == ConnectionStatus.connected; + + // ==================== Mining Operations ==================== + + Future getBlockTemplate() async { + final resp = await _request('GET', '/mining/template'); + return BlockTemplate.fromJson(resp); + } + + Future submitWork(MinedWork work) async { + final resp = await _request('POST', '/mining/submit', work.toJson()); + return SubmitResult.fromJson(resp); + } + + Future startMining({String? algorithm}) async { + final body = algorithm != null ? {'algorithm': algorithm} : null; + await _request('POST', '/mining/start', body); + } + + Future stopMining() async { + await _request('POST', '/mining/stop'); + } + + Future isMining() async { + final resp = await _request('GET', '/mining/status'); + return resp['mining'] as bool; + } + + // ==================== Stats Operations ==================== + + Future getHashrate() async { + final resp = await _request('GET', '/stats/hashrate'); + return Hashrate.fromJson(resp); + } + + Future getStats() async { + final resp = await _request('GET', '/stats'); + return MiningStats.fromJson(resp); + } + + Future getEarnings({TimePeriod? period}) async { + final path = period != null ? '/stats/earnings?period=${period.name}' : '/stats/earnings'; + final resp = await _request('GET', path); + return Earnings.fromJson(resp); + } + + Future getShareStats() async { + final resp = await _request('GET', '/stats/shares'); + return ShareStats.fromJson(resp); + } + + // ==================== Device Operations ==================== + + Future> listDevices() async { + final resp = await _request('GET', '/devices'); + return (resp['devices'] as List).map((e) => MiningDevice.fromJson(e)).toList(); + } + + Future getDevice(String deviceId) async { + final resp = await _request('GET', '/devices/${Uri.encodeComponent(deviceId)}'); + return MiningDevice.fromJson(resp); + } + + Future setDeviceConfig(String deviceId, DeviceConfig deviceConfig) async { + final resp = await _request('PUT', '/devices/${Uri.encodeComponent(deviceId)}/config', deviceConfig.toJson()); + return MiningDevice.fromJson(resp); + } + + Future enableDevice(String deviceId) async { + await setDeviceConfig(deviceId, const DeviceConfig(enabled: true)); + } + + Future disableDevice(String deviceId) async { + await setDeviceConfig(deviceId, const DeviceConfig(enabled: false)); + } + + // ==================== Worker Operations ==================== + + Future> listWorkers() async { + final resp = await _request('GET', '/workers'); + return (resp['workers'] as List).map((e) => WorkerInfo.fromJson(e)).toList(); + } + + Future getWorker(String workerId) async { + final resp = await _request('GET', '/workers/${Uri.encodeComponent(workerId)}'); + return WorkerInfo.fromJson(resp); + } + + Future removeWorker(String workerId) async { + await _request('DELETE', '/workers/${Uri.encodeComponent(workerId)}'); + } + + // ==================== Algorithm Operations ==================== + + Future> listAlgorithms() async { + final resp = await _request('GET', '/algorithms'); + return (resp['algorithms'] as List).map((e) => MiningAlgorithm.fromJson(e)).toList(); + } + + Future getAlgorithm(String name) async { + final resp = await _request('GET', '/algorithms/${Uri.encodeComponent(name)}'); + return MiningAlgorithm.fromJson(resp); + } + + Future switchAlgorithm(String algorithm) async { + await _request('POST', '/algorithms/switch', {'algorithm': algorithm}); + } + + Future getMostProfitable() async { + final resp = await _request('GET', '/algorithms/profitable'); + return MiningAlgorithm.fromJson(resp); + } + + // ==================== Lifecycle ==================== + + void close() { + _closed = true; + _activeConnection = null; + _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 MiningException('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 MiningException('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 'PUT': + response = await _client.put(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 MiningException('Unknown method: $method'); + } + + final respBody = jsonDecode(response.body) as Map; + if (response.statusCode >= 400) { + throw MiningException( + respBody['message'] as String? ?? 'HTTP ${response.statusCode}', + code: respBody['code'] as String?, + statusCode: response.statusCode, + ); + } + return respBody; + } +} diff --git a/sdk/flutter/lib/src/mining/types.dart b/sdk/flutter/lib/src/mining/types.dart new file mode 100644 index 0000000..689e93f --- /dev/null +++ b/sdk/flutter/lib/src/mining/types.dart @@ -0,0 +1,555 @@ +/// Synor Mining SDK Types for Flutter +library synor_mining_types; + +// ==================== Enums ==================== + +enum DeviceType { cpu, gpu_nvidia, gpu_amd, asic } +enum DeviceStatus { idle, mining, error, offline } +enum ConnectionStatus { disconnected, connecting, connected, reconnecting } +enum TimePeriod { hour, day, week, month, all } +enum SubmitResultStatus { accepted, rejected, stale } + +// ==================== Pool Types ==================== + +class PoolConfig { + final String url; + final String user; + final String? password; + final String? algorithm; + final double? difficulty; + + const PoolConfig({required this.url, required this.user, this.password, this.algorithm, this.difficulty}); + + Map toJson() => { + 'url': url, + 'user': user, + if (password != null) 'password': password, + if (algorithm != null) 'algorithm': algorithm, + if (difficulty != null) 'difficulty': difficulty, + }; +} + +class StratumConnection { + final String id; + final String pool; + final ConnectionStatus status; + final String algorithm; + final double difficulty; + final int connectedAt; + final int acceptedShares; + final int rejectedShares; + final int staleShares; + final int? lastShareAt; + + const StratumConnection({ + required this.id, + required this.pool, + required this.status, + required this.algorithm, + required this.difficulty, + required this.connectedAt, + required this.acceptedShares, + required this.rejectedShares, + required this.staleShares, + this.lastShareAt, + }); + + factory StratumConnection.fromJson(Map json) => StratumConnection( + id: json['id'] as String, + pool: json['pool'] as String, + status: ConnectionStatus.values.firstWhere((e) => e.name == json['status']), + algorithm: json['algorithm'] as String, + difficulty: (json['difficulty'] as num).toDouble(), + connectedAt: json['connectedAt'] as int, + acceptedShares: json['acceptedShares'] as int, + rejectedShares: json['rejectedShares'] as int, + staleShares: json['staleShares'] as int, + lastShareAt: json['lastShareAt'] as int?, + ); +} + +class PoolStats { + final String url; + final int workers; + final double hashrate; + final double difficulty; + final int lastBlock; + final int blocksFound24h; + final double luck; + + const PoolStats({ + required this.url, + required this.workers, + required this.hashrate, + required this.difficulty, + required this.lastBlock, + required this.blocksFound24h, + required this.luck, + }); + + factory PoolStats.fromJson(Map json) => PoolStats( + url: json['url'] as String, + workers: json['workers'] as int, + hashrate: (json['hashrate'] as num).toDouble(), + difficulty: (json['difficulty'] as num).toDouble(), + lastBlock: json['lastBlock'] as int, + blocksFound24h: json['blocksFound24h'] as int, + luck: (json['luck'] as num).toDouble(), + ); +} + +// ==================== Mining Types ==================== + +class TemplateTransaction { + final String txid; + final String data; + final String fee; + final int weight; + + const TemplateTransaction({required this.txid, required this.data, required this.fee, required this.weight}); + + factory TemplateTransaction.fromJson(Map json) => TemplateTransaction( + txid: json['txid'] as String, + data: json['data'] as String, + fee: json['fee'] as String, + weight: json['weight'] as int, + ); +} + +class BlockTemplate { + final String id; + final String previousBlockHash; + final String merkleRoot; + final int timestamp; + final String bits; + final int height; + final String coinbaseValue; + final List transactions; + final String target; + final String algorithm; + final String extraNonce; + + const BlockTemplate({ + required this.id, + required this.previousBlockHash, + required this.merkleRoot, + required this.timestamp, + required this.bits, + required this.height, + required this.coinbaseValue, + required this.transactions, + required this.target, + required this.algorithm, + required this.extraNonce, + }); + + factory BlockTemplate.fromJson(Map json) => BlockTemplate( + id: json['id'] as String, + previousBlockHash: json['previousBlockHash'] as String, + merkleRoot: json['merkleRoot'] as String, + timestamp: json['timestamp'] as int, + bits: json['bits'] as String, + height: json['height'] as int, + coinbaseValue: json['coinbaseValue'] as String, + transactions: (json['transactions'] as List).map((e) => TemplateTransaction.fromJson(e)).toList(), + target: json['target'] as String, + algorithm: json['algorithm'] as String, + extraNonce: json['extraNonce'] as String, + ); +} + +class MinedWork { + final String templateId; + final String nonce; + final String extraNonce; + final int timestamp; + final String hash; + + const MinedWork({ + required this.templateId, + required this.nonce, + required this.extraNonce, + required this.timestamp, + required this.hash, + }); + + Map toJson() => { + 'templateId': templateId, + 'nonce': nonce, + 'extraNonce': extraNonce, + 'timestamp': timestamp, + 'hash': hash, + }; +} + +class ShareInfo { + final String hash; + final double difficulty; + final int timestamp; + final bool accepted; + + const ShareInfo({required this.hash, required this.difficulty, required this.timestamp, required this.accepted}); + + factory ShareInfo.fromJson(Map json) => ShareInfo( + hash: json['hash'] as String, + difficulty: (json['difficulty'] as num).toDouble(), + timestamp: json['timestamp'] as int, + accepted: json['accepted'] as bool, + ); +} + +class SubmitResult { + final SubmitResultStatus status; + final ShareInfo share; + final bool blockFound; + final String? reason; + final String? blockHash; + final String? reward; + + const SubmitResult({ + required this.status, + required this.share, + required this.blockFound, + this.reason, + this.blockHash, + this.reward, + }); + + factory SubmitResult.fromJson(Map json) => SubmitResult( + status: SubmitResultStatus.values.firstWhere((e) => e.name == json['status']), + share: ShareInfo.fromJson(json['share']), + blockFound: json['blockFound'] as bool, + reason: json['reason'] as String?, + blockHash: json['blockHash'] as String?, + reward: json['reward'] as String?, + ); +} + +// ==================== Stats Types ==================== + +class Hashrate { + final double current; + final double average1h; + final double average24h; + final double peak; + final String unit; + + const Hashrate({required this.current, required this.average1h, required this.average24h, required this.peak, required this.unit}); + + factory Hashrate.fromJson(Map json) => Hashrate( + current: (json['current'] as num).toDouble(), + average1h: (json['average1h'] as num).toDouble(), + average24h: (json['average24h'] as num).toDouble(), + peak: (json['peak'] as num).toDouble(), + unit: json['unit'] as String, + ); +} + +class ShareStats { + final int accepted; + final int rejected; + final int stale; + final int total; + final double acceptRate; + + const ShareStats({required this.accepted, required this.rejected, required this.stale, required this.total, required this.acceptRate}); + + factory ShareStats.fromJson(Map json) => ShareStats( + accepted: json['accepted'] as int, + rejected: json['rejected'] as int, + stale: json['stale'] as int, + total: json['total'] as int, + acceptRate: (json['acceptRate'] as num).toDouble(), + ); +} + +class DeviceTemperature { + final double current; + final double max; + final bool throttling; + + const DeviceTemperature({required this.current, required this.max, required this.throttling}); + + factory DeviceTemperature.fromJson(Map json) => DeviceTemperature( + current: (json['current'] as num).toDouble(), + max: (json['max'] as num).toDouble(), + throttling: json['throttling'] as bool, + ); +} + +class EarningsSnapshot { + final String today; + final String yesterday; + final String thisWeek; + final String thisMonth; + final String total; + final String currency; + + const EarningsSnapshot({ + required this.today, + required this.yesterday, + required this.thisWeek, + required this.thisMonth, + required this.total, + required this.currency, + }); + + factory EarningsSnapshot.fromJson(Map json) => EarningsSnapshot( + today: json['today'] as String, + yesterday: json['yesterday'] as String, + thisWeek: json['thisWeek'] as String, + thisMonth: json['thisMonth'] as String, + total: json['total'] as String, + currency: json['currency'] as String, + ); +} + +class MiningStats { + final Hashrate hashrate; + final ShareStats shares; + final int uptime; + final double efficiency; + final EarningsSnapshot earnings; + final double? powerConsumption; + final DeviceTemperature? temperature; + + const MiningStats({ + required this.hashrate, + required this.shares, + required this.uptime, + required this.efficiency, + required this.earnings, + this.powerConsumption, + this.temperature, + }); + + factory MiningStats.fromJson(Map json) => MiningStats( + hashrate: Hashrate.fromJson(json['hashrate']), + shares: ShareStats.fromJson(json['shares']), + uptime: json['uptime'] as int, + efficiency: (json['efficiency'] as num).toDouble(), + earnings: EarningsSnapshot.fromJson(json['earnings']), + powerConsumption: (json['powerConsumption'] as num?)?.toDouble(), + temperature: json['temperature'] != null ? DeviceTemperature.fromJson(json['temperature']) : null, + ); +} + +class EarningsBreakdown { + final int date; + final String amount; + final int blocks; + final int shares; + final double hashrate; + + const EarningsBreakdown({required this.date, required this.amount, required this.blocks, required this.shares, required this.hashrate}); + + factory EarningsBreakdown.fromJson(Map json) => EarningsBreakdown( + date: json['date'] as int, + amount: json['amount'] as String, + blocks: json['blocks'] as int, + shares: json['shares'] as int, + hashrate: (json['hashrate'] as num).toDouble(), + ); +} + +class Earnings { + final TimePeriod period; + final int startDate; + final int endDate; + final String amount; + final int blocks; + final int shares; + final double averageHashrate; + final String currency; + final List breakdown; + + const Earnings({ + required this.period, + required this.startDate, + required this.endDate, + required this.amount, + required this.blocks, + required this.shares, + required this.averageHashrate, + required this.currency, + required this.breakdown, + }); + + factory Earnings.fromJson(Map json) => Earnings( + period: TimePeriod.values.firstWhere((e) => e.name == json['period']), + startDate: json['startDate'] as int, + endDate: json['endDate'] as int, + amount: json['amount'] as String, + blocks: json['blocks'] as int, + shares: json['shares'] as int, + averageHashrate: (json['averageHashrate'] as num).toDouble(), + currency: json['currency'] as String, + breakdown: (json['breakdown'] as List).map((e) => EarningsBreakdown.fromJson(e)).toList(), + ); +} + +// ==================== Device Types ==================== + +class MiningDevice { + final String id; + final String name; + final DeviceType type; + final DeviceStatus status; + final double hashrate; + final double temperature; + final double fanSpeed; + final double powerDraw; + final int memoryUsed; + final int memoryTotal; + final String? driver; + final String? firmware; + + const MiningDevice({ + required this.id, + required this.name, + required this.type, + required this.status, + required this.hashrate, + required this.temperature, + required this.fanSpeed, + required this.powerDraw, + required this.memoryUsed, + required this.memoryTotal, + this.driver, + this.firmware, + }); + + factory MiningDevice.fromJson(Map json) => MiningDevice( + id: json['id'] as String, + name: json['name'] as String, + type: DeviceType.values.firstWhere((e) => e.name == json['type']), + status: DeviceStatus.values.firstWhere((e) => e.name == json['status']), + hashrate: (json['hashrate'] as num).toDouble(), + temperature: (json['temperature'] as num).toDouble(), + fanSpeed: (json['fanSpeed'] as num).toDouble(), + powerDraw: (json['powerDraw'] as num).toDouble(), + memoryUsed: json['memoryUsed'] as int, + memoryTotal: json['memoryTotal'] as int, + driver: json['driver'] as String?, + firmware: json['firmware'] as String?, + ); +} + +class DeviceConfig { + final bool enabled; + final int? intensity; + final int? powerLimit; + final int? coreClockOffset; + final int? memoryClockOffset; + final int? fanSpeed; + + const DeviceConfig({ + required this.enabled, + this.intensity, + this.powerLimit, + this.coreClockOffset, + this.memoryClockOffset, + this.fanSpeed, + }); + + Map toJson() => { + 'enabled': enabled, + if (intensity != null) 'intensity': intensity, + if (powerLimit != null) 'powerLimit': powerLimit, + if (coreClockOffset != null) 'coreClockOffset': coreClockOffset, + if (memoryClockOffset != null) 'memoryClockOffset': memoryClockOffset, + if (fanSpeed != null) 'fanSpeed': fanSpeed, + }; +} + +class WorkerInfo { + final String id; + final String name; + final ConnectionStatus status; + final Hashrate hashrate; + final ShareStats shares; + final List devices; + final int lastSeen; + final int uptime; + + const WorkerInfo({ + required this.id, + required this.name, + required this.status, + required this.hashrate, + required this.shares, + required this.devices, + required this.lastSeen, + required this.uptime, + }); + + factory WorkerInfo.fromJson(Map json) => WorkerInfo( + id: json['id'] as String, + name: json['name'] as String, + status: ConnectionStatus.values.firstWhere((e) => e.name == json['status']), + hashrate: Hashrate.fromJson(json['hashrate']), + shares: ShareStats.fromJson(json['shares']), + devices: (json['devices'] as List).map((e) => MiningDevice.fromJson(e)).toList(), + lastSeen: json['lastSeen'] as int, + uptime: json['uptime'] as int, + ); +} + +class MiningAlgorithm { + final String name; + final String displayName; + final String hashUnit; + final String profitability; + final double difficulty; + final String blockReward; + final int blockTime; + + const MiningAlgorithm({ + required this.name, + required this.displayName, + required this.hashUnit, + required this.profitability, + required this.difficulty, + required this.blockReward, + required this.blockTime, + }); + + factory MiningAlgorithm.fromJson(Map json) => MiningAlgorithm( + name: json['name'] as String, + displayName: json['displayName'] as String, + hashUnit: json['hashUnit'] as String, + profitability: json['profitability'] as String, + difficulty: (json['difficulty'] as num).toDouble(), + blockReward: json['blockReward'] as String, + blockTime: json['blockTime'] as int, + ); +} + +// ==================== Config & Error ==================== + +class MiningConfig { + final String apiKey; + final String endpoint; + final Duration timeout; + final int retries; + final bool debug; + + const MiningConfig({ + required this.apiKey, + this.endpoint = 'https://mining.synor.io/v1', + this.timeout = const Duration(seconds: 60), + this.retries = 3, + this.debug = false, + }); +} + +class MiningException implements Exception { + final String message; + final String? code; + final int statusCode; + + const MiningException(this.message, {this.code, this.statusCode = 0}); + + @override + String toString() => 'MiningException: $message'; +} diff --git a/sdk/java/src/main/java/io/synor/economics/SynorEconomics.java b/sdk/java/src/main/java/io/synor/economics/SynorEconomics.java new file mode 100644 index 0000000..36dbf50 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/economics/SynorEconomics.java @@ -0,0 +1,298 @@ +package io.synor.economics; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Synor Economics SDK for Java. + * Pricing, billing, staking, and discount operations. + */ +public class SynorEconomics implements AutoCloseable { + private static final String DEFAULT_ENDPOINT = "https://economics.synor.io/v1"; + private static final Gson gson = new GsonBuilder().create(); + + private final EconomicsConfig config; + private final HttpClient httpClient; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public SynorEconomics(String apiKey) { + this(new EconomicsConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false)); + } + + public SynorEconomics(EconomicsConfig config) { + this.config = config; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(config.getTimeoutSecs())) + .build(); + } + + // ==================== Pricing Operations ==================== + + public CompletableFuture getPrice(ServiceType service, UsageMetrics usage) { + Map body = new HashMap<>(); + body.put("service", service.name().toLowerCase()); + body.put("usage", usage); + return request("POST", "/pricing/calculate", body) + .thenApply(resp -> gson.fromJson(resp, Price.class)); + } + + public CompletableFuture estimateCost(UsagePlan plan) { + Map body = new HashMap<>(); + body.put("service", plan.getService().name().toLowerCase()); + body.put("estimatedUsage", plan.getEstimatedUsage()); + body.put("period", plan.getPeriod().name().toLowerCase()); + return request("POST", "/pricing/estimate", body) + .thenApply(resp -> gson.fromJson(resp, CostEstimate.class)); + } + + public CompletableFuture> getPricingTiers(ServiceType service) { + return request("GET", "/pricing/tiers/" + service.name().toLowerCase(), null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + TiersResponse result = gson.fromJson(resp, type); + return result.tiers; + }); + } + + // ==================== Billing Operations ==================== + + public CompletableFuture getUsage(BillingPeriod period) { + String path = "/billing/usage"; + if (period != null) { + path += "?period=" + period.name().toLowerCase(); + } + return request("GET", path, null) + .thenApply(resp -> gson.fromJson(resp, Usage.class)); + } + + public CompletableFuture getUsageByDateRange(long startDate, long endDate) { + String path = "/billing/usage?startDate=" + startDate + "&endDate=" + endDate; + return request("GET", path, null) + .thenApply(resp -> gson.fromJson(resp, Usage.class)); + } + + public CompletableFuture> getInvoices() { + return request("GET", "/billing/invoices", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + InvoicesResponse result = gson.fromJson(resp, type); + return result.invoices; + }); + } + + public CompletableFuture getInvoice(String invoiceId) { + return request("GET", "/billing/invoices/" + encode(invoiceId), null) + .thenApply(resp -> gson.fromJson(resp, Invoice.class)); + } + + public CompletableFuture downloadInvoice(String invoiceId) { + return request("GET", "/billing/invoices/" + encode(invoiceId) + "/pdf", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + String base64 = (String) map.get("data"); + return java.util.Base64.getDecoder().decode(base64); + }); + } + + public CompletableFuture getBalance() { + return request("GET", "/billing/balance", null) + .thenApply(resp -> gson.fromJson(resp, AccountBalance.class)); + } + + public CompletableFuture addCredits(String amount, String paymentMethod) { + Map body = new HashMap<>(); + body.put("amount", amount); + body.put("paymentMethod", paymentMethod); + return request("POST", "/billing/credits", body) + .thenApply(resp -> null); + } + + // ==================== Staking Operations ==================== + + public CompletableFuture stake(String amount, Integer durationDays) { + Map body = new HashMap<>(); + body.put("amount", amount); + if (durationDays != null) { + body.put("durationDays", durationDays); + } + return request("POST", "/staking/stake", body) + .thenApply(resp -> gson.fromJson(resp, StakeReceipt.class)); + } + + public CompletableFuture unstake(String stakeId) { + return request("POST", "/staking/unstake/" + encode(stakeId), null) + .thenApply(resp -> gson.fromJson(resp, UnstakeReceipt.class)); + } + + public CompletableFuture> getStakes() { + return request("GET", "/staking/stakes", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + StakesResponse result = gson.fromJson(resp, type); + return result.stakes; + }); + } + + public CompletableFuture getStake(String stakeId) { + return request("GET", "/staking/stakes/" + encode(stakeId), null) + .thenApply(resp -> gson.fromJson(resp, StakeInfo.class)); + } + + public CompletableFuture getStakingRewards() { + return request("GET", "/staking/rewards", null) + .thenApply(resp -> gson.fromJson(resp, Rewards.class)); + } + + public CompletableFuture claimRewards() { + return request("POST", "/staking/rewards/claim", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return (String) map.get("txHash"); + }); + } + + public CompletableFuture getCurrentApy() { + return request("GET", "/staking/apy", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return ((Number) map.get("apy")).doubleValue(); + }); + } + + // ==================== Discount Operations ==================== + + public CompletableFuture applyDiscount(String code) { + Map body = new HashMap<>(); + body.put("code", code); + return request("POST", "/discounts/apply", body) + .thenApply(resp -> gson.fromJson(resp, AppliedDiscount.class)); + } + + public CompletableFuture> getAvailableDiscounts() { + return request("GET", "/discounts/available", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + DiscountsResponse result = gson.fromJson(resp, type); + return result.discounts; + }); + } + + public CompletableFuture validateDiscount(String code) { + return request("GET", "/discounts/validate/" + encode(code), null) + .thenApply(resp -> gson.fromJson(resp, Discount.class)); + } + + public CompletableFuture> getAppliedDiscounts() { + return request("GET", "/discounts/applied", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + AppliedDiscountsResponse result = gson.fromJson(resp, type); + return result.discounts; + }); + } + + public CompletableFuture removeDiscount(String discountId) { + return request("DELETE", "/discounts/" + encode(discountId), null) + .thenApply(resp -> null); + } + + // ==================== Lifecycle ==================== + + @Override + public void close() { + closed.set(true); + } + + public boolean isClosed() { + return closed.get(); + } + + public CompletableFuture healthCheck() { + return request("GET", "/health", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return "healthy".equals(map.get("status")); + }) + .exceptionally(e -> false); + } + + // ==================== Private Methods ==================== + + private CompletableFuture request(String method, String path, Object body) { + if (closed.get()) { + return CompletableFuture.failedFuture(new EconomicsException("Client has been closed")); + } + return doRequest(method, path, body, 0); + } + + private CompletableFuture doRequest(String method, String path, Object body, int attempt) { + String url = config.getEndpoint() + path; + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .header("X-SDK-Version", "java/0.1.0") + .timeout(Duration.ofSeconds(config.getTimeoutSecs())); + + HttpRequest.BodyPublisher bodyPub = body != null + ? HttpRequest.BodyPublishers.ofString(gson.toJson(body)) + : HttpRequest.BodyPublishers.noBody(); + + switch (method) { + case "GET": builder.GET(); break; + case "POST": builder.POST(bodyPub); break; + case "PUT": builder.PUT(bodyPub); break; + case "DELETE": builder.method("DELETE", bodyPub); break; + } + + return httpClient.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose(response -> { + if (response.statusCode() >= 400) { + Map errorBody = gson.fromJson(response.body(), + new TypeToken>(){}.getType()); + String message = errorBody != null && errorBody.get("message") != null + ? (String) errorBody.get("message") : "HTTP " + response.statusCode(); + String code = errorBody != null ? (String) errorBody.get("code") : null; + return CompletableFuture.failedFuture( + new EconomicsException(message, code, response.statusCode())); + } + return CompletableFuture.completedFuture(response.body()); + }) + .exceptionally(e -> { + if (attempt < config.getRetries() - 1) { + sleep((long) Math.pow(2, attempt) * 1000); + return doRequest(method, path, body, attempt + 1).join(); + } + throw new RuntimeException(e); + }); + } + + private static String encode(String value) { + return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); + } + + private static void sleep(long ms) { + try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + } + + // Helper response types + private static class TiersResponse { List tiers; } + private static class InvoicesResponse { List invoices; } + private static class StakesResponse { List stakes; } + private static class DiscountsResponse { List discounts; } + private static class AppliedDiscountsResponse { List discounts; } +} diff --git a/sdk/java/src/main/java/io/synor/economics/Types.java b/sdk/java/src/main/java/io/synor/economics/Types.java new file mode 100644 index 0000000..7bc4364 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/economics/Types.java @@ -0,0 +1,383 @@ +package io.synor.economics; + +import java.util.List; + +/** + * Economics SDK Types for Java. + */ +public class Types { + // Organizational class +} + +// ==================== Enums ==================== + +enum ServiceType { compute, storage, database, hosting, bridge, mining, rpc } +enum BillingPeriod { daily, weekly, monthly, yearly } +enum StakeStatus { active, unstaking, unlocked, slashed } +enum DiscountType { percentage, fixed, volume } + +// ==================== Config ==================== + +class EconomicsConfig { + private final String apiKey; + private final String endpoint; + private final int timeoutSecs; + private final int retries; + private final boolean debug; + + public EconomicsConfig(String apiKey) { + this(apiKey, "https://economics.synor.io/v1", 60, 3, false); + } + + public EconomicsConfig(String apiKey, String endpoint, int timeoutSecs, int retries, boolean debug) { + this.apiKey = apiKey; + this.endpoint = endpoint; + this.timeoutSecs = timeoutSecs; + this.retries = retries; + this.debug = debug; + } + + public String getApiKey() { return apiKey; } + public String getEndpoint() { return endpoint; } + public int getTimeoutSecs() { return timeoutSecs; } + public int getRetries() { return retries; } + public boolean isDebug() { return debug; } +} + +// ==================== Pricing Types ==================== + +class UsageMetrics { + private double computeHours; + private double storageGb; + private long requests; + private double bandwidthGb; + private double gpuHours; + + public double getComputeHours() { return computeHours; } + public double getStorageGb() { return storageGb; } + public long getRequests() { return requests; } + public double getBandwidthGb() { return bandwidthGb; } + public double getGpuHours() { return gpuHours; } + + public void setComputeHours(double computeHours) { this.computeHours = computeHours; } + public void setStorageGb(double storageGb) { this.storageGb = storageGb; } + public void setRequests(long requests) { this.requests = requests; } + public void setBandwidthGb(double bandwidthGb) { this.bandwidthGb = bandwidthGb; } + public void setGpuHours(double gpuHours) { this.gpuHours = gpuHours; } +} + +class PricingTier { + private String name; + private double upTo; + private double pricePerUnit; + private String unit; + + public String getName() { return name; } + public double getUpTo() { return upTo; } + public double getPricePerUnit() { return pricePerUnit; } + public String getUnit() { return unit; } +} + +class Price { + private ServiceType service; + private String amount; + private String currency; + private String amountUsd; + private List breakdown; + private String discount; + private String finalAmount; + + public ServiceType getService() { return service; } + public String getAmount() { return amount; } + public String getCurrency() { return currency; } + public String getAmountUsd() { return amountUsd; } + public List getBreakdown() { return breakdown; } + public String getDiscount() { return discount; } + public String getFinalAmount() { return finalAmount; } +} + +class UsagePlan { + private ServiceType service; + private UsageMetrics estimatedUsage; + private BillingPeriod period; + + public ServiceType getService() { return service; } + public UsageMetrics getEstimatedUsage() { return estimatedUsage; } + public BillingPeriod getPeriod() { return period; } + + public void setService(ServiceType service) { this.service = service; } + public void setEstimatedUsage(UsageMetrics estimatedUsage) { this.estimatedUsage = estimatedUsage; } + public void setPeriod(BillingPeriod period) { this.period = period; } +} + +class CostEstimate { + private ServiceType service; + private BillingPeriod period; + private String estimatedCost; + private String estimatedCostUsd; + private String currency; + private String confidenceLevel; + private List breakdown; + private String savingsFromStaking; + + public ServiceType getService() { return service; } + public BillingPeriod getPeriod() { return period; } + public String getEstimatedCost() { return estimatedCost; } + public String getEstimatedCostUsd() { return estimatedCostUsd; } + public String getCurrency() { return currency; } + public String getConfidenceLevel() { return confidenceLevel; } + public List getBreakdown() { return breakdown; } + public String getSavingsFromStaking() { return savingsFromStaking; } +} + +class CostBreakdown { + private String category; + private String amount; + private double percentage; + + public String getCategory() { return category; } + public String getAmount() { return amount; } + public double getPercentage() { return percentage; } +} + +// ==================== Billing Types ==================== + +class UsageRecord { + private ServiceType service; + private String resource; + private double quantity; + private String unit; + private long timestamp; + private String cost; + + public ServiceType getService() { return service; } + public String getResource() { return resource; } + public double getQuantity() { return quantity; } + public String getUnit() { return unit; } + public long getTimestamp() { return timestamp; } + public String getCost() { return cost; } +} + +class Usage { + private BillingPeriod period; + private long startDate; + private long endDate; + private List records; + private String totalCost; + private String currency; + + public BillingPeriod getPeriod() { return period; } + public long getStartDate() { return startDate; } + public long getEndDate() { return endDate; } + public List getRecords() { return records; } + public String getTotalCost() { return totalCost; } + public String getCurrency() { return currency; } +} + +class InvoiceLineItem { + private String description; + private double quantity; + private String unit; + private String unitPrice; + private String amount; + + public String getDescription() { return description; } + public double getQuantity() { return quantity; } + public String getUnit() { return unit; } + public String getUnitPrice() { return unitPrice; } + public String getAmount() { return amount; } +} + +class Invoice { + private String id; + private String number; + private long periodStart; + private long periodEnd; + private String subtotal; + private String tax; + private String discount; + private String total; + private String currency; + private String status; + private Long dueDate; + private Long paidAt; + private List lineItems; + private String pdfUrl; + + public String getId() { return id; } + public String getNumber() { return number; } + public long getPeriodStart() { return periodStart; } + public long getPeriodEnd() { return periodEnd; } + public String getSubtotal() { return subtotal; } + public String getTax() { return tax; } + public String getDiscount() { return discount; } + public String getTotal() { return total; } + public String getCurrency() { return currency; } + public String getStatus() { return status; } + public Long getDueDate() { return dueDate; } + public Long getPaidAt() { return paidAt; } + public List getLineItems() { return lineItems; } + public String getPdfUrl() { return pdfUrl; } +} + +class AccountBalance { + private String available; + private String pending; + private String reserved; + private String staked; + private String currency; + private long lastUpdated; + + public String getAvailable() { return available; } + public String getPending() { return pending; } + public String getReserved() { return reserved; } + public String getStaked() { return staked; } + public String getCurrency() { return currency; } + public long getLastUpdated() { return lastUpdated; } +} + +// ==================== Staking Types ==================== + +class StakeReceipt { + private String id; + private String txHash; + private String amount; + private long startDate; + private long endDate; + private double apy; + private StakeStatus status; + + public String getId() { return id; } + public String getTxHash() { return txHash; } + public String getAmount() { return amount; } + public long getStartDate() { return startDate; } + public long getEndDate() { return endDate; } + public double getApy() { return apy; } + public StakeStatus getStatus() { return status; } +} + +class StakeInfo { + private String id; + private String amount; + private StakeStatus status; + private long startDate; + private long endDate; + private double apy; + private String earnedRewards; + private String pendingRewards; + private Long unlockDate; + private int discountPercent; + + public String getId() { return id; } + public String getAmount() { return amount; } + public StakeStatus getStatus() { return status; } + public long getStartDate() { return startDate; } + public long getEndDate() { return endDate; } + public double getApy() { return apy; } + public String getEarnedRewards() { return earnedRewards; } + public String getPendingRewards() { return pendingRewards; } + public Long getUnlockDate() { return unlockDate; } + public int getDiscountPercent() { return discountPercent; } +} + +class UnstakeReceipt { + private String id; + private String txHash; + private String amount; + private long initiatedAt; + private long availableAt; + private String penalty; + + public String getId() { return id; } + public String getTxHash() { return txHash; } + public String getAmount() { return amount; } + public long getInitiatedAt() { return initiatedAt; } + public long getAvailableAt() { return availableAt; } + public String getPenalty() { return penalty; } +} + +class Rewards { + private String totalEarned; + private String pendingClaim; + private String lastClaimDate; + private double currentApy; + private List history; + + public String getTotalEarned() { return totalEarned; } + public String getPendingClaim() { return pendingClaim; } + public String getLastClaimDate() { return lastClaimDate; } + public double getCurrentApy() { return currentApy; } + public List getHistory() { return history; } +} + +class RewardRecord { + private long date; + private String amount; + private String type; + private String txHash; + + public long getDate() { return date; } + public String getAmount() { return amount; } + public String getType() { return type; } + public String getTxHash() { return txHash; } +} + +// ==================== Discount Types ==================== + +class Discount { + private String id; + private String code; + private DiscountType type; + private String value; + private Long validFrom; + private Long validUntil; + private Integer usageLimit; + private int usageCount; + private List applicableServices; + private String minimumSpend; + private boolean active; + + public String getId() { return id; } + public String getCode() { return code; } + public DiscountType getType() { return type; } + public String getValue() { return value; } + public Long getValidFrom() { return validFrom; } + public Long getValidUntil() { return validUntil; } + public Integer getUsageLimit() { return usageLimit; } + public int getUsageCount() { return usageCount; } + public List getApplicableServices() { return applicableServices; } + public String getMinimumSpend() { return minimumSpend; } + public boolean isActive() { return active; } +} + +class AppliedDiscount { + private Discount discount; + private String savedAmount; + private long appliedAt; + + public Discount getDiscount() { return discount; } + public String getSavedAmount() { return savedAmount; } + public long getAppliedAt() { return appliedAt; } +} + +// ==================== Error ==================== + +class EconomicsException extends RuntimeException { + private final String code; + private final int statusCode; + + public EconomicsException(String message) { + super(message); + this.code = null; + this.statusCode = 0; + } + + public EconomicsException(String message, String code, int statusCode) { + super(message); + this.code = code; + this.statusCode = statusCode; + } + + public String getCode() { return code; } + public int getStatusCode() { return statusCode; } +} diff --git a/sdk/java/src/main/java/io/synor/governance/SynorGovernance.java b/sdk/java/src/main/java/io/synor/governance/SynorGovernance.java new file mode 100644 index 0000000..ad4e2e5 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/governance/SynorGovernance.java @@ -0,0 +1,369 @@ +package io.synor.governance; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Synor Governance SDK for Java. + * Proposals, voting, DAOs, and vesting operations. + */ +public class SynorGovernance implements AutoCloseable { + private static final String DEFAULT_ENDPOINT = "https://governance.synor.io/v1"; + private static final Gson gson = new GsonBuilder().create(); + private static final Set FINAL_STATUSES = Set.of( + ProposalStatus.passed, ProposalStatus.rejected, ProposalStatus.executed, ProposalStatus.cancelled + ); + + private final GovernanceConfig config; + private final HttpClient httpClient; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public SynorGovernance(String apiKey) { + this(new GovernanceConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false)); + } + + public SynorGovernance(GovernanceConfig config) { + this.config = config; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(config.getTimeoutSecs())) + .build(); + } + + // ==================== Proposal Operations ==================== + + public CompletableFuture createProposal(ProposalDraft proposal) { + Map body = new HashMap<>(); + body.put("title", proposal.getTitle()); + body.put("description", proposal.getDescription()); + if (proposal.getDiscussionUrl() != null) body.put("discussionUrl", proposal.getDiscussionUrl()); + if (proposal.getVotingStartTime() != null) body.put("votingStartTime", proposal.getVotingStartTime()); + if (proposal.getVotingEndTime() != null) body.put("votingEndTime", proposal.getVotingEndTime()); + if (proposal.getActions() != null) body.put("actions", proposal.getActions()); + if (proposal.getDaoId() != null) body.put("daoId", proposal.getDaoId()); + return request("POST", "/proposals", body) + .thenApply(resp -> gson.fromJson(resp, Proposal.class)); + } + + public CompletableFuture getProposal(String proposalId) { + return request("GET", "/proposals/" + encode(proposalId), null) + .thenApply(resp -> gson.fromJson(resp, Proposal.class)); + } + + public CompletableFuture> listProposals(ProposalFilter filter) { + StringBuilder path = new StringBuilder("/proposals"); + if (filter != null) { + StringBuilder params = new StringBuilder(); + if (filter.getStatus() != null) params.append("status=").append(filter.getStatus().name().toLowerCase()); + if (filter.getProposer() != null) { + if (params.length() > 0) params.append("&"); + params.append("proposer=").append(encode(filter.getProposer())); + } + if (filter.getDaoId() != null) { + if (params.length() > 0) params.append("&"); + params.append("daoId=").append(encode(filter.getDaoId())); + } + if (filter.getLimit() != null) { + if (params.length() > 0) params.append("&"); + params.append("limit=").append(filter.getLimit()); + } + if (filter.getOffset() != null) { + if (params.length() > 0) params.append("&"); + params.append("offset=").append(filter.getOffset()); + } + if (params.length() > 0) path.append("?").append(params); + } + return request("GET", path.toString(), null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + ProposalsResponse result = gson.fromJson(resp, type); + return result.proposals; + }); + } + + public CompletableFuture cancelProposal(String proposalId) { + return request("POST", "/proposals/" + encode(proposalId) + "/cancel", null) + .thenApply(resp -> gson.fromJson(resp, Proposal.class)); + } + + public CompletableFuture executeProposal(String proposalId) { + return request("POST", "/proposals/" + encode(proposalId) + "/execute", null) + .thenApply(resp -> gson.fromJson(resp, Proposal.class)); + } + + public CompletableFuture waitForProposal(String proposalId, long pollIntervalMs, long maxWaitMs) { + long deadline = System.currentTimeMillis() + maxWaitMs; + return waitForProposalInternal(proposalId, pollIntervalMs, deadline); + } + + private CompletableFuture waitForProposalInternal(String id, long pollMs, long deadline) { + if (System.currentTimeMillis() >= deadline) { + return CompletableFuture.failedFuture(new GovernanceException("Timeout waiting for proposal completion")); + } + return getProposal(id).thenCompose(proposal -> { + if (FINAL_STATUSES.contains(proposal.getStatus())) { + return CompletableFuture.completedFuture(proposal); + } + return CompletableFuture.runAsync(() -> sleep(pollMs)) + .thenCompose(v -> waitForProposalInternal(id, pollMs, deadline)); + }); + } + + // ==================== Voting Operations ==================== + + public CompletableFuture vote(String proposalId, Vote vote, String weight) { + Map body = new HashMap<>(); + body.put("choice", vote.getChoice().name().toLowerCase()); + if (vote.getReason() != null) body.put("reason", vote.getReason()); + if (weight != null) body.put("weight", weight); + return request("POST", "/proposals/" + encode(proposalId) + "/vote", body) + .thenApply(resp -> gson.fromJson(resp, VoteReceipt.class)); + } + + public CompletableFuture> getVotes(String proposalId) { + return request("GET", "/proposals/" + encode(proposalId) + "/votes", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + VotesResponse result = gson.fromJson(resp, type); + return result.votes; + }); + } + + public CompletableFuture getMyVote(String proposalId) { + return request("GET", "/proposals/" + encode(proposalId) + "/votes/me", null) + .thenApply(resp -> gson.fromJson(resp, VoteReceipt.class)); + } + + public CompletableFuture delegate(String delegatee, String amount) { + Map body = new HashMap<>(); + body.put("delegatee", delegatee); + if (amount != null) body.put("amount", amount); + return request("POST", "/voting/delegate", body) + .thenApply(resp -> gson.fromJson(resp, DelegationReceipt.class)); + } + + public CompletableFuture undelegate(String delegatee) { + return request("POST", "/voting/undelegate", Map.of("delegatee", delegatee)) + .thenApply(resp -> gson.fromJson(resp, DelegationReceipt.class)); + } + + public CompletableFuture getVotingPower(String address) { + return request("GET", "/voting/power/" + encode(address), null) + .thenApply(resp -> gson.fromJson(resp, VotingPower.class)); + } + + public CompletableFuture> getDelegations(String address) { + return request("GET", "/voting/delegations/" + encode(address), null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + DelegationsResponse result = gson.fromJson(resp, type); + return result.delegations; + }); + } + + // ==================== DAO Operations ==================== + + public CompletableFuture createDao(DaoConfig daoConfig) { + Map body = new HashMap<>(); + body.put("name", daoConfig.getName()); + body.put("description", daoConfig.getDescription()); + body.put("type", daoConfig.getType().name().toLowerCase()); + if (daoConfig.getTokenAddress() != null) body.put("tokenAddress", daoConfig.getTokenAddress()); + if (daoConfig.getQuorumPercent() != null) body.put("quorumPercent", daoConfig.getQuorumPercent()); + body.put("votingPeriodDays", daoConfig.getVotingPeriodDays()); + body.put("timelockDays", daoConfig.getTimelockDays()); + if (daoConfig.getProposalThreshold() != null) body.put("proposalThreshold", daoConfig.getProposalThreshold()); + if (daoConfig.getMultisigMembers() != null) body.put("multisigMembers", daoConfig.getMultisigMembers()); + if (daoConfig.getMultisigThreshold() != null) body.put("multisigThreshold", daoConfig.getMultisigThreshold()); + return request("POST", "/daos", body) + .thenApply(resp -> gson.fromJson(resp, Dao.class)); + } + + public CompletableFuture getDao(String daoId) { + return request("GET", "/daos/" + encode(daoId), null) + .thenApply(resp -> gson.fromJson(resp, Dao.class)); + } + + public CompletableFuture> listDaos(Integer limit, Integer offset) { + StringBuilder path = new StringBuilder("/daos"); + if (limit != null || offset != null) { + path.append("?"); + if (limit != null) path.append("limit=").append(limit); + if (offset != null) { + if (limit != null) path.append("&"); + path.append("offset=").append(offset); + } + } + return request("GET", path.toString(), null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + DaosResponse result = gson.fromJson(resp, type); + return result.daos; + }); + } + + public CompletableFuture getDaoTreasury(String daoId) { + return request("GET", "/daos/" + encode(daoId) + "/treasury", null) + .thenApply(resp -> gson.fromJson(resp, DaoTreasury.class)); + } + + public CompletableFuture> getDaoMembers(String daoId) { + return request("GET", "/daos/" + encode(daoId) + "/members", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + MembersResponse result = gson.fromJson(resp, type); + return result.members; + }); + } + + // ==================== Vesting Operations ==================== + + public CompletableFuture createVestingSchedule(VestingSchedule schedule) { + Map body = new HashMap<>(); + body.put("beneficiary", schedule.getBeneficiary()); + body.put("totalAmount", schedule.getTotalAmount()); + body.put("startTime", schedule.getStartTime()); + body.put("cliffDuration", schedule.getCliffDuration()); + body.put("vestingDuration", schedule.getVestingDuration()); + body.put("revocable", schedule.isRevocable()); + return request("POST", "/vesting", body) + .thenApply(resp -> gson.fromJson(resp, VestingContract.class)); + } + + public CompletableFuture getVestingContract(String contractId) { + return request("GET", "/vesting/" + encode(contractId), null) + .thenApply(resp -> gson.fromJson(resp, VestingContract.class)); + } + + public CompletableFuture> listVestingContracts(String beneficiary) { + StringBuilder path = new StringBuilder("/vesting"); + if (beneficiary != null) { + path.append("?beneficiary=").append(encode(beneficiary)); + } + return request("GET", path.toString(), null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + VestingContractsResponse result = gson.fromJson(resp, type); + return result.contracts; + }); + } + + public CompletableFuture claimVested(String contractId) { + return request("POST", "/vesting/" + encode(contractId) + "/claim", null) + .thenApply(resp -> gson.fromJson(resp, ClaimReceipt.class)); + } + + public CompletableFuture revokeVesting(String contractId) { + return request("POST", "/vesting/" + encode(contractId) + "/revoke", null) + .thenApply(resp -> gson.fromJson(resp, VestingContract.class)); + } + + public CompletableFuture getReleasableAmount(String contractId) { + return request("GET", "/vesting/" + encode(contractId) + "/releasable", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return (String) map.get("amount"); + }); + } + + // ==================== Lifecycle ==================== + + @Override + public void close() { + closed.set(true); + } + + public boolean isClosed() { + return closed.get(); + } + + public CompletableFuture healthCheck() { + return request("GET", "/health", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return "healthy".equals(map.get("status")); + }) + .exceptionally(e -> false); + } + + // ==================== Private Methods ==================== + + private CompletableFuture request(String method, String path, Object body) { + if (closed.get()) { + return CompletableFuture.failedFuture(new GovernanceException("Client has been closed")); + } + return doRequest(method, path, body, 0); + } + + private CompletableFuture doRequest(String method, String path, Object body, int attempt) { + String url = config.getEndpoint() + path; + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .header("X-SDK-Version", "java/0.1.0") + .timeout(Duration.ofSeconds(config.getTimeoutSecs())); + + HttpRequest.BodyPublisher bodyPub = body != null + ? HttpRequest.BodyPublishers.ofString(gson.toJson(body)) + : HttpRequest.BodyPublishers.noBody(); + + switch (method) { + case "GET": builder.GET(); break; + case "POST": builder.POST(bodyPub); break; + case "PUT": builder.PUT(bodyPub); break; + case "DELETE": builder.method("DELETE", bodyPub); break; + } + + return httpClient.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose(response -> { + if (response.statusCode() >= 400) { + Map errorBody = gson.fromJson(response.body(), + new TypeToken>(){}.getType()); + String message = errorBody != null && errorBody.get("message") != null + ? (String) errorBody.get("message") : "HTTP " + response.statusCode(); + String code = errorBody != null ? (String) errorBody.get("code") : null; + return CompletableFuture.failedFuture( + new GovernanceException(message, code, response.statusCode())); + } + return CompletableFuture.completedFuture(response.body()); + }) + .exceptionally(e -> { + if (attempt < config.getRetries() - 1) { + sleep((long) Math.pow(2, attempt) * 1000); + return doRequest(method, path, body, attempt + 1).join(); + } + throw new RuntimeException(e); + }); + } + + private static String encode(String value) { + return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); + } + + private static void sleep(long ms) { + try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + } + + // Helper response types + private static class ProposalsResponse { List proposals; } + private static class VotesResponse { List votes; } + private static class DelegationsResponse { List delegations; } + private static class DaosResponse { List daos; } + private static class MembersResponse { List members; } + private static class VestingContractsResponse { List contracts; } +} diff --git a/sdk/java/src/main/java/io/synor/governance/Types.java b/sdk/java/src/main/java/io/synor/governance/Types.java new file mode 100644 index 0000000..3b5b06f --- /dev/null +++ b/sdk/java/src/main/java/io/synor/governance/Types.java @@ -0,0 +1,407 @@ +package io.synor.governance; + +import java.util.List; + +/** + * Governance SDK Types for Java. + */ +public class Types { + // Organizational class +} + +// ==================== Enums ==================== + +enum ProposalStatus { draft, active, passed, rejected, executed, cancelled } +enum VoteChoice { yes, no, abstain } +enum DaoType { token, multisig, hybrid } +enum VestingStatus { pending, active, paused, completed, revoked } + +// ==================== Config ==================== + +class GovernanceConfig { + private final String apiKey; + private final String endpoint; + private final int timeoutSecs; + private final int retries; + private final boolean debug; + + public GovernanceConfig(String apiKey) { + this(apiKey, "https://governance.synor.io/v1", 60, 3, false); + } + + public GovernanceConfig(String apiKey, String endpoint, int timeoutSecs, int retries, boolean debug) { + this.apiKey = apiKey; + this.endpoint = endpoint; + this.timeoutSecs = timeoutSecs; + this.retries = retries; + this.debug = debug; + } + + public String getApiKey() { return apiKey; } + public String getEndpoint() { return endpoint; } + public int getTimeoutSecs() { return timeoutSecs; } + public int getRetries() { return retries; } + public boolean isDebug() { return debug; } +} + +// ==================== Proposal Types ==================== + +class ProposalAction { + private String target; + private String method; + private String data; + private String value; + + public String getTarget() { return target; } + public String getMethod() { return method; } + public String getData() { return data; } + public String getValue() { return value; } + + public void setTarget(String target) { this.target = target; } + public void setMethod(String method) { this.method = method; } + public void setData(String data) { this.data = data; } + public void setValue(String value) { this.value = value; } +} + +class ProposalDraft { + private String title; + private String description; + private String discussionUrl; + private Long votingStartTime; + private Long votingEndTime; + private List actions; + private String daoId; + + public String getTitle() { return title; } + public String getDescription() { return description; } + public String getDiscussionUrl() { return discussionUrl; } + public Long getVotingStartTime() { return votingStartTime; } + public Long getVotingEndTime() { return votingEndTime; } + public List getActions() { return actions; } + public String getDaoId() { return daoId; } + + public void setTitle(String title) { this.title = title; } + public void setDescription(String description) { this.description = description; } + public void setDiscussionUrl(String discussionUrl) { this.discussionUrl = discussionUrl; } + public void setVotingStartTime(Long votingStartTime) { this.votingStartTime = votingStartTime; } + public void setVotingEndTime(Long votingEndTime) { this.votingEndTime = votingEndTime; } + public void setActions(List actions) { this.actions = actions; } + public void setDaoId(String daoId) { this.daoId = daoId; } +} + +class VoteBreakdown { + private String yes; + private String no; + private String abstain; + private String quorum; + private double quorumPercent; + private boolean quorumReached; + + public String getYes() { return yes; } + public String getNo() { return no; } + public String getAbstain() { return abstain; } + public String getQuorum() { return quorum; } + public double getQuorumPercent() { return quorumPercent; } + public boolean isQuorumReached() { return quorumReached; } +} + +class Proposal { + private String id; + private String title; + private String description; + private String proposer; + private ProposalStatus status; + private long createdAt; + private long votingStartTime; + private long votingEndTime; + private VoteBreakdown votes; + private String discussionUrl; + private List actions; + private String daoId; + private String snapshotBlock; + private Long executedAt; + private String executedTxHash; + + public String getId() { return id; } + public String getTitle() { return title; } + public String getDescription() { return description; } + public String getProposer() { return proposer; } + public ProposalStatus getStatus() { return status; } + public long getCreatedAt() { return createdAt; } + public long getVotingStartTime() { return votingStartTime; } + public long getVotingEndTime() { return votingEndTime; } + public VoteBreakdown getVotes() { return votes; } + public String getDiscussionUrl() { return discussionUrl; } + public List getActions() { return actions; } + public String getDaoId() { return daoId; } + public String getSnapshotBlock() { return snapshotBlock; } + public Long getExecutedAt() { return executedAt; } + public String getExecutedTxHash() { return executedTxHash; } +} + +class ProposalFilter { + private ProposalStatus status; + private String proposer; + private String daoId; + private Integer limit; + private Integer offset; + + public ProposalStatus getStatus() { return status; } + public String getProposer() { return proposer; } + public String getDaoId() { return daoId; } + public Integer getLimit() { return limit; } + public Integer getOffset() { return offset; } + + public void setStatus(ProposalStatus status) { this.status = status; } + public void setProposer(String proposer) { this.proposer = proposer; } + public void setDaoId(String daoId) { this.daoId = daoId; } + public void setLimit(Integer limit) { this.limit = limit; } + public void setOffset(Integer offset) { this.offset = offset; } +} + +// ==================== Voting Types ==================== + +class Vote { + private VoteChoice choice; + private String reason; + + public VoteChoice getChoice() { return choice; } + public String getReason() { return reason; } + + public void setChoice(VoteChoice choice) { this.choice = choice; } + public void setReason(String reason) { this.reason = reason; } +} + +class VoteReceipt { + private String id; + private String proposalId; + private String voter; + private VoteChoice choice; + private String weight; + private long timestamp; + private String txHash; + private String reason; + + public String getId() { return id; } + public String getProposalId() { return proposalId; } + public String getVoter() { return voter; } + public VoteChoice getChoice() { return choice; } + public String getWeight() { return weight; } + public long getTimestamp() { return timestamp; } + public String getTxHash() { return txHash; } + public String getReason() { return reason; } +} + +class VotingPower { + private String address; + private String balance; + private String delegatedTo; + private String delegatedFrom; + private String totalPower; + private long blockNumber; + + public String getAddress() { return address; } + public String getBalance() { return balance; } + public String getDelegatedTo() { return delegatedTo; } + public String getDelegatedFrom() { return delegatedFrom; } + public String getTotalPower() { return totalPower; } + public long getBlockNumber() { return blockNumber; } +} + +class DelegationReceipt { + private String id; + private String delegator; + private String delegatee; + private String amount; + private long timestamp; + private String txHash; + + public String getId() { return id; } + public String getDelegator() { return delegator; } + public String getDelegatee() { return delegatee; } + public String getAmount() { return amount; } + public long getTimestamp() { return timestamp; } + public String getTxHash() { return txHash; } +} + +// ==================== DAO Types ==================== + +class DaoConfig { + private String name; + private String description; + private DaoType type; + private String tokenAddress; + private String quorumPercent; + private int votingPeriodDays; + private int timelockDays; + private Integer proposalThreshold; + private List multisigMembers; + private Integer multisigThreshold; + + public String getName() { return name; } + public String getDescription() { return description; } + public DaoType getType() { return type; } + public String getTokenAddress() { return tokenAddress; } + public String getQuorumPercent() { return quorumPercent; } + public int getVotingPeriodDays() { return votingPeriodDays; } + public int getTimelockDays() { return timelockDays; } + public Integer getProposalThreshold() { return proposalThreshold; } + public List getMultisigMembers() { return multisigMembers; } + public Integer getMultisigThreshold() { return multisigThreshold; } + + public void setName(String name) { this.name = name; } + public void setDescription(String description) { this.description = description; } + public void setType(DaoType type) { this.type = type; } + public void setTokenAddress(String tokenAddress) { this.tokenAddress = tokenAddress; } + public void setQuorumPercent(String quorumPercent) { this.quorumPercent = quorumPercent; } + public void setVotingPeriodDays(int votingPeriodDays) { this.votingPeriodDays = votingPeriodDays; } + public void setTimelockDays(int timelockDays) { this.timelockDays = timelockDays; } + public void setProposalThreshold(Integer proposalThreshold) { this.proposalThreshold = proposalThreshold; } + public void setMultisigMembers(List multisigMembers) { this.multisigMembers = multisigMembers; } + public void setMultisigThreshold(Integer multisigThreshold) { this.multisigThreshold = multisigThreshold; } +} + +class DaoTreasury { + private String address; + private String balance; + private String currency; + private List assets; + + public String getAddress() { return address; } + public String getBalance() { return balance; } + public String getCurrency() { return currency; } + public List getAssets() { return assets; } +} + +class TreasuryAsset { + private String address; + private String symbol; + private String balance; + private String valueUsd; + + public String getAddress() { return address; } + public String getSymbol() { return symbol; } + public String getBalance() { return balance; } + public String getValueUsd() { return valueUsd; } +} + +class Dao { + private String id; + private String name; + private String description; + private DaoType type; + private String tokenAddress; + private String governorAddress; + private String timelockAddress; + private DaoTreasury treasury; + private String quorumPercent; + private int votingPeriodDays; + private int timelockDays; + private int proposalCount; + private int memberCount; + private long createdAt; + + public String getId() { return id; } + public String getName() { return name; } + public String getDescription() { return description; } + public DaoType getType() { return type; } + public String getTokenAddress() { return tokenAddress; } + public String getGovernorAddress() { return governorAddress; } + public String getTimelockAddress() { return timelockAddress; } + public DaoTreasury getTreasury() { return treasury; } + public String getQuorumPercent() { return quorumPercent; } + public int getVotingPeriodDays() { return votingPeriodDays; } + public int getTimelockDays() { return timelockDays; } + public int getProposalCount() { return proposalCount; } + public int getMemberCount() { return memberCount; } + public long getCreatedAt() { return createdAt; } +} + +// ==================== Vesting Types ==================== + +class VestingSchedule { + private String beneficiary; + private String totalAmount; + private long startTime; + private long cliffDuration; + private long vestingDuration; + private boolean revocable; + + public String getBeneficiary() { return beneficiary; } + public String getTotalAmount() { return totalAmount; } + public long getStartTime() { return startTime; } + public long getCliffDuration() { return cliffDuration; } + public long getVestingDuration() { return vestingDuration; } + public boolean isRevocable() { return revocable; } + + public void setBeneficiary(String beneficiary) { this.beneficiary = beneficiary; } + public void setTotalAmount(String totalAmount) { this.totalAmount = totalAmount; } + public void setStartTime(long startTime) { this.startTime = startTime; } + public void setCliffDuration(long cliffDuration) { this.cliffDuration = cliffDuration; } + public void setVestingDuration(long vestingDuration) { this.vestingDuration = vestingDuration; } + public void setRevocable(boolean revocable) { this.revocable = revocable; } +} + +class VestingContract { + private String id; + private String contractAddress; + private String beneficiary; + private String totalAmount; + private String releasedAmount; + private String releasableAmount; + private long startTime; + private long cliffEnd; + private long vestingEnd; + private VestingStatus status; + private long createdAt; + private String txHash; + + public String getId() { return id; } + public String getContractAddress() { return contractAddress; } + public String getBeneficiary() { return beneficiary; } + public String getTotalAmount() { return totalAmount; } + public String getReleasedAmount() { return releasedAmount; } + public String getReleasableAmount() { return releasableAmount; } + public long getStartTime() { return startTime; } + public long getCliffEnd() { return cliffEnd; } + public long getVestingEnd() { return vestingEnd; } + public VestingStatus getStatus() { return status; } + public long getCreatedAt() { return createdAt; } + public String getTxHash() { return txHash; } +} + +class ClaimReceipt { + private String vestingContractId; + private String amount; + private String txHash; + private long timestamp; + private String remainingAmount; + + public String getVestingContractId() { return vestingContractId; } + public String getAmount() { return amount; } + public String getTxHash() { return txHash; } + public long getTimestamp() { return timestamp; } + public String getRemainingAmount() { return remainingAmount; } +} + +// ==================== Error ==================== + +class GovernanceException extends RuntimeException { + private final String code; + private final int statusCode; + + public GovernanceException(String message) { + super(message); + this.code = null; + this.statusCode = 0; + } + + public GovernanceException(String message, String code, int statusCode) { + super(message); + this.code = code; + this.statusCode = statusCode; + } + + public String getCode() { return code; } + public int getStatusCode() { return statusCode; } +} diff --git a/sdk/java/src/main/java/io/synor/mining/SynorMining.java b/sdk/java/src/main/java/io/synor/mining/SynorMining.java new file mode 100644 index 0000000..adb5872 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/mining/SynorMining.java @@ -0,0 +1,329 @@ +package io.synor.mining; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Synor Mining SDK for Java. + * Pool connections, block templates, hashrate stats, and GPU management. + */ +public class SynorMining implements AutoCloseable { + private static final String DEFAULT_ENDPOINT = "https://mining.synor.io/v1"; + private static final Gson gson = new GsonBuilder().create(); + + private final MiningConfig config; + private final HttpClient httpClient; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final AtomicReference activeConnection = new AtomicReference<>(); + + public SynorMining(String apiKey) { + this(new MiningConfig(apiKey, DEFAULT_ENDPOINT, 60, 3, false)); + } + + public SynorMining(MiningConfig config) { + this.config = config; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(config.getTimeoutSecs())) + .build(); + } + + // ==================== Pool Operations ==================== + + public CompletableFuture connect(PoolConfig pool) { + Map body = new HashMap<>(); + body.put("url", pool.getUrl()); + body.put("user", pool.getUser()); + if (pool.getPassword() != null) body.put("password", pool.getPassword()); + if (pool.getAlgorithm() != null) body.put("algorithm", pool.getAlgorithm()); + if (pool.getDifficulty() != null) body.put("difficulty", pool.getDifficulty()); + return request("POST", "/pool/connect", body) + .thenApply(resp -> { + StratumConnection conn = gson.fromJson(resp, StratumConnection.class); + activeConnection.set(conn); + return conn; + }); + } + + public CompletableFuture disconnect() { + StratumConnection conn = activeConnection.get(); + if (conn == null) { + return CompletableFuture.completedFuture(null); + } + return request("POST", "/pool/disconnect", null) + .thenApply(resp -> { + activeConnection.set(null); + return null; + }); + } + + public CompletableFuture getConnection() { + return request("GET", "/pool/connection", null) + .thenApply(resp -> { + StratumConnection conn = gson.fromJson(resp, StratumConnection.class); + activeConnection.set(conn); + return conn; + }); + } + + public CompletableFuture getPoolStats() { + return request("GET", "/pool/stats", null) + .thenApply(resp -> gson.fromJson(resp, PoolStats.class)); + } + + public boolean isConnected() { + StratumConnection conn = activeConnection.get(); + return conn != null && conn.getStatus() == ConnectionStatus.connected; + } + + // ==================== Mining Operations ==================== + + public CompletableFuture getBlockTemplate() { + return request("GET", "/mining/template", null) + .thenApply(resp -> gson.fromJson(resp, BlockTemplate.class)); + } + + public CompletableFuture submitWork(MinedWork work) { + Map body = new HashMap<>(); + body.put("templateId", work.getTemplateId()); + body.put("nonce", work.getNonce()); + body.put("extraNonce", work.getExtraNonce()); + body.put("timestamp", work.getTimestamp()); + body.put("hash", work.getHash()); + return request("POST", "/mining/submit", body) + .thenApply(resp -> gson.fromJson(resp, SubmitResult.class)); + } + + public CompletableFuture startMining(String algorithm) { + Map body = new HashMap<>(); + if (algorithm != null) body.put("algorithm", algorithm); + return request("POST", "/mining/start", body) + .thenApply(resp -> null); + } + + public CompletableFuture stopMining() { + return request("POST", "/mining/stop", null) + .thenApply(resp -> null); + } + + public CompletableFuture isMining() { + return request("GET", "/mining/status", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return Boolean.TRUE.equals(map.get("mining")); + }); + } + + // ==================== Stats Operations ==================== + + public CompletableFuture getHashrate() { + return request("GET", "/stats/hashrate", null) + .thenApply(resp -> gson.fromJson(resp, Hashrate.class)); + } + + public CompletableFuture getStats() { + return request("GET", "/stats", null) + .thenApply(resp -> gson.fromJson(resp, MiningStats.class)); + } + + public CompletableFuture getEarnings(TimePeriod period) { + String path = "/stats/earnings"; + if (period != null) { + path += "?period=" + period.name().toLowerCase(); + } + return request("GET", path, null) + .thenApply(resp -> gson.fromJson(resp, Earnings.class)); + } + + public CompletableFuture getShareStats() { + return request("GET", "/stats/shares", null) + .thenApply(resp -> gson.fromJson(resp, ShareStats.class)); + } + + // ==================== Device Operations ==================== + + public CompletableFuture> listDevices() { + return request("GET", "/devices", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + DevicesResponse result = gson.fromJson(resp, type); + return result.devices; + }); + } + + public CompletableFuture getDevice(String deviceId) { + return request("GET", "/devices/" + encode(deviceId), null) + .thenApply(resp -> gson.fromJson(resp, MiningDevice.class)); + } + + public CompletableFuture setDeviceConfig(String deviceId, DeviceConfig deviceConfig) { + Map body = new HashMap<>(); + body.put("enabled", deviceConfig.isEnabled()); + if (deviceConfig.getIntensity() != null) body.put("intensity", deviceConfig.getIntensity()); + if (deviceConfig.getPowerLimit() != null) body.put("powerLimit", deviceConfig.getPowerLimit()); + if (deviceConfig.getCoreClockOffset() != null) body.put("coreClockOffset", deviceConfig.getCoreClockOffset()); + if (deviceConfig.getMemoryClockOffset() != null) body.put("memoryClockOffset", deviceConfig.getMemoryClockOffset()); + if (deviceConfig.getFanSpeed() != null) body.put("fanSpeed", deviceConfig.getFanSpeed()); + return request("PUT", "/devices/" + encode(deviceId) + "/config", body) + .thenApply(resp -> gson.fromJson(resp, MiningDevice.class)); + } + + public CompletableFuture enableDevice(String deviceId) { + DeviceConfig cfg = new DeviceConfig(); + cfg.setEnabled(true); + return setDeviceConfig(deviceId, cfg).thenApply(d -> null); + } + + public CompletableFuture disableDevice(String deviceId) { + DeviceConfig cfg = new DeviceConfig(); + cfg.setEnabled(false); + return setDeviceConfig(deviceId, cfg).thenApply(d -> null); + } + + // ==================== Worker Operations ==================== + + public CompletableFuture> listWorkers() { + return request("GET", "/workers", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + WorkersResponse result = gson.fromJson(resp, type); + return result.workers; + }); + } + + public CompletableFuture getWorker(String workerId) { + return request("GET", "/workers/" + encode(workerId), null) + .thenApply(resp -> gson.fromJson(resp, WorkerInfo.class)); + } + + public CompletableFuture removeWorker(String workerId) { + return request("DELETE", "/workers/" + encode(workerId), null) + .thenApply(resp -> null); + } + + // ==================== Algorithm Operations ==================== + + public CompletableFuture> listAlgorithms() { + return request("GET", "/algorithms", null) + .thenApply(resp -> { + Type type = new TypeToken(){}.getType(); + AlgorithmsResponse result = gson.fromJson(resp, type); + return result.algorithms; + }); + } + + public CompletableFuture getAlgorithm(String name) { + return request("GET", "/algorithms/" + encode(name), null) + .thenApply(resp -> gson.fromJson(resp, MiningAlgorithm.class)); + } + + public CompletableFuture switchAlgorithm(String algorithm) { + return request("POST", "/algorithms/switch", Map.of("algorithm", algorithm)) + .thenApply(resp -> null); + } + + public CompletableFuture getMostProfitable() { + return request("GET", "/algorithms/profitable", null) + .thenApply(resp -> gson.fromJson(resp, MiningAlgorithm.class)); + } + + // ==================== Lifecycle ==================== + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + activeConnection.set(null); + } + } + + public boolean isClosed() { + return closed.get(); + } + + public CompletableFuture healthCheck() { + return request("GET", "/health", null) + .thenApply(resp -> { + Map map = gson.fromJson(resp, new TypeToken>(){}.getType()); + return "healthy".equals(map.get("status")); + }) + .exceptionally(e -> false); + } + + // ==================== Private Methods ==================== + + private CompletableFuture request(String method, String path, Object body) { + if (closed.get()) { + return CompletableFuture.failedFuture(new MiningException("Client has been closed")); + } + return doRequest(method, path, body, 0); + } + + private CompletableFuture doRequest(String method, String path, Object body, int attempt) { + String url = config.getEndpoint() + path; + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .header("X-SDK-Version", "java/0.1.0") + .timeout(Duration.ofSeconds(config.getTimeoutSecs())); + + HttpRequest.BodyPublisher bodyPub = body != null + ? HttpRequest.BodyPublishers.ofString(gson.toJson(body)) + : HttpRequest.BodyPublishers.noBody(); + + switch (method) { + case "GET": builder.GET(); break; + case "POST": builder.POST(bodyPub); break; + case "PUT": builder.PUT(bodyPub); break; + case "DELETE": builder.method("DELETE", bodyPub); break; + } + + return httpClient.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString()) + .thenCompose(response -> { + if (response.statusCode() >= 400) { + Map errorBody = gson.fromJson(response.body(), + new TypeToken>(){}.getType()); + String message = errorBody != null && errorBody.get("message") != null + ? (String) errorBody.get("message") : "HTTP " + response.statusCode(); + String code = errorBody != null ? (String) errorBody.get("code") : null; + return CompletableFuture.failedFuture( + new MiningException(message, code, response.statusCode())); + } + return CompletableFuture.completedFuture(response.body()); + }) + .exceptionally(e -> { + if (attempt < config.getRetries() - 1) { + sleep((long) Math.pow(2, attempt) * 1000); + return doRequest(method, path, body, attempt + 1).join(); + } + throw new RuntimeException(e); + }); + } + + private static String encode(String value) { + return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); + } + + private static void sleep(long ms) { + try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + } + + // Helper response types + private static class DevicesResponse { List devices; } + private static class WorkersResponse { List workers; } + private static class AlgorithmsResponse { List algorithms; } +} diff --git a/sdk/java/src/main/java/io/synor/mining/Types.java b/sdk/java/src/main/java/io/synor/mining/Types.java new file mode 100644 index 0000000..474bc39 --- /dev/null +++ b/sdk/java/src/main/java/io/synor/mining/Types.java @@ -0,0 +1,425 @@ +package io.synor.mining; + +import java.util.List; + +/** + * Mining SDK Types for Java. + */ +public class Types { + // Organizational class +} + +// ==================== Enums ==================== + +enum DeviceType { cpu, gpu_nvidia, gpu_amd, asic } +enum DeviceStatus { idle, mining, error, offline } +enum ConnectionStatus { disconnected, connecting, connected, reconnecting } +enum TimePeriod { hour, day, week, month, all } +enum SubmitResultStatus { accepted, rejected, stale } + +// ==================== Config ==================== + +class MiningConfig { + private final String apiKey; + private final String endpoint; + private final int timeoutSecs; + private final int retries; + private final boolean debug; + + public MiningConfig(String apiKey) { + this(apiKey, "https://mining.synor.io/v1", 60, 3, false); + } + + public MiningConfig(String apiKey, String endpoint, int timeoutSecs, int retries, boolean debug) { + this.apiKey = apiKey; + this.endpoint = endpoint; + this.timeoutSecs = timeoutSecs; + this.retries = retries; + this.debug = debug; + } + + public String getApiKey() { return apiKey; } + public String getEndpoint() { return endpoint; } + public int getTimeoutSecs() { return timeoutSecs; } + public int getRetries() { return retries; } + public boolean isDebug() { return debug; } +} + +// ==================== Pool Types ==================== + +class PoolConfig { + private String url; + private String user; + private String password; + private String algorithm; + private Double difficulty; + + public String getUrl() { return url; } + public String getUser() { return user; } + public String getPassword() { return password; } + public String getAlgorithm() { return algorithm; } + public Double getDifficulty() { return difficulty; } + + public void setUrl(String url) { this.url = url; } + public void setUser(String user) { this.user = user; } + public void setPassword(String password) { this.password = password; } + public void setAlgorithm(String algorithm) { this.algorithm = algorithm; } + public void setDifficulty(Double difficulty) { this.difficulty = difficulty; } +} + +class StratumConnection { + private String id; + private String pool; + private ConnectionStatus status; + private String algorithm; + private double difficulty; + private long connectedAt; + private int acceptedShares; + private int rejectedShares; + private int staleShares; + private Long lastShareAt; + + public String getId() { return id; } + public String getPool() { return pool; } + public ConnectionStatus getStatus() { return status; } + public String getAlgorithm() { return algorithm; } + public double getDifficulty() { return difficulty; } + public long getConnectedAt() { return connectedAt; } + public int getAcceptedShares() { return acceptedShares; } + public int getRejectedShares() { return rejectedShares; } + public int getStaleShares() { return staleShares; } + public Long getLastShareAt() { return lastShareAt; } +} + +class PoolStats { + private String url; + private int workers; + private double hashrate; + private double difficulty; + private long lastBlock; + private int blocksFound24h; + private double luck; + + public String getUrl() { return url; } + public int getWorkers() { return workers; } + public double getHashrate() { return hashrate; } + public double getDifficulty() { return difficulty; } + public long getLastBlock() { return lastBlock; } + public int getBlocksFound24h() { return blocksFound24h; } + public double getLuck() { return luck; } +} + +// ==================== Mining Types ==================== + +class TemplateTransaction { + private String txid; + private String data; + private String fee; + private int weight; + + public String getTxid() { return txid; } + public String getData() { return data; } + public String getFee() { return fee; } + public int getWeight() { return weight; } +} + +class BlockTemplate { + private String id; + private String previousBlockHash; + private String merkleRoot; + private long timestamp; + private String bits; + private long height; + private String coinbaseValue; + private List transactions; + private String target; + private String algorithm; + private String extraNonce; + + public String getId() { return id; } + public String getPreviousBlockHash() { return previousBlockHash; } + public String getMerkleRoot() { return merkleRoot; } + public long getTimestamp() { return timestamp; } + public String getBits() { return bits; } + public long getHeight() { return height; } + public String getCoinbaseValue() { return coinbaseValue; } + public List getTransactions() { return transactions; } + public String getTarget() { return target; } + public String getAlgorithm() { return algorithm; } + public String getExtraNonce() { return extraNonce; } +} + +class MinedWork { + private String templateId; + private String nonce; + private String extraNonce; + private long timestamp; + private String hash; + + public String getTemplateId() { return templateId; } + public String getNonce() { return nonce; } + public String getExtraNonce() { return extraNonce; } + public long getTimestamp() { return timestamp; } + public String getHash() { return hash; } + + public void setTemplateId(String templateId) { this.templateId = templateId; } + public void setNonce(String nonce) { this.nonce = nonce; } + public void setExtraNonce(String extraNonce) { this.extraNonce = extraNonce; } + public void setTimestamp(long timestamp) { this.timestamp = timestamp; } + public void setHash(String hash) { this.hash = hash; } +} + +class ShareInfo { + private String hash; + private double difficulty; + private long timestamp; + private boolean accepted; + + public String getHash() { return hash; } + public double getDifficulty() { return difficulty; } + public long getTimestamp() { return timestamp; } + public boolean isAccepted() { return accepted; } +} + +class SubmitResult { + private SubmitResultStatus status; + private ShareInfo share; + private boolean blockFound; + private String reason; + private String blockHash; + private String reward; + + public SubmitResultStatus getStatus() { return status; } + public ShareInfo getShare() { return share; } + public boolean isBlockFound() { return blockFound; } + public String getReason() { return reason; } + public String getBlockHash() { return blockHash; } + public String getReward() { return reward; } +} + +// ==================== Stats Types ==================== + +class Hashrate { + private double current; + private double average1h; + private double average24h; + private double peak; + private String unit; + + public double getCurrent() { return current; } + public double getAverage1h() { return average1h; } + public double getAverage24h() { return average24h; } + public double getPeak() { return peak; } + public String getUnit() { return unit; } +} + +class ShareStats { + private int accepted; + private int rejected; + private int stale; + private int total; + private double acceptRate; + + public int getAccepted() { return accepted; } + public int getRejected() { return rejected; } + public int getStale() { return stale; } + public int getTotal() { return total; } + public double getAcceptRate() { return acceptRate; } +} + +class DeviceTemperature { + private double current; + private double max; + private boolean throttling; + + public double getCurrent() { return current; } + public double getMax() { return max; } + public boolean isThrottling() { return throttling; } +} + +class EarningsSnapshot { + private String today; + private String yesterday; + private String thisWeek; + private String thisMonth; + private String total; + private String currency; + + public String getToday() { return today; } + public String getYesterday() { return yesterday; } + public String getThisWeek() { return thisWeek; } + public String getThisMonth() { return thisMonth; } + public String getTotal() { return total; } + public String getCurrency() { return currency; } +} + +class MiningStats { + private Hashrate hashrate; + private ShareStats shares; + private long uptime; + private double efficiency; + private EarningsSnapshot earnings; + private Double powerConsumption; + private DeviceTemperature temperature; + + public Hashrate getHashrate() { return hashrate; } + public ShareStats getShares() { return shares; } + public long getUptime() { return uptime; } + public double getEfficiency() { return efficiency; } + public EarningsSnapshot getEarnings() { return earnings; } + public Double getPowerConsumption() { return powerConsumption; } + public DeviceTemperature getTemperature() { return temperature; } +} + +class EarningsBreakdown { + private long date; + private String amount; + private int blocks; + private int shares; + private double hashrate; + + public long getDate() { return date; } + public String getAmount() { return amount; } + public int getBlocks() { return blocks; } + public int getShares() { return shares; } + public double getHashrate() { return hashrate; } +} + +class Earnings { + private TimePeriod period; + private long startDate; + private long endDate; + private String amount; + private int blocks; + private int shares; + private double averageHashrate; + private String currency; + private List breakdown; + + public TimePeriod getPeriod() { return period; } + public long getStartDate() { return startDate; } + public long getEndDate() { return endDate; } + public String getAmount() { return amount; } + public int getBlocks() { return blocks; } + public int getShares() { return shares; } + public double getAverageHashrate() { return averageHashrate; } + public String getCurrency() { return currency; } + public List getBreakdown() { return breakdown; } +} + +// ==================== Device Types ==================== + +class MiningDevice { + private String id; + private String name; + private DeviceType type; + private DeviceStatus status; + private double hashrate; + private double temperature; + private double fanSpeed; + private double powerDraw; + private long memoryUsed; + private long memoryTotal; + private String driver; + private String firmware; + + public String getId() { return id; } + public String getName() { return name; } + public DeviceType getType() { return type; } + public DeviceStatus getStatus() { return status; } + public double getHashrate() { return hashrate; } + public double getTemperature() { return temperature; } + public double getFanSpeed() { return fanSpeed; } + public double getPowerDraw() { return powerDraw; } + public long getMemoryUsed() { return memoryUsed; } + public long getMemoryTotal() { return memoryTotal; } + public String getDriver() { return driver; } + public String getFirmware() { return firmware; } +} + +class DeviceConfig { + private boolean enabled; + private Integer intensity; + private Integer powerLimit; + private Integer coreClockOffset; + private Integer memoryClockOffset; + private Integer fanSpeed; + + public boolean isEnabled() { return enabled; } + public Integer getIntensity() { return intensity; } + public Integer getPowerLimit() { return powerLimit; } + public Integer getCoreClockOffset() { return coreClockOffset; } + public Integer getMemoryClockOffset() { return memoryClockOffset; } + public Integer getFanSpeed() { return fanSpeed; } + + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public void setIntensity(Integer intensity) { this.intensity = intensity; } + public void setPowerLimit(Integer powerLimit) { this.powerLimit = powerLimit; } + public void setCoreClockOffset(Integer coreClockOffset) { this.coreClockOffset = coreClockOffset; } + public void setMemoryClockOffset(Integer memoryClockOffset) { this.memoryClockOffset = memoryClockOffset; } + public void setFanSpeed(Integer fanSpeed) { this.fanSpeed = fanSpeed; } +} + +// ==================== Worker Types ==================== + +class WorkerInfo { + private String id; + private String name; + private ConnectionStatus status; + private Hashrate hashrate; + private ShareStats shares; + private List devices; + private long lastSeen; + private long uptime; + + public String getId() { return id; } + public String getName() { return name; } + public ConnectionStatus getStatus() { return status; } + public Hashrate getHashrate() { return hashrate; } + public ShareStats getShares() { return shares; } + public List getDevices() { return devices; } + public long getLastSeen() { return lastSeen; } + public long getUptime() { return uptime; } +} + +// ==================== Algorithm Types ==================== + +class MiningAlgorithm { + private String name; + private String displayName; + private String hashUnit; + private String profitability; + private double difficulty; + private String blockReward; + private int blockTime; + + public String getName() { return name; } + public String getDisplayName() { return displayName; } + public String getHashUnit() { return hashUnit; } + public String getProfitability() { return profitability; } + public double getDifficulty() { return difficulty; } + public String getBlockReward() { return blockReward; } + public int getBlockTime() { return blockTime; } +} + +// ==================== Error ==================== + +class MiningException extends RuntimeException { + private final String code; + private final int statusCode; + + public MiningException(String message) { + super(message); + this.code = null; + this.statusCode = 0; + } + + public MiningException(String message, String code, int statusCode) { + super(message); + this.code = code; + this.statusCode = statusCode; + } + + public String getCode() { return code; } + public int getStatusCode() { return statusCode; } +} diff --git a/sdk/kotlin/src/main/kotlin/io/synor/economics/SynorEconomics.kt b/sdk/kotlin/src/main/kotlin/io/synor/economics/SynorEconomics.kt new file mode 100644 index 0000000..101d0a2 --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/economics/SynorEconomics.kt @@ -0,0 +1,415 @@ +package io.synor.economics + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.delay +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Synor Economics SDK for Kotlin. + * Pricing, billing, staking, and discount operations. + */ +class SynorEconomics(private val config: EconomicsConfig) : AutoCloseable { + + companion object { + private val gson: Gson = GsonBuilder().create() + } + + private val httpClient = HttpClient(CIO) { + engine { requestTimeout = config.timeoutSecs * 1000 } + } + private val closed = AtomicBoolean(false) + + constructor(apiKey: String) : this(EconomicsConfig(apiKey)) + + // ==================== Pricing Operations ==================== + + suspend fun getPrice(service: ServiceType, usage: UsageMetrics): Price { + val body = mapOf("service" to service.name.lowercase(), "usage" to usage) + val resp = request("POST", "/pricing/calculate", body) + return gson.fromJson(resp, Price::class.java) + } + + suspend fun estimateCost(plan: UsagePlan): CostEstimate { + val body = mapOf( + "service" to plan.service.name.lowercase(), + "estimatedUsage" to plan.estimatedUsage, + "period" to plan.period.name.lowercase() + ) + val resp = request("POST", "/pricing/estimate", body) + return gson.fromJson(resp, CostEstimate::class.java) + } + + suspend fun getPricingTiers(service: ServiceType): List { + val resp = request("GET", "/pricing/tiers/${service.name.lowercase()}") + val result = gson.fromJson(resp, TiersResponse::class.java) + return result.tiers ?: emptyList() + } + + // ==================== Billing Operations ==================== + + suspend fun getUsage(period: BillingPeriod? = null): Usage { + val path = if (period != null) "/billing/usage?period=${period.name.lowercase()}" else "/billing/usage" + val resp = request("GET", path) + return gson.fromJson(resp, Usage::class.java) + } + + suspend fun getUsageByDateRange(startDate: Long, endDate: Long): Usage { + val resp = request("GET", "/billing/usage?startDate=$startDate&endDate=$endDate") + return gson.fromJson(resp, Usage::class.java) + } + + suspend fun getInvoices(): List { + val resp = request("GET", "/billing/invoices") + val result = gson.fromJson(resp, InvoicesResponse::class.java) + return result.invoices ?: emptyList() + } + + suspend fun getInvoice(invoiceId: String): Invoice { + val resp = request("GET", "/billing/invoices/${invoiceId.urlEncode()}") + return gson.fromJson(resp, Invoice::class.java) + } + + suspend fun downloadInvoice(invoiceId: String): ByteArray { + val resp = request("GET", "/billing/invoices/${invoiceId.urlEncode()}/pdf") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + return java.util.Base64.getDecoder().decode(map["data"] as String) + } + + suspend fun getBalance(): AccountBalance { + val resp = request("GET", "/billing/balance") + return gson.fromJson(resp, AccountBalance::class.java) + } + + suspend fun addCredits(amount: String, paymentMethod: String) { + request("POST", "/billing/credits", mapOf("amount" to amount, "paymentMethod" to paymentMethod)) + } + + // ==================== Staking Operations ==================== + + suspend fun stake(amount: String, durationDays: Int? = null): StakeReceipt { + val body = mutableMapOf("amount" to amount) + durationDays?.let { body["durationDays"] = it } + val resp = request("POST", "/staking/stake", body) + return gson.fromJson(resp, StakeReceipt::class.java) + } + + suspend fun unstake(stakeId: String): UnstakeReceipt { + val resp = request("POST", "/staking/unstake/${stakeId.urlEncode()}") + return gson.fromJson(resp, UnstakeReceipt::class.java) + } + + suspend fun getStakes(): List { + val resp = request("GET", "/staking/stakes") + val result = gson.fromJson(resp, StakesResponse::class.java) + return result.stakes ?: emptyList() + } + + suspend fun getStake(stakeId: String): StakeInfo { + val resp = request("GET", "/staking/stakes/${stakeId.urlEncode()}") + return gson.fromJson(resp, StakeInfo::class.java) + } + + suspend fun getStakingRewards(): Rewards { + val resp = request("GET", "/staking/rewards") + return gson.fromJson(resp, Rewards::class.java) + } + + suspend fun claimRewards(): String { + val resp = request("POST", "/staking/rewards/claim") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + return map["txHash"] as String + } + + suspend fun getCurrentApy(): Double { + val resp = request("GET", "/staking/apy") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + return (map["apy"] as Number).toDouble() + } + + // ==================== Discount Operations ==================== + + suspend fun applyDiscount(code: String): AppliedDiscount { + val resp = request("POST", "/discounts/apply", mapOf("code" to code)) + return gson.fromJson(resp, AppliedDiscount::class.java) + } + + suspend fun getAvailableDiscounts(): List { + val resp = request("GET", "/discounts/available") + val result = gson.fromJson(resp, DiscountsResponse::class.java) + return result.discounts ?: emptyList() + } + + suspend fun validateDiscount(code: String): Discount { + val resp = request("GET", "/discounts/validate/${code.urlEncode()}") + return gson.fromJson(resp, Discount::class.java) + } + + suspend fun getAppliedDiscounts(): List { + val resp = request("GET", "/discounts/applied") + val result = gson.fromJson(resp, AppliedDiscountsResponse::class.java) + return result.discounts ?: emptyList() + } + + suspend fun removeDiscount(discountId: String) { + request("DELETE", "/discounts/${discountId.urlEncode()}") + } + + // ==================== Lifecycle ==================== + + override fun close() { + closed.set(true) + httpClient.close() + } + + fun isClosed(): Boolean = closed.get() + + suspend fun healthCheck(): Boolean = try { + val resp = request("GET", "/health") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + map["status"] == "healthy" + } catch (e: Exception) { false } + + // ==================== Private Methods ==================== + + private suspend fun request(method: String, path: String, body: Any? = null): String { + if (closed.get()) throw EconomicsException("Client has been closed") + + var lastError: Exception? = null + repeat(config.retries) { attempt -> + try { + return doRequest(method, path, body) + } catch (e: Exception) { + lastError = e + if (attempt < config.retries - 1) delay((1L shl attempt) * 1000) + } + } + throw lastError ?: EconomicsException("Unknown error") + } + + private suspend fun doRequest(method: String, path: String, body: Any?): String { + val url = "${config.endpoint}$path" + val response: HttpResponse = httpClient.request(url) { + this.method = HttpMethod.parse(method) + header("Authorization", "Bearer ${config.apiKey}") + header("Content-Type", "application/json") + header("X-SDK-Version", "kotlin/0.1.0") + body?.let { setBody(gson.toJson(it)) } + } + + val responseBody = response.bodyAsText() + if (response.status.value >= 400) { + val errorMap = try { + gson.fromJson>(responseBody, object : TypeToken>() {}.type) + } catch (e: Exception) { emptyMap() } + throw EconomicsException( + errorMap["message"] as? String ?: "HTTP ${response.status.value}", + errorMap["code"] as? String, + response.status.value + ) + } + return responseBody + } + + private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8") +} + +// ==================== Types ==================== + +data class EconomicsConfig( + val apiKey: String, + val endpoint: String = "https://economics.synor.io/v1", + val timeoutSecs: Long = 60, + val retries: Int = 3, + val debug: Boolean = false +) + +enum class ServiceType { compute, storage, database, hosting, bridge, mining, rpc } +enum class BillingPeriod { daily, weekly, monthly, yearly } +enum class StakeStatus { active, unstaking, unlocked, slashed } +enum class DiscountType { percentage, fixed, volume } + +data class UsageMetrics( + val computeHours: Double = 0.0, + val storageGb: Double = 0.0, + val requests: Long = 0, + val bandwidthGb: Double = 0.0, + val gpuHours: Double = 0.0 +) + +data class PricingTier( + val name: String, + val upTo: Double, + val pricePerUnit: Double, + val unit: String +) + +data class Price( + val service: ServiceType, + val amount: String, + val currency: String, + val amountUsd: String, + val breakdown: List? = null, + val discount: String? = null, + val finalAmount: String +) + +data class UsagePlan( + val service: ServiceType, + val estimatedUsage: UsageMetrics, + val period: BillingPeriod +) + +data class CostBreakdown( + val category: String, + val amount: String, + val percentage: Double +) + +data class CostEstimate( + val service: ServiceType, + val period: BillingPeriod, + val estimatedCost: String, + val estimatedCostUsd: String, + val currency: String, + val confidenceLevel: String, + val breakdown: List? = null, + val savingsFromStaking: String? = null +) + +data class UsageRecord( + val service: ServiceType, + val resource: String, + val quantity: Double, + val unit: String, + val timestamp: Long, + val cost: String +) + +data class Usage( + val period: BillingPeriod, + val startDate: Long, + val endDate: Long, + val records: List, + val totalCost: String, + val currency: String +) + +data class InvoiceLineItem( + val description: String, + val quantity: Double, + val unit: String, + val unitPrice: String, + val amount: String +) + +data class Invoice( + val id: String, + val number: String, + val periodStart: Long, + val periodEnd: Long, + val subtotal: String, + val tax: String, + val discount: String, + val total: String, + val currency: String, + val status: String, + val dueDate: Long? = null, + val paidAt: Long? = null, + val lineItems: List, + val pdfUrl: String? = null +) + +data class AccountBalance( + val available: String, + val pending: String, + val reserved: String, + val staked: String, + val currency: String, + val lastUpdated: Long +) + +data class StakeReceipt( + val id: String, + val txHash: String, + val amount: String, + val startDate: Long, + val endDate: Long, + val apy: Double, + val status: StakeStatus +) + +data class StakeInfo( + val id: String, + val amount: String, + val status: StakeStatus, + val startDate: Long, + val endDate: Long, + val apy: Double, + val earnedRewards: String, + val pendingRewards: String, + val unlockDate: Long? = null, + val discountPercent: Int = 0 +) + +data class UnstakeReceipt( + val id: String, + val txHash: String, + val amount: String, + val initiatedAt: Long, + val availableAt: Long, + val penalty: String? = null +) + +data class RewardRecord( + val date: Long, + val amount: String, + val type: String, + val txHash: String? = null +) + +data class Rewards( + val totalEarned: String, + val pendingClaim: String, + val lastClaimDate: String? = null, + val currentApy: Double, + val history: List +) + +data class Discount( + val id: String, + val code: String, + val type: DiscountType, + val value: String, + val validFrom: Long? = null, + val validUntil: Long? = null, + val usageLimit: Int? = null, + val usageCount: Int = 0, + val applicableServices: List? = null, + val minimumSpend: String? = null, + val active: Boolean = true +) + +data class AppliedDiscount( + val discount: Discount, + val savedAmount: String, + val appliedAt: Long +) + +internal data class TiersResponse(val tiers: List?) +internal data class InvoicesResponse(val invoices: List?) +internal data class StakesResponse(val stakes: List?) +internal data class DiscountsResponse(val discounts: List?) +internal data class AppliedDiscountsResponse(val discounts: List?) + +class EconomicsException( + message: String, + val code: String? = null, + val statusCode: Int = 0 +) : RuntimeException(message) diff --git a/sdk/kotlin/src/main/kotlin/io/synor/governance/SynorGovernance.kt b/sdk/kotlin/src/main/kotlin/io/synor/governance/SynorGovernance.kt new file mode 100644 index 0000000..047d338 --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/governance/SynorGovernance.kt @@ -0,0 +1,465 @@ +package io.synor.governance + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.delay +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Synor Governance SDK for Kotlin. + * Proposals, voting, DAOs, and vesting operations. + */ +class SynorGovernance(private val config: GovernanceConfig) : AutoCloseable { + + companion object { + private val gson: Gson = GsonBuilder().create() + private val FINAL_STATUSES = setOf( + ProposalStatus.passed, ProposalStatus.rejected, ProposalStatus.executed, ProposalStatus.cancelled + ) + } + + private val httpClient = HttpClient(CIO) { + engine { requestTimeout = config.timeoutSecs * 1000 } + } + private val closed = AtomicBoolean(false) + + constructor(apiKey: String) : this(GovernanceConfig(apiKey)) + + // ==================== Proposal Operations ==================== + + suspend fun createProposal(proposal: ProposalDraft): Proposal { + val body = mutableMapOf( + "title" to proposal.title, + "description" to proposal.description + ) + proposal.discussionUrl?.let { body["discussionUrl"] = it } + proposal.votingStartTime?.let { body["votingStartTime"] = it } + proposal.votingEndTime?.let { body["votingEndTime"] = it } + proposal.actions?.let { body["actions"] = it } + proposal.daoId?.let { body["daoId"] = it } + val resp = request("POST", "/proposals", body) + return gson.fromJson(resp, Proposal::class.java) + } + + suspend fun getProposal(proposalId: String): Proposal { + val resp = request("GET", "/proposals/${proposalId.urlEncode()}") + return gson.fromJson(resp, Proposal::class.java) + } + + suspend fun listProposals(filter: ProposalFilter? = null): List { + val params = mutableListOf() + filter?.status?.let { params.add("status=${it.name.lowercase()}") } + filter?.proposer?.let { params.add("proposer=${it.urlEncode()}") } + filter?.daoId?.let { params.add("daoId=${it.urlEncode()}") } + filter?.limit?.let { params.add("limit=$it") } + filter?.offset?.let { params.add("offset=$it") } + + val path = if (params.isNotEmpty()) "/proposals?${params.joinToString("&")}" else "/proposals" + val resp = request("GET", path) + val result = gson.fromJson(resp, ProposalsResponse::class.java) + return result.proposals ?: emptyList() + } + + suspend fun cancelProposal(proposalId: String): Proposal { + val resp = request("POST", "/proposals/${proposalId.urlEncode()}/cancel") + return gson.fromJson(resp, Proposal::class.java) + } + + suspend fun executeProposal(proposalId: String): Proposal { + val resp = request("POST", "/proposals/${proposalId.urlEncode()}/execute") + return gson.fromJson(resp, Proposal::class.java) + } + + suspend fun waitForProposal(proposalId: String, pollIntervalMs: Long = 60000, maxWaitMs: Long = 604800000): Proposal { + val deadline = System.currentTimeMillis() + maxWaitMs + while (System.currentTimeMillis() < deadline) { + val proposal = getProposal(proposalId) + if (proposal.status in FINAL_STATUSES) return proposal + delay(pollIntervalMs) + } + throw GovernanceException("Timeout waiting for proposal completion") + } + + // ==================== Voting Operations ==================== + + suspend fun vote(proposalId: String, vote: Vote, weight: String? = null): VoteReceipt { + val body = mutableMapOf("choice" to vote.choice.name.lowercase()) + vote.reason?.let { body["reason"] = it } + weight?.let { body["weight"] = it } + val resp = request("POST", "/proposals/${proposalId.urlEncode()}/vote", body) + return gson.fromJson(resp, VoteReceipt::class.java) + } + + suspend fun getVotes(proposalId: String): List { + val resp = request("GET", "/proposals/${proposalId.urlEncode()}/votes") + val result = gson.fromJson(resp, VotesResponse::class.java) + return result.votes ?: emptyList() + } + + suspend fun getMyVote(proposalId: String): VoteReceipt { + val resp = request("GET", "/proposals/${proposalId.urlEncode()}/votes/me") + return gson.fromJson(resp, VoteReceipt::class.java) + } + + suspend fun delegate(delegatee: String, amount: String? = null): DelegationReceipt { + val body = mutableMapOf("delegatee" to delegatee) + amount?.let { body["amount"] = it } + val resp = request("POST", "/voting/delegate", body) + return gson.fromJson(resp, DelegationReceipt::class.java) + } + + suspend fun undelegate(delegatee: String): DelegationReceipt { + val resp = request("POST", "/voting/undelegate", mapOf("delegatee" to delegatee)) + return gson.fromJson(resp, DelegationReceipt::class.java) + } + + suspend fun getVotingPower(address: String): VotingPower { + val resp = request("GET", "/voting/power/${address.urlEncode()}") + return gson.fromJson(resp, VotingPower::class.java) + } + + suspend fun getDelegations(address: String): List { + val resp = request("GET", "/voting/delegations/${address.urlEncode()}") + val result = gson.fromJson(resp, DelegationsResponse::class.java) + return result.delegations ?: emptyList() + } + + // ==================== DAO Operations ==================== + + suspend fun createDao(daoConfig: DaoConfig): Dao { + val body = mutableMapOf( + "name" to daoConfig.name, + "description" to daoConfig.description, + "type" to daoConfig.type.name.lowercase(), + "votingPeriodDays" to daoConfig.votingPeriodDays, + "timelockDays" to daoConfig.timelockDays + ) + daoConfig.tokenAddress?.let { body["tokenAddress"] = it } + daoConfig.quorumPercent?.let { body["quorumPercent"] = it } + daoConfig.proposalThreshold?.let { body["proposalThreshold"] = it } + daoConfig.multisigMembers?.let { body["multisigMembers"] = it } + daoConfig.multisigThreshold?.let { body["multisigThreshold"] = it } + val resp = request("POST", "/daos", body) + return gson.fromJson(resp, Dao::class.java) + } + + suspend fun getDao(daoId: String): Dao { + val resp = request("GET", "/daos/${daoId.urlEncode()}") + return gson.fromJson(resp, Dao::class.java) + } + + suspend fun listDaos(limit: Int? = null, offset: Int? = null): List { + val params = mutableListOf() + limit?.let { params.add("limit=$it") } + offset?.let { params.add("offset=$it") } + val path = if (params.isNotEmpty()) "/daos?${params.joinToString("&")}" else "/daos" + val resp = request("GET", path) + val result = gson.fromJson(resp, DaosResponse::class.java) + return result.daos ?: emptyList() + } + + suspend fun getDaoTreasury(daoId: String): DaoTreasury { + val resp = request("GET", "/daos/${daoId.urlEncode()}/treasury") + return gson.fromJson(resp, DaoTreasury::class.java) + } + + suspend fun getDaoMembers(daoId: String): List { + val resp = request("GET", "/daos/${daoId.urlEncode()}/members") + val result = gson.fromJson(resp, MembersResponse::class.java) + return result.members ?: emptyList() + } + + // ==================== Vesting Operations ==================== + + suspend fun createVestingSchedule(schedule: VestingSchedule): VestingContract { + val body = mapOf( + "beneficiary" to schedule.beneficiary, + "totalAmount" to schedule.totalAmount, + "startTime" to schedule.startTime, + "cliffDuration" to schedule.cliffDuration, + "vestingDuration" to schedule.vestingDuration, + "revocable" to schedule.revocable + ) + val resp = request("POST", "/vesting", body) + return gson.fromJson(resp, VestingContract::class.java) + } + + suspend fun getVestingContract(contractId: String): VestingContract { + val resp = request("GET", "/vesting/${contractId.urlEncode()}") + return gson.fromJson(resp, VestingContract::class.java) + } + + suspend fun listVestingContracts(beneficiary: String? = null): List { + val path = if (beneficiary != null) "/vesting?beneficiary=${beneficiary.urlEncode()}" else "/vesting" + val resp = request("GET", path) + val result = gson.fromJson(resp, VestingContractsResponse::class.java) + return result.contracts ?: emptyList() + } + + suspend fun claimVested(contractId: String): ClaimReceipt { + val resp = request("POST", "/vesting/${contractId.urlEncode()}/claim") + return gson.fromJson(resp, ClaimReceipt::class.java) + } + + suspend fun revokeVesting(contractId: String): VestingContract { + val resp = request("POST", "/vesting/${contractId.urlEncode()}/revoke") + return gson.fromJson(resp, VestingContract::class.java) + } + + suspend fun getReleasableAmount(contractId: String): String { + val resp = request("GET", "/vesting/${contractId.urlEncode()}/releasable") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + return map["amount"] as String + } + + // ==================== Lifecycle ==================== + + override fun close() { + closed.set(true) + httpClient.close() + } + + fun isClosed(): Boolean = closed.get() + + suspend fun healthCheck(): Boolean = try { + val resp = request("GET", "/health") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + map["status"] == "healthy" + } catch (e: Exception) { false } + + // ==================== Private Methods ==================== + + private suspend fun request(method: String, path: String, body: Any? = null): String { + if (closed.get()) throw GovernanceException("Client has been closed") + + var lastError: Exception? = null + repeat(config.retries) { attempt -> + try { + return doRequest(method, path, body) + } catch (e: Exception) { + lastError = e + if (attempt < config.retries - 1) delay((1L shl attempt) * 1000) + } + } + throw lastError ?: GovernanceException("Unknown error") + } + + private suspend fun doRequest(method: String, path: String, body: Any?): String { + val url = "${config.endpoint}$path" + val response: HttpResponse = httpClient.request(url) { + this.method = HttpMethod.parse(method) + header("Authorization", "Bearer ${config.apiKey}") + header("Content-Type", "application/json") + header("X-SDK-Version", "kotlin/0.1.0") + body?.let { setBody(gson.toJson(it)) } + } + + val responseBody = response.bodyAsText() + if (response.status.value >= 400) { + val errorMap = try { + gson.fromJson>(responseBody, object : TypeToken>() {}.type) + } catch (e: Exception) { emptyMap() } + throw GovernanceException( + errorMap["message"] as? String ?: "HTTP ${response.status.value}", + errorMap["code"] as? String, + response.status.value + ) + } + return responseBody + } + + private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8") +} + +// ==================== Types ==================== + +data class GovernanceConfig( + val apiKey: String, + val endpoint: String = "https://governance.synor.io/v1", + val timeoutSecs: Long = 60, + val retries: Int = 3, + val debug: Boolean = false +) + +enum class ProposalStatus { draft, active, passed, rejected, executed, cancelled } +enum class VoteChoice { yes, no, abstain } +enum class DaoType { token, multisig, hybrid } +enum class VestingStatus { pending, active, paused, completed, revoked } + +data class ProposalAction( + val target: String, + val method: String, + val data: String, + val value: String? = null +) + +data class ProposalDraft( + val title: String, + val description: String, + val discussionUrl: String? = null, + val votingStartTime: Long? = null, + val votingEndTime: Long? = null, + val actions: List? = null, + val daoId: String? = null +) + +data class VoteBreakdown( + val yes: String, + val no: String, + val abstain: String, + val quorum: String, + val quorumPercent: Double, + val quorumReached: Boolean +) + +data class Proposal( + val id: String, + val title: String, + val description: String, + val proposer: String, + val status: ProposalStatus, + val createdAt: Long, + val votingStartTime: Long, + val votingEndTime: Long, + val votes: VoteBreakdown, + val discussionUrl: String? = null, + val actions: List? = null, + val daoId: String? = null, + val snapshotBlock: String? = null, + val executedAt: Long? = null, + val executedTxHash: String? = null +) + +data class ProposalFilter( + val status: ProposalStatus? = null, + val proposer: String? = null, + val daoId: String? = null, + val limit: Int? = null, + val offset: Int? = null +) + +data class Vote(val choice: VoteChoice, val reason: String? = null) + +data class VoteReceipt( + val id: String, + val proposalId: String, + val voter: String, + val choice: VoteChoice, + val weight: String, + val timestamp: Long, + val txHash: String, + val reason: String? = null +) + +data class VotingPower( + val address: String, + val balance: String, + val delegatedTo: String? = null, + val delegatedFrom: String, + val totalPower: String, + val blockNumber: Long +) + +data class DelegationReceipt( + val id: String, + val delegator: String, + val delegatee: String, + val amount: String, + val timestamp: Long, + val txHash: String +) + +data class DaoConfig( + val name: String, + val description: String, + val type: DaoType, + val tokenAddress: String? = null, + val quorumPercent: String? = null, + val votingPeriodDays: Int = 7, + val timelockDays: Int = 2, + val proposalThreshold: Int? = null, + val multisigMembers: List? = null, + val multisigThreshold: Int? = null +) + +data class TreasuryAsset( + val address: String, + val symbol: String, + val balance: String, + val valueUsd: String +) + +data class DaoTreasury( + val address: String, + val balance: String, + val currency: String, + val assets: List +) + +data class Dao( + val id: String, + val name: String, + val description: String, + val type: DaoType, + val tokenAddress: String? = null, + val governorAddress: String, + val timelockAddress: String, + val treasury: DaoTreasury, + val quorumPercent: String, + val votingPeriodDays: Int, + val timelockDays: Int, + val proposalCount: Int, + val memberCount: Int, + val createdAt: Long +) + +data class VestingSchedule( + val beneficiary: String, + val totalAmount: String, + val startTime: Long, + val cliffDuration: Long, + val vestingDuration: Long, + val revocable: Boolean = false +) + +data class VestingContract( + val id: String, + val contractAddress: String, + val beneficiary: String, + val totalAmount: String, + val releasedAmount: String, + val releasableAmount: String, + val startTime: Long, + val cliffEnd: Long, + val vestingEnd: Long, + val status: VestingStatus, + val createdAt: Long, + val txHash: String +) + +data class ClaimReceipt( + val vestingContractId: String, + val amount: String, + val txHash: String, + val timestamp: Long, + val remainingAmount: String +) + +internal data class ProposalsResponse(val proposals: List?) +internal data class VotesResponse(val votes: List?) +internal data class DelegationsResponse(val delegations: List?) +internal data class DaosResponse(val daos: List?) +internal data class MembersResponse(val members: List?) +internal data class VestingContractsResponse(val contracts: List?) + +class GovernanceException( + message: String, + val code: String? = null, + val statusCode: Int = 0 +) : RuntimeException(message) diff --git a/sdk/kotlin/src/main/kotlin/io/synor/mining/SynorMining.kt b/sdk/kotlin/src/main/kotlin/io/synor/mining/SynorMining.kt new file mode 100644 index 0000000..17fe4ad --- /dev/null +++ b/sdk/kotlin/src/main/kotlin/io/synor/mining/SynorMining.kt @@ -0,0 +1,464 @@ +package io.synor.mining + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.delay +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * Synor Mining SDK for Kotlin. + * Pool connections, block templates, hashrate stats, and GPU management. + */ +class SynorMining(private val config: MiningConfig) : AutoCloseable { + + companion object { + private val gson: Gson = GsonBuilder().create() + } + + private val httpClient = HttpClient(CIO) { + engine { requestTimeout = config.timeoutSecs * 1000 } + } + private val closed = AtomicBoolean(false) + private val activeConnection = AtomicReference(null) + + constructor(apiKey: String) : this(MiningConfig(apiKey)) + + // ==================== Pool Operations ==================== + + suspend fun connect(pool: PoolConfig): StratumConnection { + val body = mutableMapOf("url" to pool.url, "user" to pool.user) + pool.password?.let { body["password"] = it } + pool.algorithm?.let { body["algorithm"] = it } + pool.difficulty?.let { body["difficulty"] = it } + val resp = request("POST", "/pool/connect", body) + val conn = gson.fromJson(resp, StratumConnection::class.java) + activeConnection.set(conn) + return conn + } + + suspend fun disconnect() { + if (activeConnection.get() == null) return + request("POST", "/pool/disconnect") + activeConnection.set(null) + } + + suspend fun getConnection(): StratumConnection { + val resp = request("GET", "/pool/connection") + val conn = gson.fromJson(resp, StratumConnection::class.java) + activeConnection.set(conn) + return conn + } + + suspend fun getPoolStats(): PoolStats { + val resp = request("GET", "/pool/stats") + return gson.fromJson(resp, PoolStats::class.java) + } + + fun isConnected(): Boolean { + val conn = activeConnection.get() + return conn != null && conn.status == ConnectionStatus.connected + } + + // ==================== Mining Operations ==================== + + suspend fun getBlockTemplate(): BlockTemplate { + val resp = request("GET", "/mining/template") + return gson.fromJson(resp, BlockTemplate::class.java) + } + + suspend fun submitWork(work: MinedWork): SubmitResult { + val body = mapOf( + "templateId" to work.templateId, + "nonce" to work.nonce, + "extraNonce" to work.extraNonce, + "timestamp" to work.timestamp, + "hash" to work.hash + ) + val resp = request("POST", "/mining/submit", body) + return gson.fromJson(resp, SubmitResult::class.java) + } + + suspend fun startMining(algorithm: String? = null) { + val body = if (algorithm != null) mapOf("algorithm" to algorithm) else null + request("POST", "/mining/start", body) + } + + suspend fun stopMining() { + request("POST", "/mining/stop") + } + + suspend fun isMining(): Boolean { + val resp = request("GET", "/mining/status") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + return map["mining"] == true + } + + // ==================== Stats Operations ==================== + + suspend fun getHashrate(): Hashrate { + val resp = request("GET", "/stats/hashrate") + return gson.fromJson(resp, Hashrate::class.java) + } + + suspend fun getStats(): MiningStats { + val resp = request("GET", "/stats") + return gson.fromJson(resp, MiningStats::class.java) + } + + suspend fun getEarnings(period: TimePeriod? = null): Earnings { + val path = if (period != null) "/stats/earnings?period=${period.name.lowercase()}" else "/stats/earnings" + val resp = request("GET", path) + return gson.fromJson(resp, Earnings::class.java) + } + + suspend fun getShareStats(): ShareStats { + val resp = request("GET", "/stats/shares") + return gson.fromJson(resp, ShareStats::class.java) + } + + // ==================== Device Operations ==================== + + suspend fun listDevices(): List { + val resp = request("GET", "/devices") + val result = gson.fromJson(resp, DevicesResponse::class.java) + return result.devices ?: emptyList() + } + + suspend fun getDevice(deviceId: String): MiningDevice { + val resp = request("GET", "/devices/${deviceId.urlEncode()}") + return gson.fromJson(resp, MiningDevice::class.java) + } + + suspend fun setDeviceConfig(deviceId: String, deviceConfig: DeviceConfig): MiningDevice { + val body = mutableMapOf("enabled" to deviceConfig.enabled) + deviceConfig.intensity?.let { body["intensity"] = it } + deviceConfig.powerLimit?.let { body["powerLimit"] = it } + deviceConfig.coreClockOffset?.let { body["coreClockOffset"] = it } + deviceConfig.memoryClockOffset?.let { body["memoryClockOffset"] = it } + deviceConfig.fanSpeed?.let { body["fanSpeed"] = it } + val resp = request("PUT", "/devices/${deviceId.urlEncode()}/config", body) + return gson.fromJson(resp, MiningDevice::class.java) + } + + suspend fun enableDevice(deviceId: String) { + setDeviceConfig(deviceId, DeviceConfig(enabled = true)) + } + + suspend fun disableDevice(deviceId: String) { + setDeviceConfig(deviceId, DeviceConfig(enabled = false)) + } + + // ==================== Worker Operations ==================== + + suspend fun listWorkers(): List { + val resp = request("GET", "/workers") + val result = gson.fromJson(resp, WorkersResponse::class.java) + return result.workers ?: emptyList() + } + + suspend fun getWorker(workerId: String): WorkerInfo { + val resp = request("GET", "/workers/${workerId.urlEncode()}") + return gson.fromJson(resp, WorkerInfo::class.java) + } + + suspend fun removeWorker(workerId: String) { + request("DELETE", "/workers/${workerId.urlEncode()}") + } + + // ==================== Algorithm Operations ==================== + + suspend fun listAlgorithms(): List { + val resp = request("GET", "/algorithms") + val result = gson.fromJson(resp, AlgorithmsResponse::class.java) + return result.algorithms ?: emptyList() + } + + suspend fun getAlgorithm(name: String): MiningAlgorithm { + val resp = request("GET", "/algorithms/${name.urlEncode()}") + return gson.fromJson(resp, MiningAlgorithm::class.java) + } + + suspend fun switchAlgorithm(algorithm: String) { + request("POST", "/algorithms/switch", mapOf("algorithm" to algorithm)) + } + + suspend fun getMostProfitable(): MiningAlgorithm { + val resp = request("GET", "/algorithms/profitable") + return gson.fromJson(resp, MiningAlgorithm::class.java) + } + + // ==================== Lifecycle ==================== + + override fun close() { + if (closed.compareAndSet(false, true)) { + activeConnection.set(null) + httpClient.close() + } + } + + fun isClosed(): Boolean = closed.get() + + suspend fun healthCheck(): Boolean = try { + val resp = request("GET", "/health") + val map = gson.fromJson>(resp, object : TypeToken>() {}.type) + map["status"] == "healthy" + } catch (e: Exception) { false } + + // ==================== Private Methods ==================== + + private suspend fun request(method: String, path: String, body: Any? = null): String { + if (closed.get()) throw MiningException("Client has been closed") + + var lastError: Exception? = null + repeat(config.retries) { attempt -> + try { + return doRequest(method, path, body) + } catch (e: Exception) { + lastError = e + if (attempt < config.retries - 1) delay((1L shl attempt) * 1000) + } + } + throw lastError ?: MiningException("Unknown error") + } + + private suspend fun doRequest(method: String, path: String, body: Any?): String { + val url = "${config.endpoint}$path" + val response: HttpResponse = httpClient.request(url) { + this.method = HttpMethod.parse(method) + header("Authorization", "Bearer ${config.apiKey}") + header("Content-Type", "application/json") + header("X-SDK-Version", "kotlin/0.1.0") + body?.let { setBody(gson.toJson(it)) } + } + + val responseBody = response.bodyAsText() + if (response.status.value >= 400) { + val errorMap = try { + gson.fromJson>(responseBody, object : TypeToken>() {}.type) + } catch (e: Exception) { emptyMap() } + throw MiningException( + errorMap["message"] as? String ?: "HTTP ${response.status.value}", + errorMap["code"] as? String, + response.status.value + ) + } + return responseBody + } + + private fun String.urlEncode(): String = java.net.URLEncoder.encode(this, "UTF-8") +} + +// ==================== Types ==================== + +data class MiningConfig( + val apiKey: String, + val endpoint: String = "https://mining.synor.io/v1", + val timeoutSecs: Long = 60, + val retries: Int = 3, + val debug: Boolean = false +) + +enum class DeviceType { cpu, gpu_nvidia, gpu_amd, asic } +enum class DeviceStatus { idle, mining, error, offline } +enum class ConnectionStatus { disconnected, connecting, connected, reconnecting } +enum class TimePeriod { hour, day, week, month, all } +enum class SubmitResultStatus { accepted, rejected, stale } + +data class PoolConfig( + val url: String, + val user: String, + val password: String? = null, + val algorithm: String? = null, + val difficulty: Double? = null +) + +data class StratumConnection( + val id: String, + val pool: String, + val status: ConnectionStatus, + val algorithm: String, + val difficulty: Double, + val connectedAt: Long, + val acceptedShares: Int, + val rejectedShares: Int, + val staleShares: Int, + val lastShareAt: Long? = null +) + +data class PoolStats( + val url: String, + val workers: Int, + val hashrate: Double, + val difficulty: Double, + val lastBlock: Long, + val blocksFound24h: Int, + val luck: Double +) + +data class TemplateTransaction( + val txid: String, + val data: String, + val fee: String, + val weight: Int +) + +data class BlockTemplate( + val id: String, + val previousBlockHash: String, + val merkleRoot: String, + val timestamp: Long, + val bits: String, + val height: Long, + val coinbaseValue: String, + val transactions: List, + val target: String, + val algorithm: String, + val extraNonce: String +) + +data class MinedWork( + val templateId: String, + val nonce: String, + val extraNonce: String, + val timestamp: Long, + val hash: String +) + +data class ShareInfo( + val hash: String, + val difficulty: Double, + val timestamp: Long, + val accepted: Boolean +) + +data class SubmitResult( + val status: SubmitResultStatus, + val share: ShareInfo, + val blockFound: Boolean, + val reason: String? = null, + val blockHash: String? = null, + val reward: String? = null +) + +data class Hashrate( + val current: Double, + val average1h: Double, + val average24h: Double, + val peak: Double, + val unit: String +) + +data class ShareStats( + val accepted: Int, + val rejected: Int, + val stale: Int, + val total: Int, + val acceptRate: Double +) + +data class DeviceTemperature( + val current: Double, + val max: Double, + val throttling: Boolean +) + +data class EarningsSnapshot( + val today: String, + val yesterday: String, + val thisWeek: String, + val thisMonth: String, + val total: String, + val currency: String +) + +data class MiningStats( + val hashrate: Hashrate, + val shares: ShareStats, + val uptime: Long, + val efficiency: Double, + val earnings: EarningsSnapshot, + val powerConsumption: Double? = null, + val temperature: DeviceTemperature? = null +) + +data class EarningsBreakdown( + val date: Long, + val amount: String, + val blocks: Int, + val shares: Int, + val hashrate: Double +) + +data class Earnings( + val period: TimePeriod, + val startDate: Long, + val endDate: Long, + val amount: String, + val blocks: Int, + val shares: Int, + val averageHashrate: Double, + val currency: String, + val breakdown: List +) + +data class MiningDevice( + val id: String, + val name: String, + val type: DeviceType, + val status: DeviceStatus, + val hashrate: Double, + val temperature: Double, + val fanSpeed: Double, + val powerDraw: Double, + val memoryUsed: Long, + val memoryTotal: Long, + val driver: String? = null, + val firmware: String? = null +) + +data class DeviceConfig( + val enabled: Boolean, + val intensity: Int? = null, + val powerLimit: Int? = null, + val coreClockOffset: Int? = null, + val memoryClockOffset: Int? = null, + val fanSpeed: Int? = null +) + +data class WorkerInfo( + val id: String, + val name: String, + val status: ConnectionStatus, + val hashrate: Hashrate, + val shares: ShareStats, + val devices: List, + val lastSeen: Long, + val uptime: Long +) + +data class MiningAlgorithm( + val name: String, + val displayName: String, + val hashUnit: String, + val profitability: String, + val difficulty: Double, + val blockReward: String, + val blockTime: Int +) + +internal data class DevicesResponse(val devices: List?) +internal data class WorkersResponse(val workers: List?) +internal data class AlgorithmsResponse(val algorithms: List?) + +class MiningException( + message: String, + val code: String? = null, + val statusCode: Int = 0 +) : RuntimeException(message) diff --git a/sdk/ruby/lib/synor_economics.rb b/sdk/ruby/lib/synor_economics.rb new file mode 100644 index 0000000..0a03b2e --- /dev/null +++ b/sdk/ruby/lib/synor_economics.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "synor_economics/version" +require_relative "synor_economics/types" +require_relative "synor_economics/client" + +module SynorEconomics + class Error < StandardError; end + class ClientClosedError < Error; end + + class HttpError < Error + attr_reader :status_code, :code + + def initialize(message, status_code: nil, code: nil) + super(message) + @status_code = status_code + @code = code + end + end +end diff --git a/sdk/ruby/lib/synor_economics/client.rb b/sdk/ruby/lib/synor_economics/client.rb new file mode 100644 index 0000000..fc0da92 --- /dev/null +++ b/sdk/ruby/lib/synor_economics/client.rb @@ -0,0 +1,366 @@ +# frozen_string_literal: true + +require "faraday" +require "json" +require "uri" + +module SynorEconomics + # Synor Economics SDK client for Ruby. + # Pricing, billing, staking, and discount management. + class Client + attr_reader :closed + + def initialize(config) + @config = config + @closed = false + @conn = Faraday.new(url: config.endpoint) do |f| + f.request :json + f.response :json + f.options.timeout = config.timeout + f.headers["Authorization"] = "Bearer #{config.api_key}" + f.headers["Content-Type"] = "application/json" + f.headers["X-SDK-Version"] = "ruby/#{VERSION}" + end + end + + # ==================== Pricing Operations ==================== + + def get_pricing(service: nil) + path = service ? "/pricing?service=#{service}" : "/pricing" + response = get(path) + (response["pricing"] || []).map { |p| parse_service_pricing(p) } + end + + def get_price(service:, usage:) + body = { service: service, usage: usage_to_hash(usage) } + response = post("/pricing/calculate", body) + parse_price_result(response) + end + + def estimate_cost(plan:) + body = { plan: plan.map { |p| usage_plan_item_to_hash(p) } } + response = post("/pricing/estimate", body) + parse_cost_estimate(response) + end + + # ==================== Billing Operations ==================== + + def get_usage(period: nil) + path = period ? "/billing/usage?period=#{period}" : "/billing/usage" + response = get(path) + parse_usage(response) + end + + def get_invoices + response = get("/billing/invoices") + (response["invoices"] || []).map { |i| parse_invoice(i) } + end + + def get_invoice(invoice_id) + response = get("/billing/invoices/#{encode(invoice_id)}") + parse_invoice(response) + end + + def get_balance + response = get("/billing/balance") + parse_account_balance(response) + end + + # ==================== Staking Operations ==================== + + def stake(amount:, duration_days: nil) + body = { amount: amount } + body[:duration_days] = duration_days if duration_days + response = post("/staking/stake", body) + parse_stake_receipt(response) + end + + def unstake(stake_id) + response = post("/staking/#{encode(stake_id)}/unstake", {}) + parse_unstake_receipt(response) + end + + def get_stakes + response = get("/staking") + (response["stakes"] || []).map { |s| parse_stake(s) } + end + + def get_stake(stake_id) + response = get("/staking/#{encode(stake_id)}") + parse_stake(response) + end + + def get_staking_rewards + response = get("/staking/rewards") + parse_staking_rewards(response) + end + + def claim_rewards + response = post("/staking/rewards/claim", {}) + parse_stake_receipt(response) + end + + # ==================== Discount Operations ==================== + + def apply_discount(code) + response = post("/discounts/apply", { code: code }) + parse_discount_result(response) + end + + def remove_discount(code) + delete("/discounts/#{encode(code)}") + end + + def get_discounts + response = get("/discounts") + (response["discounts"] || []).map { |d| parse_discount(d) } + end + + # ==================== Lifecycle ==================== + + def health_check + response = get("/health") + response["status"] == "healthy" + rescue StandardError + false + end + + def close + @closed = true + @conn.close if @conn.respond_to?(:close) + end + + private + + def get(path, params = {}) + execute { @conn.get(path, params).body } + end + + def post(path, body) + execute { @conn.post(path, body).body } + end + + def delete(path) + execute { @conn.delete(path).body } + end + + def execute + raise ClientClosedError, "Client has been closed" if @closed + + last_error = nil + @config.retries.times do |attempt| + begin + response = yield + check_error(response) if response.is_a?(Hash) + return response + rescue StandardError => e + last_error = e + sleep(2**attempt) if attempt < @config.retries - 1 + end + end + raise last_error + end + + def check_error(response) + return unless response["error"] || (response["code"] && response["message"]) + + message = response["message"] || response["error"] || "Unknown error" + code = response["code"] + status = response["status_code"] || 0 + raise HttpError.new(message, status_code: status, code: code) + end + + def encode(str) + URI.encode_www_form_component(str) + end + + # Parsing methods + + def parse_service_pricing(data) + ServicePricing.new( + service: data["service"], + unit: data["unit"], + price_per_unit: data["price_per_unit"], + currency: data["currency"], + minimum: data["minimum"], + maximum: data["maximum"] + ) + end + + def parse_price_result(data) + PriceResult.new( + service: data["service"], + amount: data["amount"], + currency: data["currency"], + usage: data["usage"] ? parse_usage_metrics(data["usage"]) : nil + ) + end + + def parse_usage_metrics(data) + UsageMetrics.new( + service: data["service"], + compute_units: data["compute_units"], + storage_bytes: data["storage_bytes"], + bandwidth_bytes: data["bandwidth_bytes"], + duration_seconds: data["duration_seconds"], + requests: data["requests"] + ) + end + + def parse_cost_estimate(data) + CostEstimate.new( + total: data["total"], + currency: data["currency"], + breakdown: (data["breakdown"] || []).map { |b| CostItem.new(service: b["service"], amount: b["amount"]) }, + discount_applied: data["discount_applied"], + period: data["period"] + ) + end + + def parse_usage(data) + Usage.new( + period: data["period"], + start_date: data["start_date"], + end_date: data["end_date"], + items: (data["items"] || []).map { |i| parse_usage_item(i) }, + total_cost: data["total_cost"], + currency: data["currency"] + ) + end + + def parse_usage_item(data) + UsageItem.new( + service: data["service"], + compute_units: data["compute_units"], + storage_bytes: data["storage_bytes"], + bandwidth_bytes: data["bandwidth_bytes"], + requests: data["requests"], + cost: data["cost"] + ) + end + + def parse_invoice(data) + Invoice.new( + id: data["id"], + date: data["date"], + due_date: data["due_date"], + status: data["status"], + lines: (data["lines"] || []).map { |l| parse_invoice_line(l) }, + subtotal: data["subtotal"], + discount: data["discount"], + tax: data["tax"], + total: data["total"], + currency: data["currency"], + pdf_url: data["pdf_url"] + ) + end + + def parse_invoice_line(data) + InvoiceLine.new( + service: data["service"], + description: data["description"], + quantity: data["quantity"], + unit_price: data["unit_price"], + amount: data["amount"] + ) + end + + def parse_account_balance(data) + AccountBalance.new( + available: data["available"], + pending: data["pending"], + staked: data["staked"], + total: data["total"], + currency: data["currency"] + ) + end + + def parse_stake(data) + Stake.new( + id: data["id"], + amount: data["amount"], + staked_at: data["staked_at"], + unlock_at: data["unlock_at"], + status: data["status"], + rewards_earned: data["rewards_earned"], + apy: data["apy"] + ) + end + + def parse_stake_receipt(data) + StakeReceipt.new( + id: data["id"], + amount: data["amount"], + tx_hash: data["tx_hash"], + staked_at: data["staked_at"], + unlock_at: data["unlock_at"], + apy: data["apy"] + ) + end + + def parse_unstake_receipt(data) + UnstakeReceipt.new( + id: data["id"], + amount: data["amount"], + tx_hash: data["tx_hash"], + unstaked_at: data["unstaked_at"], + available_at: data["available_at"] + ) + end + + def parse_staking_rewards(data) + StakingRewards.new( + pending: data["pending"], + claimed: data["claimed"], + total: data["total"], + current_apy: data["current_apy"], + last_claim: data["last_claim"], + next_distribution: data["next_distribution"] + ) + end + + def parse_discount(data) + Discount.new( + code: data["code"], + type: data["type"], + value: data["value"], + description: data["description"], + applicable_services: data["applicable_services"], + valid_from: data["valid_from"], + valid_until: data["valid_until"], + max_uses: data["max_uses"], + current_uses: data["current_uses"], + active: data["active"] + ) + end + + def parse_discount_result(data) + DiscountResult.new( + discount: parse_discount(data["discount"]), + savings_estimate: data["savings_estimate"], + applied_at: data["applied_at"] + ) + end + + # Conversion methods + + def usage_to_hash(usage) + { + service: usage.service, + compute_units: usage.compute_units, + storage_bytes: usage.storage_bytes, + bandwidth_bytes: usage.bandwidth_bytes, + duration_seconds: usage.duration_seconds, + requests: usage.requests + } + end + + def usage_plan_item_to_hash(item) + { + service: item.service, + projected_usage: usage_to_hash(item.projected_usage), + period: item.period + } + end + end +end diff --git a/sdk/ruby/lib/synor_economics/types.rb b/sdk/ruby/lib/synor_economics/types.rb new file mode 100644 index 0000000..1188fbf --- /dev/null +++ b/sdk/ruby/lib/synor_economics/types.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module SynorEconomics + # Service types + module ServiceType + COMPUTE = "compute" + STORAGE = "storage" + DATABASE = "database" + HOSTING = "hosting" + RPC = "rpc" + BRIDGE = "bridge" + end + + # Billing periods + module BillingPeriod + HOURLY = "hourly" + DAILY = "daily" + WEEKLY = "weekly" + MONTHLY = "monthly" + end + + # Stake status + module StakeStatus + ACTIVE = "active" + UNSTAKING = "unstaking" + WITHDRAWN = "withdrawn" + end + + # Discount types + module DiscountType + PERCENTAGE = "percentage" + FIXED = "fixed" + CREDITS = "credits" + end + + # Invoice status + module InvoiceStatus + PENDING = "pending" + PAID = "paid" + OVERDUE = "overdue" + CANCELLED = "cancelled" + end + + # Configuration + Config = Struct.new(:api_key, :endpoint, :timeout, :retries, :debug, keyword_init: true) do + def initialize(api_key:, endpoint: "https://economics.synor.io/v1", timeout: 30, retries: 3, debug: false) + super + end + end + + # Data types + ServicePricing = Struct.new(:service, :unit, :price_per_unit, :currency, :minimum, :maximum, keyword_init: true) + + UsageMetrics = Struct.new(:service, :compute_units, :storage_bytes, :bandwidth_bytes, :duration_seconds, :requests, keyword_init: true) + + PriceResult = Struct.new(:service, :amount, :currency, :usage, keyword_init: true) + + UsagePlanItem = Struct.new(:service, :projected_usage, :period, keyword_init: true) + + CostItem = Struct.new(:service, :amount, keyword_init: true) + + CostEstimate = Struct.new(:total, :currency, :breakdown, :discount_applied, :period, keyword_init: true) + + UsageItem = Struct.new(:service, :compute_units, :storage_bytes, :bandwidth_bytes, :requests, :cost, keyword_init: true) + + Usage = Struct.new(:period, :start_date, :end_date, :items, :total_cost, :currency, keyword_init: true) + + InvoiceLine = Struct.new(:service, :description, :quantity, :unit_price, :amount, keyword_init: true) + + Invoice = Struct.new(:id, :date, :due_date, :status, :lines, :subtotal, :discount, :tax, :total, :currency, :pdf_url, keyword_init: true) + + AccountBalance = Struct.new(:available, :pending, :staked, :total, :currency, keyword_init: true) + + Stake = Struct.new(:id, :amount, :staked_at, :unlock_at, :status, :rewards_earned, :apy, keyword_init: true) + + StakeReceipt = Struct.new(:id, :amount, :tx_hash, :staked_at, :unlock_at, :apy, keyword_init: true) + + UnstakeReceipt = Struct.new(:id, :amount, :tx_hash, :unstaked_at, :available_at, keyword_init: true) + + StakingRewards = Struct.new(:pending, :claimed, :total, :current_apy, :last_claim, :next_distribution, keyword_init: true) + + Discount = Struct.new(:code, :type, :value, :description, :applicable_services, :valid_from, :valid_until, :max_uses, :current_uses, :active, keyword_init: true) + + DiscountResult = Struct.new(:discount, :savings_estimate, :applied_at, keyword_init: true) +end diff --git a/sdk/ruby/lib/synor_economics/version.rb b/sdk/ruby/lib/synor_economics/version.rb new file mode 100644 index 0000000..55a7ad0 --- /dev/null +++ b/sdk/ruby/lib/synor_economics/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorEconomics + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_governance.rb b/sdk/ruby/lib/synor_governance.rb new file mode 100644 index 0000000..9608e7e --- /dev/null +++ b/sdk/ruby/lib/synor_governance.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "synor_governance/version" +require_relative "synor_governance/types" +require_relative "synor_governance/client" + +module SynorGovernance + class Error < StandardError; end + class ClientClosedError < Error; end + + class HttpError < Error + attr_reader :status_code, :code + + def initialize(message, status_code: nil, code: nil) + super(message) + @status_code = status_code + @code = code + end + end +end diff --git a/sdk/ruby/lib/synor_governance/client.rb b/sdk/ruby/lib/synor_governance/client.rb new file mode 100644 index 0000000..e530a24 --- /dev/null +++ b/sdk/ruby/lib/synor_governance/client.rb @@ -0,0 +1,422 @@ +# frozen_string_literal: true + +require "faraday" +require "json" +require "uri" + +module SynorGovernance + # Synor Governance SDK client for Ruby. + # Proposals, voting, DAOs, and vesting operations. + class Client + FINAL_STATUSES = [ + ProposalStatus::PASSED, + ProposalStatus::REJECTED, + ProposalStatus::EXECUTED, + ProposalStatus::CANCELLED + ].freeze + + attr_reader :closed + + def initialize(config) + @config = config + @closed = false + @conn = Faraday.new(url: config.endpoint) do |f| + f.request :json + f.response :json + f.options.timeout = config.timeout + f.headers["Authorization"] = "Bearer #{config.api_key}" + f.headers["Content-Type"] = "application/json" + f.headers["X-SDK-Version"] = "ruby/#{VERSION}" + end + end + + # ==================== Proposal Operations ==================== + + def create_proposal(draft) + body = proposal_draft_to_hash(draft) + response = post("/proposals", body) + parse_proposal(response) + end + + def get_proposal(proposal_id) + response = get("/proposals/#{encode(proposal_id)}") + parse_proposal(response) + end + + def list_proposals(filter: nil) + params = {} + if filter + params[:status] = filter.status if filter.status + params[:proposer] = filter.proposer if filter.proposer + params[:dao_id] = filter.dao_id if filter.dao_id + params[:limit] = filter.limit if filter.limit + params[:offset] = filter.offset if filter.offset + end + response = get("/proposals", params) + (response["proposals"] || []).map { |p| parse_proposal(p) } + end + + def cancel_proposal(proposal_id) + response = post("/proposals/#{encode(proposal_id)}/cancel", {}) + parse_proposal(response) + end + + def execute_proposal(proposal_id) + response = post("/proposals/#{encode(proposal_id)}/execute", {}) + parse_proposal(response) + end + + def wait_for_proposal(proposal_id, poll_interval: 60, max_wait: 604_800) + deadline = Time.now + max_wait + while Time.now < deadline + proposal = get_proposal(proposal_id) + return proposal if FINAL_STATUSES.include?(proposal.status) + + sleep(poll_interval) + end + raise Error, "Timeout waiting for proposal completion" + end + + # ==================== Voting Operations ==================== + + def vote(proposal_id:, vote:, weight: nil) + body = { choice: vote.choice } + body[:reason] = vote.reason if vote.reason + body[:weight] = weight if weight + response = post("/proposals/#{encode(proposal_id)}/vote", body) + parse_vote_receipt(response) + end + + def get_votes(proposal_id) + response = get("/proposals/#{encode(proposal_id)}/votes") + (response["votes"] || []).map { |v| parse_vote_receipt(v) } + end + + def get_my_vote(proposal_id) + response = get("/proposals/#{encode(proposal_id)}/votes/me") + parse_vote_receipt(response) + end + + def delegate(delegatee:, amount: nil) + body = { delegatee: delegatee } + body[:amount] = amount if amount + response = post("/voting/delegate", body) + parse_delegation_receipt(response) + end + + def undelegate(delegatee) + response = post("/voting/undelegate", { delegatee: delegatee }) + parse_delegation_receipt(response) + end + + def get_voting_power(address) + response = get("/voting/power/#{encode(address)}") + parse_voting_power(response) + end + + def get_delegations(address) + response = get("/voting/delegations/#{encode(address)}") + (response["delegations"] || []).map { |d| parse_delegation_receipt(d) } + end + + # ==================== DAO Operations ==================== + + def create_dao(config) + body = dao_config_to_hash(config) + response = post("/daos", body) + parse_dao(response) + end + + def get_dao(dao_id) + response = get("/daos/#{encode(dao_id)}") + parse_dao(response) + end + + def list_daos(limit: nil, offset: nil) + params = {} + params[:limit] = limit if limit + params[:offset] = offset if offset + response = get("/daos", params) + (response["daos"] || []).map { |d| parse_dao(d) } + end + + def get_dao_treasury(dao_id) + response = get("/daos/#{encode(dao_id)}/treasury") + parse_dao_treasury(response) + end + + def get_dao_members(dao_id) + response = get("/daos/#{encode(dao_id)}/members") + response["members"] || [] + end + + # ==================== Vesting Operations ==================== + + def create_vesting_schedule(schedule) + body = vesting_schedule_to_hash(schedule) + response = post("/vesting", body) + parse_vesting_contract(response) + end + + def get_vesting_contract(contract_id) + response = get("/vesting/#{encode(contract_id)}") + parse_vesting_contract(response) + end + + def list_vesting_contracts(beneficiary: nil) + path = beneficiary ? "/vesting?beneficiary=#{encode(beneficiary)}" : "/vesting" + response = get(path) + (response["contracts"] || []).map { |c| parse_vesting_contract(c) } + end + + def claim_vested(contract_id) + response = post("/vesting/#{encode(contract_id)}/claim", {}) + parse_claim_receipt(response) + end + + def revoke_vesting(contract_id) + response = post("/vesting/#{encode(contract_id)}/revoke", {}) + parse_vesting_contract(response) + end + + def get_releasable_amount(contract_id) + response = get("/vesting/#{encode(contract_id)}/releasable") + response["amount"] + end + + # ==================== Lifecycle ==================== + + def health_check + response = get("/health") + response["status"] == "healthy" + rescue StandardError + false + end + + def close + @closed = true + @conn.close if @conn.respond_to?(:close) + end + + private + + def get(path, params = {}) + execute { @conn.get(path, params).body } + end + + def post(path, body) + execute { @conn.post(path, body).body } + end + + def execute + raise ClientClosedError, "Client has been closed" if @closed + + last_error = nil + @config.retries.times do |attempt| + begin + response = yield + check_error(response) if response.is_a?(Hash) + return response + rescue StandardError => e + last_error = e + sleep(2**attempt) if attempt < @config.retries - 1 + end + end + raise last_error + end + + def check_error(response) + return unless response["error"] || (response["code"] && response["message"]) + + message = response["message"] || response["error"] || "Unknown error" + code = response["code"] + status = response["status_code"] || 0 + raise HttpError.new(message, status_code: status, code: code) + end + + def encode(str) + URI.encode_www_form_component(str) + end + + # Parsing methods + + def parse_proposal(data) + Proposal.new( + id: data["id"], + title: data["title"], + description: data["description"], + discussion_url: data["discussion_url"], + proposer: data["proposer"], + status: data["status"], + created_at: data["created_at"], + voting_start_time: data["voting_start_time"], + voting_end_time: data["voting_end_time"], + execution_time: data["execution_time"], + vote_tally: data["vote_tally"] ? parse_vote_tally(data["vote_tally"]) : nil, + actions: (data["actions"] || []).map { |a| parse_proposal_action(a) }, + dao_id: data["dao_id"] + ) + end + + def parse_vote_tally(data) + VoteTally.new( + for_votes: data["for_votes"], + against_votes: data["against_votes"], + abstain_votes: data["abstain_votes"], + quorum: data["quorum"], + quorum_required: data["quorum_required"], + total_voters: data["total_voters"] + ) + end + + def parse_proposal_action(data) + ProposalAction.new( + target: data["target"], + method: data["method"], + data: data["data"], + value: data["value"] + ) + end + + def parse_vote_receipt(data) + VoteReceipt.new( + id: data["id"], + proposal_id: data["proposal_id"], + voter: data["voter"], + choice: data["choice"], + weight: data["weight"], + reason: data["reason"], + voted_at: data["voted_at"], + tx_hash: data["tx_hash"] + ) + end + + def parse_voting_power(data) + VotingPower.new( + address: data["address"], + delegated_power: data["delegated_power"], + own_power: data["own_power"], + total_power: data["total_power"], + delegators: data["delegators"] || [] + ) + end + + def parse_delegation_receipt(data) + DelegationReceipt.new( + id: data["id"], + from: data["from"], + to: data["to"], + amount: data["amount"], + delegated_at: data["delegated_at"], + tx_hash: data["tx_hash"] + ) + end + + def parse_dao(data) + Dao.new( + id: data["id"], + name: data["name"], + description: data["description"], + type: data["type"], + token_address: data["token_address"], + voting_period_days: data["voting_period_days"], + timelock_days: data["timelock_days"], + quorum_percent: data["quorum_percent"], + proposal_threshold: data["proposal_threshold"], + total_proposals: data["total_proposals"], + active_proposals: data["active_proposals"], + treasury_value: data["treasury_value"], + member_count: data["member_count"], + created_at: data["created_at"] + ) + end + + def parse_dao_treasury(data) + DaoTreasury.new( + dao_id: data["dao_id"], + total_value: data["total_value"], + tokens: (data["tokens"] || []).map { |t| parse_treasury_token(t) }, + last_updated: data["last_updated"] + ) + end + + def parse_treasury_token(data) + TreasuryToken.new( + address: data["address"], + balance: data["balance"], + name: data["name"], + symbol: data["symbol"] + ) + end + + def parse_vesting_contract(data) + VestingContract.new( + id: data["id"], + beneficiary: data["beneficiary"], + grantor: data["grantor"], + total_amount: data["total_amount"], + released_amount: data["released_amount"], + releasable_amount: data["releasable_amount"], + start_time: data["start_time"], + cliff_time: data["cliff_time"], + end_time: data["end_time"], + status: data["status"], + revocable: data["revocable"], + created_at: data["created_at"] + ) + end + + def parse_claim_receipt(data) + ClaimReceipt.new( + id: data["id"], + contract_id: data["contract_id"], + amount: data["amount"], + tx_hash: data["tx_hash"], + claimed_at: data["claimed_at"] + ) + end + + # Conversion methods + + def proposal_draft_to_hash(draft) + hash = { title: draft.title, description: draft.description } + hash[:discussion_url] = draft.discussion_url if draft.discussion_url + hash[:voting_start_time] = draft.voting_start_time if draft.voting_start_time + hash[:voting_end_time] = draft.voting_end_time if draft.voting_end_time + hash[:dao_id] = draft.dao_id if draft.dao_id + hash[:actions] = draft.actions.map { |a| proposal_action_to_hash(a) } if draft.actions + hash + end + + def proposal_action_to_hash(action) + { target: action.target, method: action.method, data: action.data, value: action.value } + end + + def dao_config_to_hash(config) + hash = { + name: config.name, + description: config.description, + type: config.type, + voting_period_days: config.voting_period_days, + timelock_days: config.timelock_days + } + hash[:token_address] = config.token_address if config.token_address + hash[:quorum_percent] = config.quorum_percent if config.quorum_percent + hash[:proposal_threshold] = config.proposal_threshold if config.proposal_threshold + hash[:multisig_members] = config.multisig_members if config.multisig_members + hash[:multisig_threshold] = config.multisig_threshold if config.multisig_threshold + hash + end + + def vesting_schedule_to_hash(schedule) + { + beneficiary: schedule.beneficiary, + total_amount: schedule.total_amount, + start_time: schedule.start_time, + cliff_duration: schedule.cliff_duration, + vesting_duration: schedule.vesting_duration, + revocable: schedule.revocable + } + end + end +end diff --git a/sdk/ruby/lib/synor_governance/types.rb b/sdk/ruby/lib/synor_governance/types.rb new file mode 100644 index 0000000..a10587e --- /dev/null +++ b/sdk/ruby/lib/synor_governance/types.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module SynorGovernance + # Proposal status + module ProposalStatus + PENDING = "pending" + ACTIVE = "active" + PASSED = "passed" + REJECTED = "rejected" + EXECUTED = "executed" + CANCELLED = "cancelled" + end + + # Vote choice + module VoteChoice + FOR = "for" + AGAINST = "against" + ABSTAIN = "abstain" + end + + # DAO type + module DaoType + TOKEN = "token" + MULTISIG = "multisig" + HYBRID = "hybrid" + end + + # Vesting status + module VestingStatus + ACTIVE = "active" + COMPLETED = "completed" + REVOKED = "revoked" + end + + # Configuration + Config = Struct.new(:api_key, :endpoint, :timeout, :retries, :debug, keyword_init: true) do + def initialize(api_key:, endpoint: "https://governance.synor.io/v1", timeout: 30, retries: 3, debug: false) + super + end + end + + # Data types + ProposalAction = Struct.new(:target, :method, :data, :value, keyword_init: true) + + ProposalDraft = Struct.new(:title, :description, :discussion_url, :voting_start_time, :voting_end_time, :dao_id, :actions, keyword_init: true) + + VoteTally = Struct.new(:for_votes, :against_votes, :abstain_votes, :quorum, :quorum_required, :total_voters, keyword_init: true) + + Proposal = Struct.new(:id, :title, :description, :discussion_url, :proposer, :status, :created_at, :voting_start_time, :voting_end_time, :execution_time, :vote_tally, :actions, :dao_id, keyword_init: true) + + ProposalFilter = Struct.new(:status, :proposer, :dao_id, :limit, :offset, keyword_init: true) + + Vote = Struct.new(:choice, :reason, keyword_init: true) + + VoteReceipt = Struct.new(:id, :proposal_id, :voter, :choice, :weight, :reason, :voted_at, :tx_hash, keyword_init: true) + + VotingPower = Struct.new(:address, :delegated_power, :own_power, :total_power, :delegators, keyword_init: true) + + DelegationReceipt = Struct.new(:id, :from, :to, :amount, :delegated_at, :tx_hash, keyword_init: true) + + DaoConfig = Struct.new(:name, :description, :type, :voting_period_days, :timelock_days, :token_address, :quorum_percent, :proposal_threshold, :multisig_members, :multisig_threshold, keyword_init: true) + + Dao = Struct.new(:id, :name, :description, :type, :token_address, :voting_period_days, :timelock_days, :quorum_percent, :proposal_threshold, :total_proposals, :active_proposals, :treasury_value, :member_count, :created_at, keyword_init: true) + + TreasuryToken = Struct.new(:address, :balance, :name, :symbol, keyword_init: true) + + DaoTreasury = Struct.new(:dao_id, :total_value, :tokens, :last_updated, keyword_init: true) + + VestingSchedule = Struct.new(:beneficiary, :total_amount, :start_time, :cliff_duration, :vesting_duration, :revocable, keyword_init: true) + + VestingContract = Struct.new(:id, :beneficiary, :grantor, :total_amount, :released_amount, :releasable_amount, :start_time, :cliff_time, :end_time, :status, :revocable, :created_at, keyword_init: true) + + ClaimReceipt = Struct.new(:id, :contract_id, :amount, :tx_hash, :claimed_at, keyword_init: true) +end diff --git a/sdk/ruby/lib/synor_governance/version.rb b/sdk/ruby/lib/synor_governance/version.rb new file mode 100644 index 0000000..41d5b92 --- /dev/null +++ b/sdk/ruby/lib/synor_governance/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorGovernance + VERSION = "0.1.0" +end diff --git a/sdk/ruby/lib/synor_mining.rb b/sdk/ruby/lib/synor_mining.rb new file mode 100644 index 0000000..218d8f9 --- /dev/null +++ b/sdk/ruby/lib/synor_mining.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative "synor_mining/version" +require_relative "synor_mining/types" +require_relative "synor_mining/client" + +module SynorMining + class Error < StandardError; end + class ClientClosedError < Error; end + + class HttpError < Error + attr_reader :status_code, :code + + def initialize(message, status_code: nil, code: nil) + super(message) + @status_code = status_code + @code = code + end + end +end diff --git a/sdk/ruby/lib/synor_mining/client.rb b/sdk/ruby/lib/synor_mining/client.rb new file mode 100644 index 0000000..b93e49d --- /dev/null +++ b/sdk/ruby/lib/synor_mining/client.rb @@ -0,0 +1,437 @@ +# frozen_string_literal: true + +require "faraday" +require "json" +require "uri" + +module SynorMining + # Synor Mining SDK client for Ruby. + # Pool connections, block templates, hashrate stats, and GPU management. + class Client + attr_reader :closed, :active_connection + + def initialize(config) + @config = config + @closed = false + @active_connection = nil + @conn = Faraday.new(url: config.endpoint) do |f| + f.request :json + f.response :json + f.options.timeout = config.timeout + f.headers["Authorization"] = "Bearer #{config.api_key}" + f.headers["Content-Type"] = "application/json" + f.headers["X-SDK-Version"] = "ruby/#{VERSION}" + end + end + + # ==================== Pool Operations ==================== + + def connect(pool:) + body = { url: pool.url, user: pool.user } + body[:password] = pool.password if pool.password + body[:algorithm] = pool.algorithm if pool.algorithm + body[:difficulty] = pool.difficulty if pool.difficulty + response = post("/pool/connect", body) + @active_connection = parse_stratum_connection(response) + end + + def disconnect + return unless @active_connection + + post("/pool/disconnect", {}) + @active_connection = nil + end + + def get_connection + response = get("/pool/connection") + @active_connection = parse_stratum_connection(response) + end + + def get_pool_stats + response = get("/pool/stats") + parse_pool_stats(response) + end + + def connected? + @active_connection&.status == ConnectionStatus::CONNECTED + end + + # ==================== Mining Operations ==================== + + def get_block_template + response = get("/mining/template") + parse_block_template(response) + end + + def submit_work(work:) + body = { + template_id: work.template_id, + nonce: work.nonce, + extra_nonce: work.extra_nonce, + timestamp: work.timestamp, + hash: work.hash + } + response = post("/mining/submit", body) + parse_submit_result(response) + end + + def start_mining(algorithm: nil) + body = algorithm ? { algorithm: algorithm } : {} + post("/mining/start", body) + end + + def stop_mining + post("/mining/stop", {}) + end + + def mining? + response = get("/mining/status") + response["mining"] == true + end + + # ==================== Stats Operations ==================== + + def get_hashrate + response = get("/stats/hashrate") + parse_hashrate(response) + end + + def get_stats + response = get("/stats") + parse_mining_stats(response) + end + + def get_earnings(period: nil) + path = period ? "/stats/earnings?period=#{period}" : "/stats/earnings" + response = get(path) + parse_earnings(response) + end + + def get_share_stats + response = get("/stats/shares") + parse_share_stats(response) + end + + # ==================== Device Operations ==================== + + def list_devices + response = get("/devices") + (response["devices"] || []).map { |d| parse_mining_device(d) } + end + + def get_device(device_id) + response = get("/devices/#{encode(device_id)}") + parse_mining_device(response) + end + + def set_device_config(device_id:, config:) + body = { enabled: config.enabled } + body[:intensity] = config.intensity if config.intensity + body[:power_limit] = config.power_limit if config.power_limit + body[:core_clock_offset] = config.core_clock_offset if config.core_clock_offset + body[:memory_clock_offset] = config.memory_clock_offset if config.memory_clock_offset + body[:fan_speed] = config.fan_speed if config.fan_speed + response = put("/devices/#{encode(device_id)}/config", body) + parse_mining_device(response) + end + + def enable_device(device_id) + set_device_config(device_id: device_id, config: DeviceConfig.new(enabled: true)) + end + + def disable_device(device_id) + set_device_config(device_id: device_id, config: DeviceConfig.new(enabled: false)) + end + + # ==================== Worker Operations ==================== + + def list_workers + response = get("/workers") + (response["workers"] || []).map { |w| parse_worker_info(w) } + end + + def get_worker(worker_id) + response = get("/workers/#{encode(worker_id)}") + parse_worker_info(response) + end + + def remove_worker(worker_id) + delete("/workers/#{encode(worker_id)}") + end + + # ==================== Algorithm Operations ==================== + + def list_algorithms + response = get("/algorithms") + (response["algorithms"] || []).map { |a| parse_mining_algorithm(a) } + end + + def get_algorithm(name) + response = get("/algorithms/#{encode(name)}") + parse_mining_algorithm(response) + end + + def switch_algorithm(algorithm) + post("/algorithms/switch", { algorithm: algorithm }) + end + + def get_most_profitable + response = get("/algorithms/profitable") + parse_mining_algorithm(response) + end + + # ==================== Lifecycle ==================== + + def health_check + response = get("/health") + response["status"] == "healthy" + rescue StandardError + false + end + + def close + @closed = true + @active_connection = nil + @conn.close if @conn.respond_to?(:close) + end + + private + + def get(path, params = {}) + execute { @conn.get(path, params).body } + end + + def post(path, body) + execute { @conn.post(path, body).body } + end + + def put(path, body) + execute { @conn.put(path, body).body } + end + + def delete(path) + execute { @conn.delete(path).body } + end + + def execute + raise ClientClosedError, "Client has been closed" if @closed + + last_error = nil + @config.retries.times do |attempt| + begin + response = yield + check_error(response) if response.is_a?(Hash) + return response + rescue StandardError => e + last_error = e + sleep(2**attempt) if attempt < @config.retries - 1 + end + end + raise last_error + end + + def check_error(response) + return unless response["error"] || (response["code"] && response["message"]) + + message = response["message"] || response["error"] || "Unknown error" + code = response["code"] + status = response["status_code"] || 0 + raise HttpError.new(message, status_code: status, code: code) + end + + def encode(str) + URI.encode_www_form_component(str) + end + + # Parsing methods + + def parse_stratum_connection(data) + StratumConnection.new( + id: data["id"], + pool: data["pool"], + status: data["status"], + algorithm: data["algorithm"], + difficulty: data["difficulty"], + connected_at: data["connected_at"], + accepted_shares: data["accepted_shares"], + rejected_shares: data["rejected_shares"], + stale_shares: data["stale_shares"], + last_share_at: data["last_share_at"] + ) + end + + def parse_pool_stats(data) + PoolStats.new( + url: data["url"], + workers: data["workers"], + hashrate: data["hashrate"], + difficulty: data["difficulty"], + last_block: data["last_block"], + blocks_found_24h: data["blocks_found_24h"], + luck: data["luck"] + ) + end + + def parse_block_template(data) + BlockTemplate.new( + id: data["id"], + previous_block_hash: data["previous_block_hash"], + merkle_root: data["merkle_root"], + timestamp: data["timestamp"], + bits: data["bits"], + height: data["height"], + coinbase_value: data["coinbase_value"], + transactions: (data["transactions"] || []).map { |t| parse_template_transaction(t) }, + target: data["target"], + algorithm: data["algorithm"], + extra_nonce: data["extra_nonce"] + ) + end + + def parse_template_transaction(data) + TemplateTransaction.new( + txid: data["txid"], + data: data["data"], + fee: data["fee"], + weight: data["weight"] + ) + end + + def parse_submit_result(data) + SubmitResult.new( + status: data["status"], + share: data["share"] ? parse_share_info(data["share"]) : nil, + block_found: data["block_found"], + reason: data["reason"], + block_hash: data["block_hash"], + reward: data["reward"] + ) + end + + def parse_share_info(data) + ShareInfo.new( + hash: data["hash"], + difficulty: data["difficulty"], + timestamp: data["timestamp"], + accepted: data["accepted"] + ) + end + + def parse_hashrate(data) + Hashrate.new( + current: data["current"], + average_1h: data["average_1h"], + average_24h: data["average_24h"], + peak: data["peak"], + unit: data["unit"] + ) + end + + def parse_share_stats(data) + ShareStats.new( + accepted: data["accepted"], + rejected: data["rejected"], + stale: data["stale"], + total: data["total"], + accept_rate: data["accept_rate"] + ) + end + + def parse_mining_stats(data) + MiningStats.new( + hashrate: data["hashrate"] ? parse_hashrate(data["hashrate"]) : nil, + shares: data["shares"] ? parse_share_stats(data["shares"]) : nil, + uptime: data["uptime"], + efficiency: data["efficiency"], + earnings: data["earnings"] ? parse_earnings_snapshot(data["earnings"]) : nil, + power_consumption: data["power_consumption"], + temperature: data["temperature"] ? parse_device_temperature(data["temperature"]) : nil + ) + end + + def parse_earnings_snapshot(data) + EarningsSnapshot.new( + today: data["today"], + yesterday: data["yesterday"], + this_week: data["this_week"], + this_month: data["this_month"], + total: data["total"], + currency: data["currency"] + ) + end + + def parse_device_temperature(data) + DeviceTemperature.new( + current: data["current"], + max: data["max"], + throttling: data["throttling"] + ) + end + + def parse_earnings(data) + Earnings.new( + period: data["period"], + start_date: data["start_date"], + end_date: data["end_date"], + amount: data["amount"], + blocks: data["blocks"], + shares: data["shares"], + average_hashrate: data["average_hashrate"], + currency: data["currency"], + breakdown: (data["breakdown"] || []).map { |b| parse_earnings_breakdown(b) } + ) + end + + def parse_earnings_breakdown(data) + EarningsBreakdown.new( + date: data["date"], + amount: data["amount"], + blocks: data["blocks"], + shares: data["shares"], + hashrate: data["hashrate"] + ) + end + + def parse_mining_device(data) + MiningDevice.new( + id: data["id"], + name: data["name"], + type: data["type"], + status: data["status"], + hashrate: data["hashrate"], + temperature: data["temperature"], + fan_speed: data["fan_speed"], + power_draw: data["power_draw"], + memory_used: data["memory_used"], + memory_total: data["memory_total"], + driver: data["driver"], + firmware: data["firmware"] + ) + end + + def parse_worker_info(data) + WorkerInfo.new( + id: data["id"], + name: data["name"], + status: data["status"], + hashrate: data["hashrate"] ? parse_hashrate(data["hashrate"]) : nil, + shares: data["shares"] ? parse_share_stats(data["shares"]) : nil, + devices: (data["devices"] || []).map { |d| parse_mining_device(d) }, + last_seen: data["last_seen"], + uptime: data["uptime"] + ) + end + + def parse_mining_algorithm(data) + MiningAlgorithm.new( + name: data["name"], + display_name: data["display_name"], + hash_unit: data["hash_unit"], + profitability: data["profitability"], + difficulty: data["difficulty"], + block_reward: data["block_reward"], + block_time: data["block_time"] + ) + end + end +end diff --git a/sdk/ruby/lib/synor_mining/types.rb b/sdk/ruby/lib/synor_mining/types.rb new file mode 100644 index 0000000..70ad272 --- /dev/null +++ b/sdk/ruby/lib/synor_mining/types.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module SynorMining + # Device types + module DeviceType + CPU = "cpu" + GPU_NVIDIA = "gpu_nvidia" + GPU_AMD = "gpu_amd" + ASIC = "asic" + end + + # Device status + module DeviceStatus + IDLE = "idle" + MINING = "mining" + ERROR = "error" + OFFLINE = "offline" + end + + # Connection status + module ConnectionStatus + DISCONNECTED = "disconnected" + CONNECTING = "connecting" + CONNECTED = "connected" + RECONNECTING = "reconnecting" + end + + # Time periods + module TimePeriod + HOUR = "hour" + DAY = "day" + WEEK = "week" + MONTH = "month" + ALL = "all" + end + + # Submit status + module SubmitStatus + ACCEPTED = "accepted" + REJECTED = "rejected" + STALE = "stale" + end + + # Configuration + Config = Struct.new(:api_key, :endpoint, :timeout, :retries, :debug, keyword_init: true) do + def initialize(api_key:, endpoint: "https://mining.synor.io/v1", timeout: 60, retries: 3, debug: false) + super + end + end + + # Data types + PoolConfig = Struct.new(:url, :user, :password, :algorithm, :difficulty, keyword_init: true) + + StratumConnection = Struct.new(:id, :pool, :status, :algorithm, :difficulty, :connected_at, :accepted_shares, :rejected_shares, :stale_shares, :last_share_at, keyword_init: true) + + PoolStats = Struct.new(:url, :workers, :hashrate, :difficulty, :last_block, :blocks_found_24h, :luck, keyword_init: true) + + TemplateTransaction = Struct.new(:txid, :data, :fee, :weight, keyword_init: true) + + BlockTemplate = Struct.new(:id, :previous_block_hash, :merkle_root, :timestamp, :bits, :height, :coinbase_value, :transactions, :target, :algorithm, :extra_nonce, keyword_init: true) + + MinedWork = Struct.new(:template_id, :nonce, :extra_nonce, :timestamp, :hash, keyword_init: true) + + ShareInfo = Struct.new(:hash, :difficulty, :timestamp, :accepted, keyword_init: true) + + SubmitResult = Struct.new(:status, :share, :block_found, :reason, :block_hash, :reward, keyword_init: true) + + Hashrate = Struct.new(:current, :average_1h, :average_24h, :peak, :unit, keyword_init: true) + + ShareStats = Struct.new(:accepted, :rejected, :stale, :total, :accept_rate, keyword_init: true) + + DeviceTemperature = Struct.new(:current, :max, :throttling, keyword_init: true) + + EarningsSnapshot = Struct.new(:today, :yesterday, :this_week, :this_month, :total, :currency, keyword_init: true) + + MiningStats = Struct.new(:hashrate, :shares, :uptime, :efficiency, :earnings, :power_consumption, :temperature, keyword_init: true) + + EarningsBreakdown = Struct.new(:date, :amount, :blocks, :shares, :hashrate, keyword_init: true) + + Earnings = Struct.new(:period, :start_date, :end_date, :amount, :blocks, :shares, :average_hashrate, :currency, :breakdown, keyword_init: true) + + MiningDevice = Struct.new(:id, :name, :type, :status, :hashrate, :temperature, :fan_speed, :power_draw, :memory_used, :memory_total, :driver, :firmware, keyword_init: true) + + DeviceConfig = Struct.new(:enabled, :intensity, :power_limit, :core_clock_offset, :memory_clock_offset, :fan_speed, keyword_init: true) + + WorkerInfo = Struct.new(:id, :name, :status, :hashrate, :shares, :devices, :last_seen, :uptime, keyword_init: true) + + MiningAlgorithm = Struct.new(:name, :display_name, :hash_unit, :profitability, :difficulty, :block_reward, :block_time, keyword_init: true) +end diff --git a/sdk/ruby/lib/synor_mining/version.rb b/sdk/ruby/lib/synor_mining/version.rb new file mode 100644 index 0000000..3e015b5 --- /dev/null +++ b/sdk/ruby/lib/synor_mining/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SynorMining + VERSION = "0.1.0" +end diff --git a/sdk/swift/Sources/Synor/Economics/SynorEconomics.swift b/sdk/swift/Sources/Synor/Economics/SynorEconomics.swift new file mode 100644 index 0000000..1ae638e --- /dev/null +++ b/sdk/swift/Sources/Synor/Economics/SynorEconomics.swift @@ -0,0 +1,286 @@ +import Foundation + +/// Synor Economics SDK client for Swift. +/// +/// Provides pricing, billing, staking, and discount operations. +/// +/// Example: +/// ```swift +/// let economics = SynorEconomics(config: EconomicsConfig(apiKey: "your-api-key")) +/// +/// // Get price for compute usage +/// let usage = UsageMetrics(computeHours: 100, gpuHours: 10) +/// let price = try await economics.getPrice(service: .compute, usage: usage) +/// +/// // Stake tokens +/// let receipt = try await economics.stake(amount: "1000000000000000000") +/// ``` +public class SynorEconomics { + private let config: EconomicsConfig + private let session: URLSession + private let decoder: JSONDecoder + private let encoder: JSONEncoder + private var closed = false + + public init(config: EconomicsConfig) { + 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: - Pricing Operations + + /// Get price for service usage. + public func getPrice(service: ServiceType, usage: UsageMetrics) async throws -> Price { + let body: [String: Any] = [ + "service": service.rawValue, + "usage": [ + "computeHours": usage.computeHours, + "storageGb": usage.storageGb, + "requests": usage.requests, + "bandwidthGb": usage.bandwidthGb, + "gpuHours": usage.gpuHours + ] + ] + return try await post(path: "/pricing/calculate", body: body) + } + + /// Estimate cost for a usage plan. + public func estimateCost(plan: UsagePlan) async throws -> CostEstimate { + let body: [String: Any] = [ + "service": plan.service.rawValue, + "estimatedUsage": [ + "computeHours": plan.estimatedUsage.computeHours, + "storageGb": plan.estimatedUsage.storageGb, + "requests": plan.estimatedUsage.requests, + "bandwidthGb": plan.estimatedUsage.bandwidthGb, + "gpuHours": plan.estimatedUsage.gpuHours + ], + "period": plan.period.rawValue + ] + return try await post(path: "/pricing/estimate", body: body) + } + + /// Get pricing tiers for a service. + public func getPricingTiers(service: ServiceType) async throws -> [PricingTier] { + let response: TiersResponse = try await get(path: "/pricing/tiers/\(service.rawValue)") + return response.tiers ?? [] + } + + // MARK: - Billing Operations + + /// Get usage for a billing period. + public func getUsage(period: BillingPeriod? = nil) async throws -> Usage { + var path = "/billing/usage" + if let period = period { + path += "?period=\(period.rawValue)" + } + return try await get(path: path) + } + + /// Get usage by date range. + public func getUsageByDateRange(startDate: Int64, endDate: Int64) async throws -> Usage { + return try await get(path: "/billing/usage?startDate=\(startDate)&endDate=\(endDate)") + } + + /// Get all invoices. + public func getInvoices() async throws -> [Invoice] { + let response: InvoicesResponse = try await get(path: "/billing/invoices") + return response.invoices ?? [] + } + + /// Get invoice by ID. + public func getInvoice(invoiceId: String) async throws -> Invoice { + return try await get(path: "/billing/invoices/\(invoiceId.urlEncoded)") + } + + /// Download invoice as PDF data. + public func downloadInvoice(invoiceId: String) async throws -> Data { + let response: [String: String] = try await get(path: "/billing/invoices/\(invoiceId.urlEncoded)/pdf") + guard let base64 = response["data"], + let data = Data(base64Encoded: base64) else { + throw EconomicsError(message: "Invalid PDF data") + } + return data + } + + /// Get account balance. + public func getBalance() async throws -> AccountBalance { + return try await get(path: "/billing/balance") + } + + /// Add credits to account. + public func addCredits(amount: String, paymentMethod: String) async throws { + let body = ["amount": amount, "paymentMethod": paymentMethod] + let _: [String: Any] = try await post(path: "/billing/credits", body: body) + } + + // MARK: - Staking Operations + + /// Stake tokens. + public func stake(amount: String, durationDays: Int? = nil) async throws -> StakeReceipt { + var body: [String: Any] = ["amount": amount] + if let duration = durationDays { body["durationDays"] = duration } + return try await post(path: "/staking/stake", body: body) + } + + /// Unstake tokens. + public func unstake(stakeId: String) async throws -> UnstakeReceipt { + return try await post(path: "/staking/unstake/\(stakeId.urlEncoded)", body: [:] as [String: String]) + } + + /// Get all stakes. + public func getStakes() async throws -> [StakeInfo] { + let response: StakesResponse = try await get(path: "/staking/stakes") + return response.stakes ?? [] + } + + /// Get stake by ID. + public func getStake(stakeId: String) async throws -> StakeInfo { + return try await get(path: "/staking/stakes/\(stakeId.urlEncoded)") + } + + /// Get staking rewards. + public func getStakingRewards() async throws -> Rewards { + return try await get(path: "/staking/rewards") + } + + /// Claim staking rewards. + public func claimRewards() async throws -> String { + let response: [String: String] = try await post(path: "/staking/rewards/claim", body: [:] as [String: String]) + return response["txHash"] ?? "" + } + + /// Get current APY. + public func getCurrentApy() async throws -> Double { + let response: [String: Double] = try await get(path: "/staking/apy") + return response["apy"] ?? 0 + } + + // MARK: - Discount Operations + + /// Apply a discount code. + public func applyDiscount(code: String) async throws -> AppliedDiscount { + return try await post(path: "/discounts/apply", body: ["code": code]) + } + + /// Get available discounts. + public func getAvailableDiscounts() async throws -> [Discount] { + let response: DiscountsResponse = try await get(path: "/discounts/available") + return response.discounts ?? [] + } + + /// Validate a discount code. + public func validateDiscount(code: String) async throws -> Discount { + return try await get(path: "/discounts/validate/\(code.urlEncoded)") + } + + /// Get applied discounts. + public func getAppliedDiscounts() async throws -> [AppliedDiscount] { + let response: AppliedDiscountsResponse = try await get(path: "/discounts/applied") + return response.discounts ?? [] + } + + /// Remove a discount. + public func removeDiscount(discountId: String) async throws { + let _: [String: Any] = try await delete(path: "/discounts/\(discountId.urlEncoded)") + } + + // MARK: - Lifecycle + + /// Close the client. + public func close() { + closed = true + } + + /// Check if client is closed. + public var isClosed: Bool { closed } + + /// Health check. + 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 delete(path: String) async throws -> T { + return try await request(method: "DELETE", path: path) + } + + private func request(method: String, path: String, body: Any? = nil) async throws -> T { + guard !closed else { throw EconomicsError(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 EconomicsError(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 EconomicsError(message: "Invalid response") + } + + if httpResponse.statusCode >= 400 { + let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + throw EconomicsError( + message: errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)", + code: errorInfo?["code"] as? String, + statusCode: httpResponse.statusCode + ) + } + + return try decoder.decode(T.self, from: data) + } +} + +// MARK: - Extensions + +extension String { + var urlEncoded: String { + addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self + } +} diff --git a/sdk/swift/Sources/Synor/Economics/Types.swift b/sdk/swift/Sources/Synor/Economics/Types.swift new file mode 100644 index 0000000..d229338 --- /dev/null +++ b/sdk/swift/Sources/Synor/Economics/Types.swift @@ -0,0 +1,314 @@ +import Foundation + +// MARK: - Configuration + +/// Configuration for the Synor Economics client. +public struct EconomicsConfig { + public let apiKey: String + public let endpoint: String + public let timeout: TimeInterval + public let retries: Int + public let debug: Bool + + public init( + apiKey: String, + endpoint: String = "https://economics.synor.io/v1", + timeout: TimeInterval = 60, + retries: Int = 3, + debug: Bool = false + ) { + self.apiKey = apiKey + self.endpoint = endpoint + self.timeout = timeout + self.retries = retries + self.debug = debug + } +} + +// MARK: - Enums + +/// Service types for pricing. +public enum ServiceType: String, Codable, Sendable { + case compute + case storage + case database + case hosting + case bridge + case mining + case rpc +} + +/// Billing period. +public enum BillingPeriod: String, Codable, Sendable { + case daily + case weekly + case monthly + case yearly +} + +/// Stake status. +public enum StakeStatus: String, Codable, Sendable { + case active + case unstaking + case unlocked + case slashed +} + +/// Discount type. +public enum DiscountType: String, Codable, Sendable { + case percentage + case fixed + case volume +} + +// MARK: - Pricing Types + +/// Usage metrics for pricing calculation. +public struct UsageMetrics: Codable, Sendable { + public var computeHours: Double + public var storageGb: Double + public var requests: Int64 + public var bandwidthGb: Double + public var gpuHours: Double + + public init( + computeHours: Double = 0, + storageGb: Double = 0, + requests: Int64 = 0, + bandwidthGb: Double = 0, + gpuHours: Double = 0 + ) { + self.computeHours = computeHours + self.storageGb = storageGb + self.requests = requests + self.bandwidthGb = bandwidthGb + self.gpuHours = gpuHours + } +} + +/// Pricing tier. +public struct PricingTier: Codable, Sendable { + public let name: String + public let upTo: Double + public let pricePerUnit: Double + public let unit: String +} + +/// Price result. +public struct Price: Codable, Sendable { + public let service: ServiceType + public let amount: String + public let currency: String + public let amountUsd: String + public let breakdown: [PricingTier]? + public let discount: String? + public let finalAmount: String +} + +/// Usage plan for cost estimation. +public struct UsagePlan: Codable, Sendable { + public let service: ServiceType + public let estimatedUsage: UsageMetrics + public let period: BillingPeriod + + public init(service: ServiceType, estimatedUsage: UsageMetrics, period: BillingPeriod) { + self.service = service + self.estimatedUsage = estimatedUsage + self.period = period + } +} + +/// Cost breakdown item. +public struct CostBreakdown: Codable, Sendable { + public let category: String + public let amount: String + public let percentage: Double +} + +/// Cost estimate result. +public struct CostEstimate: Codable, Sendable { + public let service: ServiceType + public let period: BillingPeriod + public let estimatedCost: String + public let estimatedCostUsd: String + public let currency: String + public let confidenceLevel: String + public let breakdown: [CostBreakdown]? + public let savingsFromStaking: String? +} + +// MARK: - Billing Types + +/// Usage record. +public struct UsageRecord: Codable, Sendable { + public let service: ServiceType + public let resource: String + public let quantity: Double + public let unit: String + public let timestamp: Int64 + public let cost: String +} + +/// Usage summary. +public struct Usage: Codable, Sendable { + public let period: BillingPeriod + public let startDate: Int64 + public let endDate: Int64 + public let records: [UsageRecord] + public let totalCost: String + public let currency: String +} + +/// Invoice line item. +public struct InvoiceLineItem: Codable, Sendable { + public let description: String + public let quantity: Double + public let unit: String + public let unitPrice: String + public let amount: String +} + +/// Invoice. +public struct Invoice: Codable, Sendable { + public let id: String + public let number: String + public let periodStart: Int64 + public let periodEnd: Int64 + public let subtotal: String + public let tax: String + public let discount: String + public let total: String + public let currency: String + public let status: String + public let dueDate: Int64? + public let paidAt: Int64? + public let lineItems: [InvoiceLineItem] + public let pdfUrl: String? +} + +/// Account balance. +public struct AccountBalance: Codable, Sendable { + public let available: String + public let pending: String + public let reserved: String + public let staked: String + public let currency: String + public let lastUpdated: Int64 +} + +// MARK: - Staking Types + +/// Stake receipt. +public struct StakeReceipt: Codable, Sendable { + public let id: String + public let txHash: String + public let amount: String + public let startDate: Int64 + public let endDate: Int64 + public let apy: Double + public let status: StakeStatus +} + +/// Stake info. +public struct StakeInfo: Codable, Sendable { + public let id: String + public let amount: String + public let status: StakeStatus + public let startDate: Int64 + public let endDate: Int64 + public let apy: Double + public let earnedRewards: String + public let pendingRewards: String + public let unlockDate: Int64? + public let discountPercent: Int +} + +/// Unstake receipt. +public struct UnstakeReceipt: Codable, Sendable { + public let id: String + public let txHash: String + public let amount: String + public let initiatedAt: Int64 + public let availableAt: Int64 + public let penalty: String? +} + +/// Reward record. +public struct RewardRecord: Codable, Sendable { + public let date: Int64 + public let amount: String + public let type: String + public let txHash: String? +} + +/// Staking rewards. +public struct Rewards: Codable, Sendable { + public let totalEarned: String + public let pendingClaim: String + public let lastClaimDate: String? + public let currentApy: Double + public let history: [RewardRecord] +} + +// MARK: - Discount Types + +/// Discount. +public struct Discount: Codable, Sendable { + public let id: String + public let code: String + public let type: DiscountType + public let value: String + public let validFrom: Int64? + public let validUntil: Int64? + public let usageLimit: Int? + public let usageCount: Int + public let applicableServices: [ServiceType]? + public let minimumSpend: String? + public let active: Bool +} + +/// Applied discount. +public struct AppliedDiscount: Codable, Sendable { + public let discount: Discount + public let savedAmount: String + public let appliedAt: Int64 +} + +// MARK: - Response Types + +struct TiersResponse: Codable { + let tiers: [PricingTier]? +} + +struct InvoicesResponse: Codable { + let invoices: [Invoice]? +} + +struct StakesResponse: Codable { + let stakes: [StakeInfo]? +} + +struct DiscountsResponse: Codable { + let discounts: [Discount]? +} + +struct AppliedDiscountsResponse: Codable { + let discounts: [AppliedDiscount]? +} + +// MARK: - Error + +/// Error thrown by Economics operations. +public struct EconomicsError: Error, LocalizedError { + public let message: String + public let code: String? + public let statusCode: Int + + public init(message: String, code: String? = nil, statusCode: Int = 0) { + self.message = message + self.code = code + self.statusCode = statusCode + } + + public var errorDescription: String? { message } +} diff --git a/sdk/swift/Sources/Synor/Governance/SynorGovernance.swift b/sdk/swift/Sources/Synor/Governance/SynorGovernance.swift new file mode 100644 index 0000000..418374d --- /dev/null +++ b/sdk/swift/Sources/Synor/Governance/SynorGovernance.swift @@ -0,0 +1,286 @@ +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 + } +} diff --git a/sdk/swift/Sources/Synor/Governance/Types.swift b/sdk/swift/Sources/Synor/Governance/Types.swift new file mode 100644 index 0000000..0512c09 --- /dev/null +++ b/sdk/swift/Sources/Synor/Governance/Types.swift @@ -0,0 +1,374 @@ +import Foundation + +// MARK: - Configuration + +/// Configuration for the Synor Governance client. +public struct GovernanceConfig { + public let apiKey: String + public let endpoint: String + public let timeout: TimeInterval + public let retries: Int + public let debug: Bool + + public init( + apiKey: String, + endpoint: String = "https://governance.synor.io/v1", + timeout: TimeInterval = 60, + retries: Int = 3, + debug: Bool = false + ) { + self.apiKey = apiKey + self.endpoint = endpoint + self.timeout = timeout + self.retries = retries + self.debug = debug + } +} + +// MARK: - Enums + +/// Proposal status. +public enum ProposalStatus: String, Codable, Sendable { + case draft + case active + case passed + case rejected + case executed + case cancelled +} + +/// Vote choice. +public enum VoteChoice: String, Codable, Sendable { + case yes + case no + case abstain +} + +/// DAO type. +public enum DaoType: String, Codable, Sendable { + case token + case multisig + case hybrid +} + +/// Vesting status. +public enum VestingStatus: String, Codable, Sendable { + case pending + case active + case paused + case completed + case revoked +} + +// MARK: - Proposal Types + +/// Proposal action. +public struct ProposalAction: Codable, Sendable { + public var target: String + public var method: String + public var data: String + public var value: String? + + public init(target: String, method: String, data: String, value: String? = nil) { + self.target = target + self.method = method + self.data = data + self.value = value + } +} + +/// Proposal draft. +public struct ProposalDraft: Codable, Sendable { + public var title: String + public var description: String + public var discussionUrl: String? + public var votingStartTime: Int64? + public var votingEndTime: Int64? + public var actions: [ProposalAction]? + public var daoId: String? + + public init( + title: String, + description: String, + discussionUrl: String? = nil, + votingStartTime: Int64? = nil, + votingEndTime: Int64? = nil, + actions: [ProposalAction]? = nil, + daoId: String? = nil + ) { + self.title = title + self.description = description + self.discussionUrl = discussionUrl + self.votingStartTime = votingStartTime + self.votingEndTime = votingEndTime + self.actions = actions + self.daoId = daoId + } +} + +/// Vote breakdown. +public struct VoteBreakdown: Codable, Sendable { + public let yes: String + public let no: String + public let abstain: String + public let quorum: String + public let quorumPercent: Double + public let quorumReached: Bool +} + +/// Proposal. +public struct Proposal: Codable, Sendable { + public let id: String + public let title: String + public let description: String + public let proposer: String + public let status: ProposalStatus + public let createdAt: Int64 + public let votingStartTime: Int64 + public let votingEndTime: Int64 + public let votes: VoteBreakdown + public let discussionUrl: String? + public let actions: [ProposalAction]? + public let daoId: String? + public let snapshotBlock: String? + public let executedAt: Int64? + public let executedTxHash: String? +} + +/// Proposal filter. +public struct ProposalFilter { + public var status: ProposalStatus? + public var proposer: String? + public var daoId: String? + public var limit: Int? + public var offset: Int? + + public init( + status: ProposalStatus? = nil, + proposer: String? = nil, + daoId: String? = nil, + limit: Int? = nil, + offset: Int? = nil + ) { + self.status = status + self.proposer = proposer + self.daoId = daoId + self.limit = limit + self.offset = offset + } +} + +// MARK: - Voting Types + +/// Vote. +public struct Vote { + public var choice: VoteChoice + public var reason: String? + + public init(choice: VoteChoice, reason: String? = nil) { + self.choice = choice + self.reason = reason + } +} + +/// Vote receipt. +public struct VoteReceipt: Codable, Sendable { + public let id: String + public let proposalId: String + public let voter: String + public let choice: VoteChoice + public let weight: String + public let timestamp: Int64 + public let txHash: String + public let reason: String? +} + +/// Voting power. +public struct VotingPower: Codable, Sendable { + public let address: String + public let balance: String + public let delegatedTo: String? + public let delegatedFrom: String + public let totalPower: String + public let blockNumber: Int64 +} + +/// Delegation receipt. +public struct DelegationReceipt: Codable, Sendable { + public let id: String + public let delegator: String + public let delegatee: String + public let amount: String + public let timestamp: Int64 + public let txHash: String +} + +// MARK: - DAO Types + +/// DAO configuration. +public struct DaoConfig { + public var name: String + public var description: String + public var type: DaoType + public var tokenAddress: String? + public var quorumPercent: String? + public var votingPeriodDays: Int + public var timelockDays: Int + public var proposalThreshold: Int? + public var multisigMembers: [String]? + public var multisigThreshold: Int? + + public init( + name: String, + description: String, + type: DaoType, + tokenAddress: String? = nil, + quorumPercent: String? = nil, + votingPeriodDays: Int = 7, + timelockDays: Int = 2, + proposalThreshold: Int? = nil, + multisigMembers: [String]? = nil, + multisigThreshold: Int? = nil + ) { + self.name = name + self.description = description + self.type = type + self.tokenAddress = tokenAddress + self.quorumPercent = quorumPercent + self.votingPeriodDays = votingPeriodDays + self.timelockDays = timelockDays + self.proposalThreshold = proposalThreshold + self.multisigMembers = multisigMembers + self.multisigThreshold = multisigThreshold + } +} + +/// Treasury asset. +public struct TreasuryAsset: Codable, Sendable { + public let address: String + public let symbol: String + public let balance: String + public let valueUsd: String +} + +/// DAO treasury. +public struct DaoTreasury: Codable, Sendable { + public let address: String + public let balance: String + public let currency: String + public let assets: [TreasuryAsset] +} + +/// DAO. +public struct Dao: Codable, Sendable { + public let id: String + public let name: String + public let description: String + public let type: DaoType + public let tokenAddress: String? + public let governorAddress: String + public let timelockAddress: String + public let treasury: DaoTreasury + public let quorumPercent: String + public let votingPeriodDays: Int + public let timelockDays: Int + public let proposalCount: Int + public let memberCount: Int + public let createdAt: Int64 +} + +// MARK: - Vesting Types + +/// Vesting schedule. +public struct VestingSchedule { + public var beneficiary: String + public var totalAmount: String + public var startTime: Int64 + public var cliffDuration: Int64 + public var vestingDuration: Int64 + public var revocable: Bool + + public init( + beneficiary: String, + totalAmount: String, + startTime: Int64, + cliffDuration: Int64, + vestingDuration: Int64, + revocable: Bool = false + ) { + self.beneficiary = beneficiary + self.totalAmount = totalAmount + self.startTime = startTime + self.cliffDuration = cliffDuration + self.vestingDuration = vestingDuration + self.revocable = revocable + } +} + +/// Vesting contract. +public struct VestingContract: Codable, Sendable { + public let id: String + public let contractAddress: String + public let beneficiary: String + public let totalAmount: String + public let releasedAmount: String + public let releasableAmount: String + public let startTime: Int64 + public let cliffEnd: Int64 + public let vestingEnd: Int64 + public let status: VestingStatus + public let createdAt: Int64 + public let txHash: String +} + +/// Claim receipt. +public struct ClaimReceipt: Codable, Sendable { + public let vestingContractId: String + public let amount: String + public let txHash: String + public let timestamp: Int64 + public let remainingAmount: String +} + +// MARK: - Response Types + +struct ProposalsResponse: Codable { + let proposals: [Proposal]? +} + +struct VotesResponse: Codable { + let votes: [VoteReceipt]? +} + +struct DelegationsResponse: Codable { + let delegations: [DelegationReceipt]? +} + +struct DaosResponse: Codable { + let daos: [Dao]? +} + +struct MembersResponse: Codable { + let members: [String]? +} + +struct VestingContractsResponse: Codable { + let contracts: [VestingContract]? +} + +// MARK: - Error + +/// Error thrown by Governance operations. +public struct GovernanceError: Error, LocalizedError { + public let message: String + public let code: String? + public let statusCode: Int + + public init(message: String, code: String? = nil, statusCode: Int = 0) { + self.message = message + self.code = code + self.statusCode = statusCode + } + + public var errorDescription: String? { message } +} diff --git a/sdk/swift/Sources/Synor/Mining/SynorMining.swift b/sdk/swift/Sources/Synor/Mining/SynorMining.swift new file mode 100644 index 0000000..0c8900c --- /dev/null +++ b/sdk/swift/Sources/Synor/Mining/SynorMining.swift @@ -0,0 +1,279 @@ +import Foundation + +/// Synor Mining SDK client for Swift. +/// +/// Provides pool connections, block templates, hashrate stats, and GPU management. +public class SynorMining { + private let config: MiningConfig + private let session: URLSession + private let decoder: JSONDecoder + private let encoder: JSONEncoder + private var closed = false + private var activeConnection: StratumConnection? + + public init(config: MiningConfig) { + 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: - Pool Operations + + /// Connect to a mining pool. + public func connect(pool: PoolConfig) async throws -> StratumConnection { + var body: [String: Any] = ["url": pool.url, "user": pool.user] + if let password = pool.password { body["password"] = password } + if let algorithm = pool.algorithm { body["algorithm"] = algorithm } + if let difficulty = pool.difficulty { body["difficulty"] = difficulty } + let conn: StratumConnection = try await post(path: "/pool/connect", body: body) + activeConnection = conn + return conn + } + + /// Disconnect from the pool. + public func disconnect() async throws { + guard activeConnection != nil else { return } + let _: [String: Any] = try await post(path: "/pool/disconnect", body: [:] as [String: String]) + activeConnection = nil + } + + /// Get current connection. + public func getConnection() async throws -> StratumConnection { + let conn: StratumConnection = try await get(path: "/pool/connection") + activeConnection = conn + return conn + } + + /// Get pool stats. + public func getPoolStats() async throws -> PoolStats { + return try await get(path: "/pool/stats") + } + + /// Check if connected. + public var isConnected: Bool { + activeConnection?.status == .connected + } + + // MARK: - Mining Operations + + /// Get block template. + public func getBlockTemplate() async throws -> BlockTemplate { + return try await get(path: "/mining/template") + } + + /// Submit mined work. + public func submitWork(_ work: MinedWork) async throws -> SubmitResult { + let body: [String: Any] = [ + "templateId": work.templateId, + "nonce": work.nonce, + "extraNonce": work.extraNonce, + "timestamp": work.timestamp, + "hash": work.hash + ] + return try await post(path: "/mining/submit", body: body) + } + + /// Start mining. + public func startMining(algorithm: String? = nil) async throws { + var body: [String: Any] = [:] + if let algorithm = algorithm { body["algorithm"] = algorithm } + let _: [String: Any] = try await post(path: "/mining/start", body: body) + } + + /// Stop mining. + public func stopMining() async throws { + let _: [String: Any] = try await post(path: "/mining/stop", body: [:] as [String: String]) + } + + /// Check if mining. + public func isMining() async throws -> Bool { + let response: [String: Bool] = try await get(path: "/mining/status") + return response["mining"] ?? false + } + + // MARK: - Stats Operations + + /// Get hashrate. + public func getHashrate() async throws -> Hashrate { + return try await get(path: "/stats/hashrate") + } + + /// Get mining stats. + public func getStats() async throws -> MiningStats { + return try await get(path: "/stats") + } + + /// Get earnings. + public func getEarnings(period: TimePeriod? = nil) async throws -> Earnings { + let path = period != nil ? "/stats/earnings?period=\(period!.rawValue)" : "/stats/earnings" + return try await get(path: path) + } + + /// Get share stats. + public func getShareStats() async throws -> ShareStats { + return try await get(path: "/stats/shares") + } + + // MARK: - Device Operations + + /// List devices. + public func listDevices() async throws -> [MiningDevice] { + let response: DevicesResponse = try await get(path: "/devices") + return response.devices ?? [] + } + + /// Get device. + public func getDevice(deviceId: String) async throws -> MiningDevice { + return try await get(path: "/devices/\(deviceId.urlEncoded)") + } + + /// Set device config. + public func setDeviceConfig(deviceId: String, config: DeviceConfig) async throws -> MiningDevice { + var body: [String: Any] = ["enabled": config.enabled] + if let intensity = config.intensity { body["intensity"] = intensity } + if let powerLimit = config.powerLimit { body["powerLimit"] = powerLimit } + if let coreClockOffset = config.coreClockOffset { body["coreClockOffset"] = coreClockOffset } + if let memoryClockOffset = config.memoryClockOffset { body["memoryClockOffset"] = memoryClockOffset } + if let fanSpeed = config.fanSpeed { body["fanSpeed"] = fanSpeed } + return try await put(path: "/devices/\(deviceId.urlEncoded)/config", body: body) + } + + /// Enable device. + public func enableDevice(deviceId: String) async throws { + _ = try await setDeviceConfig(deviceId: deviceId, config: DeviceConfig(enabled: true)) + } + + /// Disable device. + public func disableDevice(deviceId: String) async throws { + _ = try await setDeviceConfig(deviceId: deviceId, config: DeviceConfig(enabled: false)) + } + + // MARK: - Worker Operations + + /// List workers. + public func listWorkers() async throws -> [WorkerInfo] { + let response: WorkersResponse = try await get(path: "/workers") + return response.workers ?? [] + } + + /// Get worker. + public func getWorker(workerId: String) async throws -> WorkerInfo { + return try await get(path: "/workers/\(workerId.urlEncoded)") + } + + /// Remove worker. + public func removeWorker(workerId: String) async throws { + let _: [String: Any] = try await delete(path: "/workers/\(workerId.urlEncoded)") + } + + // MARK: - Algorithm Operations + + /// List algorithms. + public func listAlgorithms() async throws -> [MiningAlgorithm] { + let response: AlgorithmsResponse = try await get(path: "/algorithms") + return response.algorithms ?? [] + } + + /// Get algorithm. + public func getAlgorithm(name: String) async throws -> MiningAlgorithm { + return try await get(path: "/algorithms/\(name.urlEncoded)") + } + + /// Switch algorithm. + public func switchAlgorithm(_ algorithm: String) async throws { + let _: [String: Any] = try await post(path: "/algorithms/switch", body: ["algorithm": algorithm]) + } + + /// Get most profitable algorithm. + public func getMostProfitable() async throws -> MiningAlgorithm { + return try await get(path: "/algorithms/profitable") + } + + // MARK: - Lifecycle + + public func close() { + closed = true + activeConnection = nil + } + + 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 put(path: String, body: Any) async throws -> T { + return try await request(method: "PUT", path: path, body: body) + } + + private func delete(path: String) async throws -> T { + return try await request(method: "DELETE", path: path) + } + + private func request(method: String, path: String, body: Any? = nil) async throws -> T { + guard !closed else { throw MiningError(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 MiningError(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 MiningError(message: "Invalid response") } + + if httpResponse.statusCode >= 400 { + let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + throw MiningError( + 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 + } +} diff --git a/sdk/swift/Sources/Synor/Mining/Types.swift b/sdk/swift/Sources/Synor/Mining/Types.swift new file mode 100644 index 0000000..d21d3f9 --- /dev/null +++ b/sdk/swift/Sources/Synor/Mining/Types.swift @@ -0,0 +1,352 @@ +import Foundation + +// MARK: - Configuration + +/// Configuration for the Synor Mining client. +public struct MiningConfig { + public let apiKey: String + public let endpoint: String + public let timeout: TimeInterval + public let retries: Int + public let debug: Bool + + public init( + apiKey: String, + endpoint: String = "https://mining.synor.io/v1", + timeout: TimeInterval = 60, + retries: Int = 3, + debug: Bool = false + ) { + self.apiKey = apiKey + self.endpoint = endpoint + self.timeout = timeout + self.retries = retries + self.debug = debug + } +} + +// MARK: - Enums + +/// Device type. +public enum DeviceType: String, Codable, Sendable { + case cpu + case gpu_nvidia + case gpu_amd + case asic +} + +/// Device status. +public enum DeviceStatus: String, Codable, Sendable { + case idle + case mining + case error + case offline +} + +/// Connection status. +public enum ConnectionStatus: String, Codable, Sendable { + case disconnected + case connecting + case connected + case reconnecting +} + +/// Time period. +public enum TimePeriod: String, Codable, Sendable { + case hour + case day + case week + case month + case all +} + +/// Submit result status. +public enum SubmitResultStatus: String, Codable, Sendable { + case accepted + case rejected + case stale +} + +// MARK: - Pool Types + +/// Pool configuration. +public struct PoolConfig { + public var url: String + public var user: String + public var password: String? + public var algorithm: String? + public var difficulty: Double? + + public init( + url: String, + user: String, + password: String? = nil, + algorithm: String? = nil, + difficulty: Double? = nil + ) { + self.url = url + self.user = user + self.password = password + self.algorithm = algorithm + self.difficulty = difficulty + } +} + +/// Stratum connection. +public struct StratumConnection: Codable, Sendable { + public let id: String + public let pool: String + public let status: ConnectionStatus + public let algorithm: String + public let difficulty: Double + public let connectedAt: Int64 + public let acceptedShares: Int + public let rejectedShares: Int + public let staleShares: Int + public let lastShareAt: Int64? +} + +/// Pool stats. +public struct PoolStats: Codable, Sendable { + public let url: String + public let workers: Int + public let hashrate: Double + public let difficulty: Double + public let lastBlock: Int64 + public let blocksFound24h: Int + public let luck: Double +} + +// MARK: - Mining Types + +/// Template transaction. +public struct TemplateTransaction: Codable, Sendable { + public let txid: String + public let data: String + public let fee: String + public let weight: Int +} + +/// Block template. +public struct BlockTemplate: Codable, Sendable { + public let id: String + public let previousBlockHash: String + public let merkleRoot: String + public let timestamp: Int64 + public let bits: String + public let height: Int64 + public let coinbaseValue: String + public let transactions: [TemplateTransaction] + public let target: String + public let algorithm: String + public let extraNonce: String +} + +/// Mined work. +public struct MinedWork { + public var templateId: String + public var nonce: String + public var extraNonce: String + public var timestamp: Int64 + public var hash: String + + public init( + templateId: String, + nonce: String, + extraNonce: String, + timestamp: Int64, + hash: String + ) { + self.templateId = templateId + self.nonce = nonce + self.extraNonce = extraNonce + self.timestamp = timestamp + self.hash = hash + } +} + +/// Share info. +public struct ShareInfo: Codable, Sendable { + public let hash: String + public let difficulty: Double + public let timestamp: Int64 + public let accepted: Bool +} + +/// Submit result. +public struct SubmitResult: Codable, Sendable { + public let status: SubmitResultStatus + public let share: ShareInfo + public let blockFound: Bool + public let reason: String? + public let blockHash: String? + public let reward: String? +} + +// MARK: - Stats Types + +/// Hashrate. +public struct Hashrate: Codable, Sendable { + public let current: Double + public let average1h: Double + public let average24h: Double + public let peak: Double + public let unit: String +} + +/// Share stats. +public struct ShareStats: Codable, Sendable { + public let accepted: Int + public let rejected: Int + public let stale: Int + public let total: Int + public let acceptRate: Double +} + +/// Device temperature. +public struct DeviceTemperature: Codable, Sendable { + public let current: Double + public let max: Double + public let throttling: Bool +} + +/// Earnings snapshot. +public struct EarningsSnapshot: Codable, Sendable { + public let today: String + public let yesterday: String + public let thisWeek: String + public let thisMonth: String + public let total: String + public let currency: String +} + +/// Mining stats. +public struct MiningStats: Codable, Sendable { + public let hashrate: Hashrate + public let shares: ShareStats + public let uptime: Int64 + public let efficiency: Double + public let earnings: EarningsSnapshot + public let powerConsumption: Double? + public let temperature: DeviceTemperature? +} + +/// Earnings breakdown. +public struct EarningsBreakdown: Codable, Sendable { + public let date: Int64 + public let amount: String + public let blocks: Int + public let shares: Int + public let hashrate: Double +} + +/// Earnings. +public struct Earnings: Codable, Sendable { + public let period: TimePeriod + public let startDate: Int64 + public let endDate: Int64 + public let amount: String + public let blocks: Int + public let shares: Int + public let averageHashrate: Double + public let currency: String + public let breakdown: [EarningsBreakdown] +} + +// MARK: - Device Types + +/// Mining device. +public struct MiningDevice: Codable, Sendable { + public let id: String + public let name: String + public let type: DeviceType + public let status: DeviceStatus + public let hashrate: Double + public let temperature: Double + public let fanSpeed: Double + public let powerDraw: Double + public let memoryUsed: Int64 + public let memoryTotal: Int64 + public let driver: String? + public let firmware: String? +} + +/// Device configuration. +public struct DeviceConfig { + public var enabled: Bool + public var intensity: Int? + public var powerLimit: Int? + public var coreClockOffset: Int? + public var memoryClockOffset: Int? + public var fanSpeed: Int? + + public init( + enabled: Bool, + intensity: Int? = nil, + powerLimit: Int? = nil, + coreClockOffset: Int? = nil, + memoryClockOffset: Int? = nil, + fanSpeed: Int? = nil + ) { + self.enabled = enabled + self.intensity = intensity + self.powerLimit = powerLimit + self.coreClockOffset = coreClockOffset + self.memoryClockOffset = memoryClockOffset + self.fanSpeed = fanSpeed + } +} + +/// Worker info. +public struct WorkerInfo: Codable, Sendable { + public let id: String + public let name: String + public let status: ConnectionStatus + public let hashrate: Hashrate + public let shares: ShareStats + public let devices: [MiningDevice] + public let lastSeen: Int64 + public let uptime: Int64 +} + +/// Mining algorithm. +public struct MiningAlgorithm: Codable, Sendable { + public let name: String + public let displayName: String + public let hashUnit: String + public let profitability: String + public let difficulty: Double + public let blockReward: String + public let blockTime: Int +} + +// MARK: - Response Types + +struct DevicesResponse: Codable { + let devices: [MiningDevice]? +} + +struct WorkersResponse: Codable { + let workers: [WorkerInfo]? +} + +struct AlgorithmsResponse: Codable { + let algorithms: [MiningAlgorithm]? +} + +// MARK: - Error + +/// Error thrown by Mining operations. +public struct MiningError: Error, LocalizedError { + public let message: String + public let code: String? + public let statusCode: Int + + public init(message: String, code: String? = nil, statusCode: Int = 0) { + self.message = message + self.code = code + self.statusCode = statusCode + } + + public var errorDescription: String? { message } +}