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).
This commit is contained in:
parent
58e57db661
commit
6607223c9e
50 changed files with 13997 additions and 4 deletions
345
sdk/c/include/synor/economics.h
Normal file
345
sdk/c/include/synor/economics.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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 */
|
||||
364
sdk/c/include/synor/governance.h
Normal file
364
sdk/c/include/synor/governance.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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 */
|
||||
394
sdk/c/include/synor/mining.h
Normal file
394
sdk/c/include/synor/mining.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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 */
|
||||
424
sdk/c/src/economics/economics.c
Normal file
424
sdk/c/src/economics/economics.c
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
/**
|
||||
* @file economics.c
|
||||
* @brief Synor Economics SDK implementation
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#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);
|
||||
}
|
||||
524
sdk/c/src/governance/governance.c
Normal file
524
sdk/c/src/governance/governance.c
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
/**
|
||||
* @file governance.c
|
||||
* @brief Synor Governance SDK implementation
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#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);
|
||||
}
|
||||
563
sdk/c/src/mining/mining.c
Normal file
563
sdk/c/src/mining/mining.c
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
/**
|
||||
* @file mining.c
|
||||
* @brief Synor Mining SDK implementation
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#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;
|
||||
}
|
||||
282
sdk/cpp/include/synor/economics.hpp
Normal file
282
sdk/cpp/include/synor/economics.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#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<CostItem> breakdown;
|
||||
std::optional<std::string> 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<UsageItem> 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<InvoiceLine> lines;
|
||||
std::string subtotal;
|
||||
std::string discount;
|
||||
std::string tax;
|
||||
std::string total;
|
||||
std::string currency;
|
||||
std::optional<std::string> 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<ServiceType> 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<std::vector<ServicePricing>> get_pricing(std::optional<ServiceType> service = std::nullopt);
|
||||
std::future<PriceResult> get_price(ServiceType service, const UsageMetrics& usage);
|
||||
std::future<CostEstimate> estimate_cost(const std::vector<UsagePlanItem>& plan);
|
||||
|
||||
// Billing operations
|
||||
std::future<Usage> get_usage(std::optional<BillingPeriod> period = std::nullopt);
|
||||
std::future<std::vector<Invoice>> get_invoices();
|
||||
std::future<Invoice> get_invoice(const std::string& invoice_id);
|
||||
std::future<AccountBalance> get_balance();
|
||||
|
||||
// Staking operations
|
||||
std::future<StakeReceipt> stake(const std::string& amount, int32_t duration_days = 0);
|
||||
std::future<UnstakeReceipt> unstake(const std::string& stake_id);
|
||||
std::future<std::vector<Stake>> get_stakes();
|
||||
std::future<Stake> get_stake(const std::string& stake_id);
|
||||
std::future<StakingRewards> get_staking_rewards();
|
||||
std::future<StakeReceipt> claim_rewards();
|
||||
|
||||
// Discount operations
|
||||
std::future<DiscountResult> apply_discount(const std::string& code);
|
||||
std::future<void> remove_discount(const std::string& code);
|
||||
std::future<std::vector<Discount>> get_discounts();
|
||||
|
||||
// Lifecycle
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
std::future<bool> health_check();
|
||||
|
||||
private:
|
||||
std::unique_ptr<SynorEconomicsImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace economics
|
||||
} // namespace synor
|
||||
300
sdk/cpp/include/synor/governance.hpp
Normal file
300
sdk/cpp/include/synor/governance.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#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<std::string> value;
|
||||
};
|
||||
|
||||
/// Proposal draft
|
||||
struct ProposalDraft {
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::optional<std::string> discussion_url;
|
||||
std::optional<int64_t> voting_start_time;
|
||||
std::optional<int64_t> voting_end_time;
|
||||
std::optional<std::string> dao_id;
|
||||
std::vector<ProposalAction> 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<std::string> discussion_url;
|
||||
std::string proposer;
|
||||
ProposalStatus status;
|
||||
int64_t created_at;
|
||||
int64_t voting_start_time;
|
||||
int64_t voting_end_time;
|
||||
std::optional<int64_t> execution_time;
|
||||
VoteTally vote_tally;
|
||||
std::vector<ProposalAction> actions;
|
||||
std::optional<std::string> dao_id;
|
||||
};
|
||||
|
||||
/// Proposal filter
|
||||
struct ProposalFilter {
|
||||
std::optional<ProposalStatus> status;
|
||||
std::optional<std::string> proposer;
|
||||
std::optional<std::string> dao_id;
|
||||
std::optional<int32_t> limit;
|
||||
std::optional<int32_t> offset;
|
||||
};
|
||||
|
||||
/// Vote
|
||||
struct Vote {
|
||||
VoteChoice choice;
|
||||
std::optional<std::string> reason;
|
||||
};
|
||||
|
||||
/// Vote receipt
|
||||
struct VoteReceipt {
|
||||
std::string id;
|
||||
std::string proposal_id;
|
||||
std::string voter;
|
||||
VoteChoice choice;
|
||||
std::string weight;
|
||||
std::optional<std::string> 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<std::string> 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<std::string> token_address;
|
||||
std::optional<double> quorum_percent;
|
||||
std::optional<std::string> proposal_threshold;
|
||||
std::vector<std::string> multisig_members;
|
||||
std::optional<int32_t> multisig_threshold;
|
||||
};
|
||||
|
||||
/// DAO
|
||||
struct Dao {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
DaoType type;
|
||||
std::optional<std::string> 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<TreasuryToken> 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<Proposal> create_proposal(const ProposalDraft& draft);
|
||||
std::future<Proposal> get_proposal(const std::string& proposal_id);
|
||||
std::future<std::vector<Proposal>> list_proposals(const ProposalFilter& filter = {});
|
||||
std::future<Proposal> cancel_proposal(const std::string& proposal_id);
|
||||
std::future<Proposal> execute_proposal(const std::string& proposal_id);
|
||||
std::future<Proposal> 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<VoteReceipt> vote(const std::string& proposal_id, const Vote& vote,
|
||||
std::optional<std::string> weight = std::nullopt);
|
||||
std::future<std::vector<VoteReceipt>> get_votes(const std::string& proposal_id);
|
||||
std::future<VoteReceipt> get_my_vote(const std::string& proposal_id);
|
||||
std::future<DelegationReceipt> delegate(const std::string& delegatee,
|
||||
std::optional<std::string> amount = std::nullopt);
|
||||
std::future<DelegationReceipt> undelegate(const std::string& delegatee);
|
||||
std::future<VotingPower> get_voting_power(const std::string& address);
|
||||
std::future<std::vector<DelegationReceipt>> get_delegations(const std::string& address);
|
||||
|
||||
// DAO operations
|
||||
std::future<Dao> create_dao(const DaoConfig& config);
|
||||
std::future<Dao> get_dao(const std::string& dao_id);
|
||||
std::future<std::vector<Dao>> list_daos(std::optional<int32_t> limit = std::nullopt,
|
||||
std::optional<int32_t> offset = std::nullopt);
|
||||
std::future<DaoTreasury> get_dao_treasury(const std::string& dao_id);
|
||||
std::future<std::vector<std::string>> get_dao_members(const std::string& dao_id);
|
||||
|
||||
// Vesting operations
|
||||
std::future<VestingContract> create_vesting_schedule(const VestingSchedule& schedule);
|
||||
std::future<VestingContract> get_vesting_contract(const std::string& contract_id);
|
||||
std::future<std::vector<VestingContract>> list_vesting_contracts(
|
||||
std::optional<std::string> beneficiary = std::nullopt);
|
||||
std::future<ClaimReceipt> claim_vested(const std::string& contract_id);
|
||||
std::future<VestingContract> revoke_vesting(const std::string& contract_id);
|
||||
std::future<std::string> get_releasable_amount(const std::string& contract_id);
|
||||
|
||||
// Lifecycle
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
std::future<bool> health_check();
|
||||
|
||||
private:
|
||||
std::unique_ptr<SynorGovernanceImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace governance
|
||||
} // namespace synor
|
||||
334
sdk/cpp/include/synor/mining.hpp
Normal file
334
sdk/cpp/include/synor/mining.hpp
Normal file
|
|
@ -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 <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#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<std::string> password;
|
||||
std::optional<std::string> algorithm;
|
||||
std::optional<double> 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<int64_t> 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<TemplateTransaction> 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<std::string> reason;
|
||||
std::optional<std::string> block_hash;
|
||||
std::optional<std::string> 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<double> power_consumption;
|
||||
std::optional<DeviceTemperature> 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<EarningsBreakdown> 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<std::string> driver;
|
||||
std::optional<std::string> firmware;
|
||||
};
|
||||
|
||||
/// Device configuration
|
||||
struct DeviceConfig {
|
||||
bool enabled;
|
||||
std::optional<int32_t> intensity;
|
||||
std::optional<int32_t> power_limit;
|
||||
std::optional<int32_t> core_clock_offset;
|
||||
std::optional<int32_t> memory_clock_offset;
|
||||
std::optional<int32_t> fan_speed;
|
||||
};
|
||||
|
||||
/// Worker info
|
||||
struct WorkerInfo {
|
||||
std::string id;
|
||||
std::string name;
|
||||
ConnectionStatus status;
|
||||
Hashrate hashrate;
|
||||
ShareStats shares;
|
||||
std::vector<MiningDevice> 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<StratumConnection> connect(const PoolConfig& pool);
|
||||
std::future<void> disconnect();
|
||||
std::future<StratumConnection> get_connection();
|
||||
std::future<PoolStats> get_pool_stats();
|
||||
bool is_connected() const;
|
||||
|
||||
// Mining operations
|
||||
std::future<BlockTemplate> get_block_template();
|
||||
std::future<SubmitResult> submit_work(const MinedWork& work);
|
||||
std::future<void> start_mining(std::optional<std::string> algorithm = std::nullopt);
|
||||
std::future<void> stop_mining();
|
||||
std::future<bool> is_mining();
|
||||
|
||||
// Stats operations
|
||||
std::future<Hashrate> get_hashrate();
|
||||
std::future<MiningStats> get_stats();
|
||||
std::future<Earnings> get_earnings(std::optional<TimePeriod> period = std::nullopt);
|
||||
std::future<ShareStats> get_share_stats();
|
||||
|
||||
// Device operations
|
||||
std::future<std::vector<MiningDevice>> list_devices();
|
||||
std::future<MiningDevice> get_device(const std::string& device_id);
|
||||
std::future<MiningDevice> set_device_config(const std::string& device_id,
|
||||
const DeviceConfig& config);
|
||||
std::future<void> enable_device(const std::string& device_id);
|
||||
std::future<void> disable_device(const std::string& device_id);
|
||||
|
||||
// Worker operations
|
||||
std::future<std::vector<WorkerInfo>> list_workers();
|
||||
std::future<WorkerInfo> get_worker(const std::string& worker_id);
|
||||
std::future<void> remove_worker(const std::string& worker_id);
|
||||
|
||||
// Algorithm operations
|
||||
std::future<std::vector<MiningAlgorithm>> list_algorithms();
|
||||
std::future<MiningAlgorithm> get_algorithm(const std::string& name);
|
||||
std::future<void> switch_algorithm(const std::string& algorithm);
|
||||
std::future<MiningAlgorithm> get_most_profitable();
|
||||
|
||||
// Lifecycle
|
||||
void close();
|
||||
bool is_closed() const;
|
||||
std::future<bool> health_check();
|
||||
|
||||
private:
|
||||
std::unique_ptr<SynorMiningImpl> impl_;
|
||||
};
|
||||
|
||||
} // namespace mining
|
||||
} // namespace synor
|
||||
165
sdk/csharp/Synor.Sdk/Economics/SynorEconomics.cs
Normal file
165
sdk/csharp/Synor.Sdk/Economics/SynorEconomics.cs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Synor.Sdk.Economics;
|
||||
|
||||
/// <summary>
|
||||
/// Synor Economics SDK client for C#.
|
||||
/// Pricing, billing, staking, and discount management.
|
||||
/// </summary>
|
||||
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<List<ServicePricing>> GetPricingAsync(ServiceType? service = null, CancellationToken ct = default)
|
||||
{
|
||||
var path = service.HasValue ? $"/pricing?service={service.Value.ToString().ToLower()}" : "/pricing";
|
||||
var r = await GetAsync<PricingResponse>(path, ct);
|
||||
return r.Pricing ?? new List<ServicePricing>();
|
||||
}
|
||||
|
||||
public async Task<PriceResult> GetPriceAsync(ServiceType service, UsageMetrics usage, CancellationToken ct = default)
|
||||
{
|
||||
var body = new { service = service.ToString().ToLower(), usage };
|
||||
return await PostAsync<PriceResult>("/pricing/calculate", body, ct);
|
||||
}
|
||||
|
||||
public async Task<CostEstimate> EstimateCostAsync(List<UsagePlanItem> plan, CancellationToken ct = default)
|
||||
=> await PostAsync<CostEstimate>("/pricing/estimate", new { plan }, ct);
|
||||
|
||||
// ==================== Billing Operations ====================
|
||||
|
||||
public async Task<Usage> GetUsageAsync(BillingPeriod? period = null, CancellationToken ct = default)
|
||||
{
|
||||
var path = period.HasValue ? $"/billing/usage?period={period.Value.ToString().ToLower()}" : "/billing/usage";
|
||||
return await GetAsync<Usage>(path, ct);
|
||||
}
|
||||
|
||||
public async Task<List<Invoice>> GetInvoicesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<InvoicesResponse>("/billing/invoices", ct);
|
||||
return r.Invoices ?? new List<Invoice>();
|
||||
}
|
||||
|
||||
public async Task<Invoice> GetInvoiceAsync(string invoiceId, CancellationToken ct = default)
|
||||
=> await GetAsync<Invoice>($"/billing/invoices/{Uri.EscapeDataString(invoiceId)}", ct);
|
||||
|
||||
public async Task<AccountBalance> GetBalanceAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<AccountBalance>("/billing/balance", ct);
|
||||
|
||||
// ==================== Staking Operations ====================
|
||||
|
||||
public async Task<StakeReceipt> StakeAsync(string amount, int durationDays = 0, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["amount"] = amount };
|
||||
if (durationDays > 0) body["durationDays"] = durationDays;
|
||||
return await PostAsync<StakeReceipt>("/staking/stake", body, ct);
|
||||
}
|
||||
|
||||
public async Task<UnstakeReceipt> UnstakeAsync(string stakeId, CancellationToken ct = default)
|
||||
=> await PostAsync<UnstakeReceipt>($"/staking/{Uri.EscapeDataString(stakeId)}/unstake", new { }, ct);
|
||||
|
||||
public async Task<List<Stake>> GetStakesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<StakesResponse>("/staking", ct);
|
||||
return r.Stakes ?? new List<Stake>();
|
||||
}
|
||||
|
||||
public async Task<Stake> GetStakeAsync(string stakeId, CancellationToken ct = default)
|
||||
=> await GetAsync<Stake>($"/staking/{Uri.EscapeDataString(stakeId)}", ct);
|
||||
|
||||
public async Task<StakingRewards> GetStakingRewardsAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<StakingRewards>("/staking/rewards", ct);
|
||||
|
||||
public async Task<StakeReceipt> ClaimRewardsAsync(CancellationToken ct = default)
|
||||
=> await PostAsync<StakeReceipt>("/staking/rewards/claim", new { }, ct);
|
||||
|
||||
// ==================== Discount Operations ====================
|
||||
|
||||
public async Task<DiscountResult> ApplyDiscountAsync(string code, CancellationToken ct = default)
|
||||
=> await PostAsync<DiscountResult>("/discounts/apply", new { code }, ct);
|
||||
|
||||
public async Task RemoveDiscountAsync(string code, CancellationToken ct = default)
|
||||
=> await DeleteAsync($"/discounts/{Uri.EscapeDataString(code)}", ct);
|
||||
|
||||
public async Task<List<Discount>> GetDiscountsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<DiscountsResponse>("/discounts", ct);
|
||||
return r.Discounts ?? new List<Discount>();
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
try { var r = await GetAsync<HealthResponse>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.GetAsync(path, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PostAsync<T>(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<T>(_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<T> ExecuteAsync<T>(Func<Task<T>> 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<Dictionary<string, object>>(c, _jsonOptions);
|
||||
throw new EconomicsException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
||||
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
181
sdk/csharp/Synor.Sdk/Economics/Types.cs
Normal file
181
sdk/csharp/Synor.Sdk/Economics/Types.cs
Normal file
|
|
@ -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<CostItem> 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<UsageItem> 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<InvoiceLine> 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<ServiceType> 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<ServicePricing>? Pricing);
|
||||
internal record InvoicesResponse(List<Invoice>? Invoices);
|
||||
internal record StakesResponse(List<Stake>? Stakes);
|
||||
internal record DiscountsResponse(List<Discount>? 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;
|
||||
}
|
||||
}
|
||||
212
sdk/csharp/Synor.Sdk/Governance/SynorGovernance.cs
Normal file
212
sdk/csharp/Synor.Sdk/Governance/SynorGovernance.cs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Synor.Sdk.Governance;
|
||||
|
||||
/// <summary>
|
||||
/// Synor Governance SDK client for C#.
|
||||
/// Proposals, voting, DAOs, and vesting operations.
|
||||
/// </summary>
|
||||
public class SynorGovernance : IDisposable
|
||||
{
|
||||
private readonly GovernanceConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private bool _disposed;
|
||||
private static readonly HashSet<ProposalStatus> 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<Proposal> CreateProposalAsync(ProposalDraft draft, CancellationToken ct = default)
|
||||
=> await PostAsync<Proposal>("/proposals", draft, ct);
|
||||
|
||||
public async Task<Proposal> GetProposalAsync(string proposalId, CancellationToken ct = default)
|
||||
=> await GetAsync<Proposal>($"/proposals/{Uri.EscapeDataString(proposalId)}", ct);
|
||||
|
||||
public async Task<List<Proposal>> ListProposalsAsync(ProposalFilter? filter = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = new List<string>();
|
||||
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<ProposalsResponse>(path, ct);
|
||||
return r.Proposals ?? new List<Proposal>();
|
||||
}
|
||||
|
||||
public async Task<Proposal> CancelProposalAsync(string proposalId, CancellationToken ct = default)
|
||||
=> await PostAsync<Proposal>($"/proposals/{Uri.EscapeDataString(proposalId)}/cancel", new { }, ct);
|
||||
|
||||
public async Task<Proposal> ExecuteProposalAsync(string proposalId, CancellationToken ct = default)
|
||||
=> await PostAsync<Proposal>($"/proposals/{Uri.EscapeDataString(proposalId)}/execute", new { }, ct);
|
||||
|
||||
public async Task<Proposal> 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<VoteReceipt> VoteAsync(string proposalId, Vote vote, string? weight = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["choice"] = vote.Choice.ToString().ToLower() };
|
||||
if (vote.Reason != null) body["reason"] = vote.Reason;
|
||||
if (weight != null) body["weight"] = weight;
|
||||
return await PostAsync<VoteReceipt>($"/proposals/{Uri.EscapeDataString(proposalId)}/vote", body, ct);
|
||||
}
|
||||
|
||||
public async Task<List<VoteReceipt>> GetVotesAsync(string proposalId, CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<VotesResponse>($"/proposals/{Uri.EscapeDataString(proposalId)}/votes", ct);
|
||||
return r.Votes ?? new List<VoteReceipt>();
|
||||
}
|
||||
|
||||
public async Task<VoteReceipt> GetMyVoteAsync(string proposalId, CancellationToken ct = default)
|
||||
=> await GetAsync<VoteReceipt>($"/proposals/{Uri.EscapeDataString(proposalId)}/votes/me", ct);
|
||||
|
||||
public async Task<DelegationReceipt> DelegateAsync(string delegatee, string? amount = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["delegatee"] = delegatee };
|
||||
if (amount != null) body["amount"] = amount;
|
||||
return await PostAsync<DelegationReceipt>("/voting/delegate", body, ct);
|
||||
}
|
||||
|
||||
public async Task<DelegationReceipt> UndelegateAsync(string delegatee, CancellationToken ct = default)
|
||||
=> await PostAsync<DelegationReceipt>("/voting/undelegate", new { delegatee }, ct);
|
||||
|
||||
public async Task<VotingPower> GetVotingPowerAsync(string address, CancellationToken ct = default)
|
||||
=> await GetAsync<VotingPower>($"/voting/power/{Uri.EscapeDataString(address)}", ct);
|
||||
|
||||
public async Task<List<DelegationReceipt>> GetDelegationsAsync(string address, CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<DelegationsResponse>($"/voting/delegations/{Uri.EscapeDataString(address)}", ct);
|
||||
return r.Delegations ?? new List<DelegationReceipt>();
|
||||
}
|
||||
|
||||
// ==================== DAO Operations ====================
|
||||
|
||||
public async Task<Dao> CreateDaoAsync(DaoConfig config, CancellationToken ct = default)
|
||||
=> await PostAsync<Dao>("/daos", config, ct);
|
||||
|
||||
public async Task<Dao> GetDaoAsync(string daoId, CancellationToken ct = default)
|
||||
=> await GetAsync<Dao>($"/daos/{Uri.EscapeDataString(daoId)}", ct);
|
||||
|
||||
public async Task<List<Dao>> ListDaosAsync(int? limit = null, int? offset = null, CancellationToken ct = default)
|
||||
{
|
||||
var q = new List<string>();
|
||||
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<DaosResponse>(path, ct);
|
||||
return r.Daos ?? new List<Dao>();
|
||||
}
|
||||
|
||||
public async Task<DaoTreasury> GetDaoTreasuryAsync(string daoId, CancellationToken ct = default)
|
||||
=> await GetAsync<DaoTreasury>($"/daos/{Uri.EscapeDataString(daoId)}/treasury", ct);
|
||||
|
||||
public async Task<List<string>> GetDaoMembersAsync(string daoId, CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<MembersResponse>($"/daos/{Uri.EscapeDataString(daoId)}/members", ct);
|
||||
return r.Members ?? new List<string>();
|
||||
}
|
||||
|
||||
// ==================== Vesting Operations ====================
|
||||
|
||||
public async Task<VestingContract> CreateVestingScheduleAsync(VestingSchedule schedule, CancellationToken ct = default)
|
||||
=> await PostAsync<VestingContract>("/vesting", schedule, ct);
|
||||
|
||||
public async Task<VestingContract> GetVestingContractAsync(string contractId, CancellationToken ct = default)
|
||||
=> await GetAsync<VestingContract>($"/vesting/{Uri.EscapeDataString(contractId)}", ct);
|
||||
|
||||
public async Task<List<VestingContract>> ListVestingContractsAsync(string? beneficiary = null, CancellationToken ct = default)
|
||||
{
|
||||
var path = beneficiary != null ? $"/vesting?beneficiary={Uri.EscapeDataString(beneficiary)}" : "/vesting";
|
||||
var r = await GetAsync<VestingContractsResponse>(path, ct);
|
||||
return r.Contracts ?? new List<VestingContract>();
|
||||
}
|
||||
|
||||
public async Task<ClaimReceipt> ClaimVestedAsync(string contractId, CancellationToken ct = default)
|
||||
=> await PostAsync<ClaimReceipt>($"/vesting/{Uri.EscapeDataString(contractId)}/claim", new { }, ct);
|
||||
|
||||
public async Task<VestingContract> RevokeVestingAsync(string contractId, CancellationToken ct = default)
|
||||
=> await PostAsync<VestingContract>($"/vesting/{Uri.EscapeDataString(contractId)}/revoke", new { }, ct);
|
||||
|
||||
public async Task<string> GetReleasableAmountAsync(string contractId, CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<ReleasableResponse>($"/vesting/{Uri.EscapeDataString(contractId)}/releasable", ct);
|
||||
return r.Amount;
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
try { var r = await GetAsync<GovernanceHealthResponse>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.GetAsync(path, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PostAsync<T>(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<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<Task<T>> 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<Dictionary<string, object>>(c, _jsonOptions);
|
||||
throw new GovernanceException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
||||
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
195
sdk/csharp/Synor.Sdk/Governance/Types.cs
Normal file
195
sdk/csharp/Synor.Sdk/Governance/Types.cs
Normal file
|
|
@ -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<ProposalAction>? 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<ProposalAction> 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<string> 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<string>? 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<TreasuryToken> 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<Proposal>? Proposals);
|
||||
internal record VotesResponse(List<VoteReceipt>? Votes);
|
||||
internal record DaosResponse(List<Dao>? Daos);
|
||||
internal record MembersResponse(List<string>? Members);
|
||||
internal record DelegationsResponse(List<DelegationReceipt>? Delegations);
|
||||
internal record VestingContractsResponse(List<VestingContract>? 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;
|
||||
}
|
||||
}
|
||||
215
sdk/csharp/Synor.Sdk/Mining/SynorMining.cs
Normal file
215
sdk/csharp/Synor.Sdk/Mining/SynorMining.cs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Synor.Sdk.Mining;
|
||||
|
||||
/// <summary>
|
||||
/// Synor Mining SDK client for C#.
|
||||
/// Pool connections, block templates, hashrate stats, and GPU management.
|
||||
/// </summary>
|
||||
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<StratumConnection> ConnectAsync(PoolConfig pool, CancellationToken ct = default)
|
||||
{
|
||||
var body = new Dictionary<string, object> { ["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<StratumConnection>("/pool/connect", body, ct);
|
||||
return _activeConnection;
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_activeConnection == null) return;
|
||||
await PostAsync<object>("/pool/disconnect", new { }, ct);
|
||||
_activeConnection = null;
|
||||
}
|
||||
|
||||
public async Task<StratumConnection> GetConnectionAsync(CancellationToken ct = default)
|
||||
{
|
||||
_activeConnection = await GetAsync<StratumConnection>("/pool/connection", ct);
|
||||
return _activeConnection;
|
||||
}
|
||||
|
||||
public async Task<PoolStats> GetPoolStatsAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<PoolStats>("/pool/stats", ct);
|
||||
|
||||
public bool IsConnected => _activeConnection?.Status == ConnectionStatus.Connected;
|
||||
|
||||
// ==================== Mining Operations ====================
|
||||
|
||||
public async Task<BlockTemplate> GetBlockTemplateAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<BlockTemplate>("/mining/template", ct);
|
||||
|
||||
public async Task<SubmitResult> SubmitWorkAsync(MinedWork work, CancellationToken ct = default)
|
||||
=> await PostAsync<SubmitResult>("/mining/submit", work, ct);
|
||||
|
||||
public async Task StartMiningAsync(string? algorithm = null, CancellationToken ct = default)
|
||||
{
|
||||
var body = algorithm != null ? new { algorithm } : (object)new { };
|
||||
await PostAsync<object>("/mining/start", body, ct);
|
||||
}
|
||||
|
||||
public async Task StopMiningAsync(CancellationToken ct = default)
|
||||
=> await PostAsync<object>("/mining/stop", new { }, ct);
|
||||
|
||||
public async Task<bool> IsMiningAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<MiningStatusResponse>("/mining/status", ct);
|
||||
return r.Mining;
|
||||
}
|
||||
|
||||
// ==================== Stats Operations ====================
|
||||
|
||||
public async Task<Hashrate> GetHashrateAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<Hashrate>("/stats/hashrate", ct);
|
||||
|
||||
public async Task<MiningStats> GetStatsAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<MiningStats>("/stats", ct);
|
||||
|
||||
public async Task<Earnings> GetEarningsAsync(TimePeriod? period = null, CancellationToken ct = default)
|
||||
{
|
||||
var path = period.HasValue ? $"/stats/earnings?period={period.Value.ToString().ToLower()}" : "/stats/earnings";
|
||||
return await GetAsync<Earnings>(path, ct);
|
||||
}
|
||||
|
||||
public async Task<ShareStats> GetShareStatsAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<ShareStats>("/stats/shares", ct);
|
||||
|
||||
// ==================== Device Operations ====================
|
||||
|
||||
public async Task<List<MiningDevice>> ListDevicesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<DevicesResponse>("/devices", ct);
|
||||
return r.Devices ?? new List<MiningDevice>();
|
||||
}
|
||||
|
||||
public async Task<MiningDevice> GetDeviceAsync(string deviceId, CancellationToken ct = default)
|
||||
=> await GetAsync<MiningDevice>($"/devices/{Uri.EscapeDataString(deviceId)}", ct);
|
||||
|
||||
public async Task<MiningDevice> SetDeviceConfigAsync(string deviceId, DeviceConfig config, CancellationToken ct = default)
|
||||
=> await PutAsync<MiningDevice>($"/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<List<WorkerInfo>> ListWorkersAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<WorkersResponse>("/workers", ct);
|
||||
return r.Workers ?? new List<WorkerInfo>();
|
||||
}
|
||||
|
||||
public async Task<WorkerInfo> GetWorkerAsync(string workerId, CancellationToken ct = default)
|
||||
=> await GetAsync<WorkerInfo>($"/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<List<MiningAlgorithm>> ListAlgorithmsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var r = await GetAsync<AlgorithmsResponse>("/algorithms", ct);
|
||||
return r.Algorithms ?? new List<MiningAlgorithm>();
|
||||
}
|
||||
|
||||
public async Task<MiningAlgorithm> GetAlgorithmAsync(string name, CancellationToken ct = default)
|
||||
=> await GetAsync<MiningAlgorithm>($"/algorithms/{Uri.EscapeDataString(name)}", ct);
|
||||
|
||||
public async Task SwitchAlgorithmAsync(string algorithm, CancellationToken ct = default)
|
||||
=> await PostAsync<object>("/algorithms/switch", new { algorithm }, ct);
|
||||
|
||||
public async Task<MiningAlgorithm> GetMostProfitableAsync(CancellationToken ct = default)
|
||||
=> await GetAsync<MiningAlgorithm>("/algorithms/profitable", ct);
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
||||
{
|
||||
try { var r = await GetAsync<MiningHealthResponse>("/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<T> GetAsync<T>(string path, CancellationToken ct)
|
||||
=> await ExecuteAsync(async () => {
|
||||
var r = await _httpClient.GetAsync(path, ct);
|
||||
await EnsureSuccessAsync(r);
|
||||
return await r.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PostAsync<T>(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<T>(_jsonOptions, ct)!;
|
||||
});
|
||||
|
||||
private async Task<T> PutAsync<T>(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<T>(_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<T> ExecuteAsync<T>(Func<Task<T>> 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<Dictionary<string, object>>(c, _jsonOptions);
|
||||
throw new MiningException(e?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)r.StatusCode}",
|
||||
e?.GetValueOrDefault("code")?.ToString(), (int)r.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
219
sdk/csharp/Synor.Sdk/Mining/Types.cs
Normal file
219
sdk/csharp/Synor.Sdk/Mining/Types.cs
Normal file
|
|
@ -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<TemplateTransaction> 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<EarningsBreakdown> 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<MiningDevice> 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<MiningDevice>? Devices);
|
||||
internal record WorkersResponse(List<WorkerInfo>? Workers);
|
||||
internal record AlgorithmsResponse(List<MiningAlgorithm>? 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ class SynorDatabase {
|
|||
String path, [
|
||||
Map<String, dynamic>? 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<Map<String, dynamic>> _doRequest(
|
||||
|
|
|
|||
217
sdk/flutter/lib/src/economics/client.dart
Normal file
217
sdk/flutter/lib/src/economics/client.dart
Normal file
|
|
@ -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<Price> getPrice(ServiceType service, UsageMetrics usage) async {
|
||||
final resp = await _request('POST', '/pricing/calculate', {
|
||||
'service': service.name,
|
||||
'usage': usage.toJson(),
|
||||
});
|
||||
return Price.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<CostEstimate> estimateCost(UsagePlan plan) async {
|
||||
final resp = await _request('POST', '/pricing/estimate', plan.toJson());
|
||||
return CostEstimate.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<PricingTier>> 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<Usage> 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<Usage> getUsageByDateRange(int startDate, int endDate) async {
|
||||
final resp = await _request('GET', '/billing/usage?startDate=$startDate&endDate=$endDate');
|
||||
return Usage.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<Invoice>> getInvoices() async {
|
||||
final resp = await _request('GET', '/billing/invoices');
|
||||
return (resp['invoices'] as List).map((e) => Invoice.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<Invoice> getInvoice(String invoiceId) async {
|
||||
final resp = await _request('GET', '/billing/invoices/${Uri.encodeComponent(invoiceId)}');
|
||||
return Invoice.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Uint8List> downloadInvoice(String invoiceId) async {
|
||||
final resp = await _request('GET', '/billing/invoices/${Uri.encodeComponent(invoiceId)}/pdf');
|
||||
return base64Decode(resp['data'] as String);
|
||||
}
|
||||
|
||||
Future<AccountBalance> getBalance() async {
|
||||
final resp = await _request('GET', '/billing/balance');
|
||||
return AccountBalance.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<void> addCredits(String amount, String paymentMethod) async {
|
||||
await _request('POST', '/billing/credits', {
|
||||
'amount': amount,
|
||||
'paymentMethod': paymentMethod,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Staking Operations ====================
|
||||
|
||||
Future<StakeReceipt> stake(String amount, {int? durationDays}) async {
|
||||
final body = <String, dynamic>{'amount': amount};
|
||||
if (durationDays != null) body['durationDays'] = durationDays;
|
||||
final resp = await _request('POST', '/staking/stake', body);
|
||||
return StakeReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<UnstakeReceipt> unstake(String stakeId) async {
|
||||
final resp = await _request('POST', '/staking/unstake/${Uri.encodeComponent(stakeId)}');
|
||||
return UnstakeReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<StakeInfo>> getStakes() async {
|
||||
final resp = await _request('GET', '/staking/stakes');
|
||||
return (resp['stakes'] as List).map((e) => StakeInfo.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<StakeInfo> getStake(String stakeId) async {
|
||||
final resp = await _request('GET', '/staking/stakes/${Uri.encodeComponent(stakeId)}');
|
||||
return StakeInfo.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Rewards> getStakingRewards() async {
|
||||
final resp = await _request('GET', '/staking/rewards');
|
||||
return Rewards.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<String> claimRewards() async {
|
||||
final resp = await _request('POST', '/staking/rewards/claim');
|
||||
return resp['txHash'] as String;
|
||||
}
|
||||
|
||||
Future<double> getCurrentApy() async {
|
||||
final resp = await _request('GET', '/staking/apy');
|
||||
return (resp['apy'] as num).toDouble();
|
||||
}
|
||||
|
||||
// ==================== Discount Operations ====================
|
||||
|
||||
Future<AppliedDiscount> applyDiscount(String code) async {
|
||||
final resp = await _request('POST', '/discounts/apply', {'code': code});
|
||||
return AppliedDiscount.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<Discount>> getAvailableDiscounts() async {
|
||||
final resp = await _request('GET', '/discounts/available');
|
||||
return (resp['discounts'] as List).map((e) => Discount.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<Discount> validateDiscount(String code) async {
|
||||
final resp = await _request('GET', '/discounts/validate/${Uri.encodeComponent(code)}');
|
||||
return Discount.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<AppliedDiscount>> getAppliedDiscounts() async {
|
||||
final resp = await _request('GET', '/discounts/applied');
|
||||
return (resp['discounts'] as List).map((e) => AppliedDiscount.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<void> removeDiscount(String discountId) async {
|
||||
await _request('DELETE', '/discounts/${Uri.encodeComponent(discountId)}');
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
void close() {
|
||||
_closed = true;
|
||||
_client.close();
|
||||
}
|
||||
|
||||
bool get isClosed => _closed;
|
||||
|
||||
Future<bool> healthCheck() async {
|
||||
try {
|
||||
final resp = await _request('GET', '/health');
|
||||
return resp['status'] == 'healthy';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
Future<Map<String, dynamic>> _request(String method, String path, [Map<String, dynamic>? body]) async {
|
||||
if (_closed) throw const 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<Map<String, dynamic>> _doRequest(String method, String path, Map<String, dynamic>? body) async {
|
||||
final uri = Uri.parse('${config.endpoint}$path');
|
||||
final headers = {
|
||||
'Authorization': 'Bearer ${config.apiKey}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-SDK-Version': 'flutter/0.1.0',
|
||||
};
|
||||
|
||||
http.Response response;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await _client.get(uri, headers: headers).timeout(config.timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await _client.post(uri, headers: headers, body: body != null ? jsonEncode(body) : null).timeout(config.timeout);
|
||||
break;
|
||||
case '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<String, dynamic>;
|
||||
if (response.statusCode >= 400) {
|
||||
throw EconomicsException(
|
||||
respBody['message'] as String? ?? 'HTTP ${response.statusCode}',
|
||||
code: respBody['code'] as String?,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
return respBody;
|
||||
}
|
||||
}
|
||||
532
sdk/flutter/lib/src/economics/types.dart
Normal file
532
sdk/flutter/lib/src/economics/types.dart
Normal file
|
|
@ -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<String, dynamic> 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<String, dynamic> 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<PricingTier>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<CostBreakdown>? 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<String, dynamic> 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<String, dynamic> 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<UsageRecord> 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<String, dynamic> 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<String, dynamic> 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<InvoiceLineItem> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<RewardRecord> history;
|
||||
|
||||
const Rewards({
|
||||
required this.totalEarned,
|
||||
required this.pendingClaim,
|
||||
this.lastClaimDate,
|
||||
required this.currentApy,
|
||||
required this.history,
|
||||
});
|
||||
|
||||
factory Rewards.fromJson(Map<String, dynamic> 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<ServiceType>? 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<String, dynamic> 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<String, dynamic> 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';
|
||||
}
|
||||
250
sdk/flutter/lib/src/governance/client.dart
Normal file
250
sdk/flutter/lib/src/governance/client.dart
Normal file
|
|
@ -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<Proposal> createProposal(ProposalDraft proposal) async {
|
||||
final resp = await _request('POST', '/proposals', proposal.toJson());
|
||||
return Proposal.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Proposal> getProposal(String proposalId) async {
|
||||
final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}');
|
||||
return Proposal.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<Proposal>> listProposals({ProposalFilter? filter}) async {
|
||||
final params = <String>[];
|
||||
if (filter?.status != null) params.add('status=${filter!.status!.name}');
|
||||
if (filter?.proposer != null) params.add('proposer=${Uri.encodeComponent(filter!.proposer!)}');
|
||||
if (filter?.daoId != null) params.add('daoId=${Uri.encodeComponent(filter!.daoId!)}');
|
||||
if (filter?.limit != null) params.add('limit=${filter!.limit}');
|
||||
if (filter?.offset != null) params.add('offset=${filter!.offset}');
|
||||
|
||||
final path = params.isEmpty ? '/proposals' : '/proposals?${params.join('&')}';
|
||||
final resp = await _request('GET', path);
|
||||
return (resp['proposals'] as List).map((e) => Proposal.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<Proposal> cancelProposal(String proposalId) async {
|
||||
final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/cancel');
|
||||
return Proposal.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Proposal> executeProposal(String proposalId) async {
|
||||
final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/execute');
|
||||
return Proposal.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Proposal> waitForProposal(
|
||||
String proposalId, {
|
||||
Duration pollInterval = const Duration(minutes: 1),
|
||||
Duration maxWait = const Duration(days: 7),
|
||||
}) async {
|
||||
final deadline = DateTime.now().add(maxWait);
|
||||
while (DateTime.now().isBefore(deadline)) {
|
||||
final proposal = await getProposal(proposalId);
|
||||
if (_finalStatuses.contains(proposal.status)) return proposal;
|
||||
await Future.delayed(pollInterval);
|
||||
}
|
||||
throw const GovernanceException('Timeout waiting for proposal completion');
|
||||
}
|
||||
|
||||
// ==================== Voting Operations ====================
|
||||
|
||||
Future<VoteReceipt> vote(String proposalId, Vote vote, {String? weight}) async {
|
||||
final body = <String, dynamic>{'choice': vote.choice.name};
|
||||
if (vote.reason != null) body['reason'] = vote.reason;
|
||||
if (weight != null) body['weight'] = weight;
|
||||
final resp = await _request('POST', '/proposals/${Uri.encodeComponent(proposalId)}/vote', body);
|
||||
return VoteReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<VoteReceipt>> getVotes(String proposalId) async {
|
||||
final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}/votes');
|
||||
return (resp['votes'] as List).map((e) => VoteReceipt.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<VoteReceipt> getMyVote(String proposalId) async {
|
||||
final resp = await _request('GET', '/proposals/${Uri.encodeComponent(proposalId)}/votes/me');
|
||||
return VoteReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<DelegationReceipt> delegate(String delegatee, {String? amount}) async {
|
||||
final body = <String, dynamic>{'delegatee': delegatee};
|
||||
if (amount != null) body['amount'] = amount;
|
||||
final resp = await _request('POST', '/voting/delegate', body);
|
||||
return DelegationReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<DelegationReceipt> undelegate(String delegatee) async {
|
||||
final resp = await _request('POST', '/voting/undelegate', {'delegatee': delegatee});
|
||||
return DelegationReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<VotingPower> getVotingPower(String address) async {
|
||||
final resp = await _request('GET', '/voting/power/${Uri.encodeComponent(address)}');
|
||||
return VotingPower.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<DelegationReceipt>> getDelegations(String address) async {
|
||||
final resp = await _request('GET', '/voting/delegations/${Uri.encodeComponent(address)}');
|
||||
return (resp['delegations'] as List).map((e) => DelegationReceipt.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
// ==================== DAO Operations ====================
|
||||
|
||||
Future<Dao> createDao(DaoConfig daoConfig) async {
|
||||
final resp = await _request('POST', '/daos', daoConfig.toJson());
|
||||
return Dao.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Dao> getDao(String daoId) async {
|
||||
final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}');
|
||||
return Dao.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<Dao>> listDaos({int? limit, int? offset}) async {
|
||||
final params = <String>[];
|
||||
if (limit != null) params.add('limit=$limit');
|
||||
if (offset != null) params.add('offset=$offset');
|
||||
final path = params.isEmpty ? '/daos' : '/daos?${params.join('&')}';
|
||||
final resp = await _request('GET', path);
|
||||
return (resp['daos'] as List).map((e) => Dao.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<DaoTreasury> getDaoTreasury(String daoId) async {
|
||||
final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}/treasury');
|
||||
return DaoTreasury.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<String>> getDaoMembers(String daoId) async {
|
||||
final resp = await _request('GET', '/daos/${Uri.encodeComponent(daoId)}/members');
|
||||
return (resp['members'] as List).cast<String>();
|
||||
}
|
||||
|
||||
// ==================== Vesting Operations ====================
|
||||
|
||||
Future<VestingContract> createVestingSchedule(VestingSchedule schedule) async {
|
||||
final resp = await _request('POST', '/vesting', schedule.toJson());
|
||||
return VestingContract.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<VestingContract> getVestingContract(String contractId) async {
|
||||
final resp = await _request('GET', '/vesting/${Uri.encodeComponent(contractId)}');
|
||||
return VestingContract.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<List<VestingContract>> listVestingContracts({String? beneficiary}) async {
|
||||
final path = beneficiary != null ? '/vesting?beneficiary=${Uri.encodeComponent(beneficiary)}' : '/vesting';
|
||||
final resp = await _request('GET', path);
|
||||
return (resp['contracts'] as List).map((e) => VestingContract.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<ClaimReceipt> claimVested(String contractId) async {
|
||||
final resp = await _request('POST', '/vesting/${Uri.encodeComponent(contractId)}/claim');
|
||||
return ClaimReceipt.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<VestingContract> revokeVesting(String contractId) async {
|
||||
final resp = await _request('POST', '/vesting/${Uri.encodeComponent(contractId)}/revoke');
|
||||
return VestingContract.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<String> getReleasableAmount(String contractId) async {
|
||||
final resp = await _request('GET', '/vesting/${Uri.encodeComponent(contractId)}/releasable');
|
||||
return resp['amount'] as String;
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
void close() {
|
||||
_closed = true;
|
||||
_client.close();
|
||||
}
|
||||
|
||||
bool get isClosed => _closed;
|
||||
|
||||
Future<bool> healthCheck() async {
|
||||
try {
|
||||
final resp = await _request('GET', '/health');
|
||||
return resp['status'] == 'healthy';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
Future<Map<String, dynamic>> _request(String method, String path, [Map<String, dynamic>? body]) async {
|
||||
if (_closed) throw const GovernanceException('Client has been closed');
|
||||
|
||||
Exception? lastError;
|
||||
for (var attempt = 0; attempt < config.retries; attempt++) {
|
||||
try {
|
||||
return await _doRequest(method, path, body);
|
||||
} catch (e) {
|
||||
lastError = e as Exception;
|
||||
if (attempt < config.retries - 1) {
|
||||
await Future.delayed(Duration(seconds: 1 << attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? const GovernanceException('Unknown error');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _doRequest(String method, String path, Map<String, dynamic>? body) async {
|
||||
final uri = Uri.parse('${config.endpoint}$path');
|
||||
final headers = {
|
||||
'Authorization': 'Bearer ${config.apiKey}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-SDK-Version': 'flutter/0.1.0',
|
||||
};
|
||||
|
||||
http.Response response;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await _client.get(uri, headers: headers).timeout(config.timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await _client.post(uri, headers: headers, body: body != null ? jsonEncode(body) : null).timeout(config.timeout);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await _client.delete(uri, headers: headers).timeout(config.timeout);
|
||||
break;
|
||||
default:
|
||||
throw GovernanceException('Unknown method: $method');
|
||||
}
|
||||
|
||||
final respBody = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
if (response.statusCode >= 400) {
|
||||
throw GovernanceException(
|
||||
respBody['message'] as String? ?? 'HTTP ${response.statusCode}',
|
||||
code: respBody['code'] as String?,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
return respBody;
|
||||
}
|
||||
}
|
||||
497
sdk/flutter/lib/src/governance/types.dart
Normal file
497
sdk/flutter/lib/src/governance/types.dart
Normal file
|
|
@ -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<String, dynamic> toJson() => {'target': target, 'method': method, 'data': data, if (value != null) 'value': value};
|
||||
|
||||
factory ProposalAction.fromJson(Map<String, dynamic> 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<ProposalAction>? actions;
|
||||
final String? daoId;
|
||||
|
||||
const ProposalDraft({
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.discussionUrl,
|
||||
this.votingStartTime,
|
||||
this.votingEndTime,
|
||||
this.actions,
|
||||
this.daoId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> 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<ProposalAction>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String>? 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<String, dynamic> 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<String, dynamic> 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<TreasuryAsset> assets;
|
||||
|
||||
const DaoTreasury({required this.address, required this.balance, required this.currency, required this.assets});
|
||||
|
||||
factory DaoTreasury.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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';
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ class SynorHosting {
|
|||
String path, [
|
||||
Map<String, dynamic>? 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<Map<String, dynamic>> _doRequest(
|
||||
|
|
|
|||
231
sdk/flutter/lib/src/mining/client.dart
Normal file
231
sdk/flutter/lib/src/mining/client.dart
Normal file
|
|
@ -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<StratumConnection> connect(PoolConfig pool) async {
|
||||
final resp = await _request('POST', '/pool/connect', pool.toJson());
|
||||
_activeConnection = StratumConnection.fromJson(resp);
|
||||
return _activeConnection!;
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
if (_activeConnection == null) return;
|
||||
await _request('POST', '/pool/disconnect');
|
||||
_activeConnection = null;
|
||||
}
|
||||
|
||||
Future<StratumConnection> getConnection() async {
|
||||
final resp = await _request('GET', '/pool/connection');
|
||||
_activeConnection = StratumConnection.fromJson(resp);
|
||||
return _activeConnection!;
|
||||
}
|
||||
|
||||
Future<PoolStats> getPoolStats() async {
|
||||
final resp = await _request('GET', '/pool/stats');
|
||||
return PoolStats.fromJson(resp);
|
||||
}
|
||||
|
||||
bool get isConnected => _activeConnection?.status == ConnectionStatus.connected;
|
||||
|
||||
// ==================== Mining Operations ====================
|
||||
|
||||
Future<BlockTemplate> getBlockTemplate() async {
|
||||
final resp = await _request('GET', '/mining/template');
|
||||
return BlockTemplate.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<SubmitResult> submitWork(MinedWork work) async {
|
||||
final resp = await _request('POST', '/mining/submit', work.toJson());
|
||||
return SubmitResult.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<void> startMining({String? algorithm}) async {
|
||||
final body = algorithm != null ? {'algorithm': algorithm} : null;
|
||||
await _request('POST', '/mining/start', body);
|
||||
}
|
||||
|
||||
Future<void> stopMining() async {
|
||||
await _request('POST', '/mining/stop');
|
||||
}
|
||||
|
||||
Future<bool> isMining() async {
|
||||
final resp = await _request('GET', '/mining/status');
|
||||
return resp['mining'] as bool;
|
||||
}
|
||||
|
||||
// ==================== Stats Operations ====================
|
||||
|
||||
Future<Hashrate> getHashrate() async {
|
||||
final resp = await _request('GET', '/stats/hashrate');
|
||||
return Hashrate.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<MiningStats> getStats() async {
|
||||
final resp = await _request('GET', '/stats');
|
||||
return MiningStats.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<Earnings> 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<ShareStats> getShareStats() async {
|
||||
final resp = await _request('GET', '/stats/shares');
|
||||
return ShareStats.fromJson(resp);
|
||||
}
|
||||
|
||||
// ==================== Device Operations ====================
|
||||
|
||||
Future<List<MiningDevice>> listDevices() async {
|
||||
final resp = await _request('GET', '/devices');
|
||||
return (resp['devices'] as List).map((e) => MiningDevice.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<MiningDevice> getDevice(String deviceId) async {
|
||||
final resp = await _request('GET', '/devices/${Uri.encodeComponent(deviceId)}');
|
||||
return MiningDevice.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<MiningDevice> setDeviceConfig(String deviceId, DeviceConfig deviceConfig) async {
|
||||
final resp = await _request('PUT', '/devices/${Uri.encodeComponent(deviceId)}/config', deviceConfig.toJson());
|
||||
return MiningDevice.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<void> enableDevice(String deviceId) async {
|
||||
await setDeviceConfig(deviceId, const DeviceConfig(enabled: true));
|
||||
}
|
||||
|
||||
Future<void> disableDevice(String deviceId) async {
|
||||
await setDeviceConfig(deviceId, const DeviceConfig(enabled: false));
|
||||
}
|
||||
|
||||
// ==================== Worker Operations ====================
|
||||
|
||||
Future<List<WorkerInfo>> listWorkers() async {
|
||||
final resp = await _request('GET', '/workers');
|
||||
return (resp['workers'] as List).map((e) => WorkerInfo.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<WorkerInfo> getWorker(String workerId) async {
|
||||
final resp = await _request('GET', '/workers/${Uri.encodeComponent(workerId)}');
|
||||
return WorkerInfo.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<void> removeWorker(String workerId) async {
|
||||
await _request('DELETE', '/workers/${Uri.encodeComponent(workerId)}');
|
||||
}
|
||||
|
||||
// ==================== Algorithm Operations ====================
|
||||
|
||||
Future<List<MiningAlgorithm>> listAlgorithms() async {
|
||||
final resp = await _request('GET', '/algorithms');
|
||||
return (resp['algorithms'] as List).map((e) => MiningAlgorithm.fromJson(e)).toList();
|
||||
}
|
||||
|
||||
Future<MiningAlgorithm> getAlgorithm(String name) async {
|
||||
final resp = await _request('GET', '/algorithms/${Uri.encodeComponent(name)}');
|
||||
return MiningAlgorithm.fromJson(resp);
|
||||
}
|
||||
|
||||
Future<void> switchAlgorithm(String algorithm) async {
|
||||
await _request('POST', '/algorithms/switch', {'algorithm': algorithm});
|
||||
}
|
||||
|
||||
Future<MiningAlgorithm> 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<bool> healthCheck() async {
|
||||
try {
|
||||
final resp = await _request('GET', '/health');
|
||||
return resp['status'] == 'healthy';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
Future<Map<String, dynamic>> _request(String method, String path, [Map<String, dynamic>? body]) async {
|
||||
if (_closed) throw const 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<Map<String, dynamic>> _doRequest(String method, String path, Map<String, dynamic>? body) async {
|
||||
final uri = Uri.parse('${config.endpoint}$path');
|
||||
final headers = {
|
||||
'Authorization': 'Bearer ${config.apiKey}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-SDK-Version': 'flutter/0.1.0',
|
||||
};
|
||||
|
||||
http.Response response;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
response = await _client.get(uri, headers: headers).timeout(config.timeout);
|
||||
break;
|
||||
case 'POST':
|
||||
response = await _client.post(uri, headers: headers, body: body != null ? jsonEncode(body) : null).timeout(config.timeout);
|
||||
break;
|
||||
case '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<String, dynamic>;
|
||||
if (response.statusCode >= 400) {
|
||||
throw MiningException(
|
||||
respBody['message'] as String? ?? 'HTTP ${response.statusCode}',
|
||||
code: respBody['code'] as String?,
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
return respBody;
|
||||
}
|
||||
}
|
||||
555
sdk/flutter/lib/src/mining/types.dart
Normal file
555
sdk/flutter/lib/src/mining/types.dart
Normal file
|
|
@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<TemplateTransaction> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<EarningsBreakdown> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<MiningDevice> 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<String, dynamic> 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<String, dynamic> 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';
|
||||
}
|
||||
298
sdk/java/src/main/java/io/synor/economics/SynorEconomics.java
Normal file
298
sdk/java/src/main/java/io/synor/economics/SynorEconomics.java
Normal file
|
|
@ -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<Price> getPrice(ServiceType service, UsageMetrics usage) {
|
||||
Map<String, Object> 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<CostEstimate> estimateCost(UsagePlan plan) {
|
||||
Map<String, Object> 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<List<PricingTier>> getPricingTiers(ServiceType service) {
|
||||
return request("GET", "/pricing/tiers/" + service.name().toLowerCase(), null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<TiersResponse>(){}.getType();
|
||||
TiersResponse result = gson.fromJson(resp, type);
|
||||
return result.tiers;
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Billing Operations ====================
|
||||
|
||||
public CompletableFuture<Usage> 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<Usage> 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<List<Invoice>> getInvoices() {
|
||||
return request("GET", "/billing/invoices", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<InvoicesResponse>(){}.getType();
|
||||
InvoicesResponse result = gson.fromJson(resp, type);
|
||||
return result.invoices;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Invoice> getInvoice(String invoiceId) {
|
||||
return request("GET", "/billing/invoices/" + encode(invoiceId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Invoice.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> downloadInvoice(String invoiceId) {
|
||||
return request("GET", "/billing/invoices/" + encode(invoiceId) + "/pdf", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
String base64 = (String) map.get("data");
|
||||
return java.util.Base64.getDecoder().decode(base64);
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<AccountBalance> getBalance() {
|
||||
return request("GET", "/billing/balance", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, AccountBalance.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> addCredits(String amount, String paymentMethod) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("amount", amount);
|
||||
body.put("paymentMethod", paymentMethod);
|
||||
return request("POST", "/billing/credits", body)
|
||||
.thenApply(resp -> null);
|
||||
}
|
||||
|
||||
// ==================== Staking Operations ====================
|
||||
|
||||
public CompletableFuture<StakeReceipt> stake(String amount, Integer durationDays) {
|
||||
Map<String, Object> 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<UnstakeReceipt> unstake(String stakeId) {
|
||||
return request("POST", "/staking/unstake/" + encode(stakeId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, UnstakeReceipt.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<StakeInfo>> getStakes() {
|
||||
return request("GET", "/staking/stakes", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<StakesResponse>(){}.getType();
|
||||
StakesResponse result = gson.fromJson(resp, type);
|
||||
return result.stakes;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<StakeInfo> getStake(String stakeId) {
|
||||
return request("GET", "/staking/stakes/" + encode(stakeId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, StakeInfo.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Rewards> getStakingRewards() {
|
||||
return request("GET", "/staking/rewards", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Rewards.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<String> claimRewards() {
|
||||
return request("POST", "/staking/rewards/claim", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return (String) map.get("txHash");
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Double> getCurrentApy() {
|
||||
return request("GET", "/staking/apy", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return ((Number) map.get("apy")).doubleValue();
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Discount Operations ====================
|
||||
|
||||
public CompletableFuture<AppliedDiscount> applyDiscount(String code) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("code", code);
|
||||
return request("POST", "/discounts/apply", body)
|
||||
.thenApply(resp -> gson.fromJson(resp, AppliedDiscount.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Discount>> getAvailableDiscounts() {
|
||||
return request("GET", "/discounts/available", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<DiscountsResponse>(){}.getType();
|
||||
DiscountsResponse result = gson.fromJson(resp, type);
|
||||
return result.discounts;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Discount> validateDiscount(String code) {
|
||||
return request("GET", "/discounts/validate/" + encode(code), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Discount.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<AppliedDiscount>> getAppliedDiscounts() {
|
||||
return request("GET", "/discounts/applied", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<AppliedDiscountsResponse>(){}.getType();
|
||||
AppliedDiscountsResponse result = gson.fromJson(resp, type);
|
||||
return result.discounts;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> 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<Boolean> healthCheck() {
|
||||
return request("GET", "/health", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return "healthy".equals(map.get("status"));
|
||||
})
|
||||
.exceptionally(e -> false);
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
private CompletableFuture<String> 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<String> 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<String, Object> errorBody = gson.fromJson(response.body(),
|
||||
new TypeToken<Map<String, Object>>(){}.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<PricingTier> tiers; }
|
||||
private static class InvoicesResponse { List<Invoice> invoices; }
|
||||
private static class StakesResponse { List<StakeInfo> stakes; }
|
||||
private static class DiscountsResponse { List<Discount> discounts; }
|
||||
private static class AppliedDiscountsResponse { List<AppliedDiscount> discounts; }
|
||||
}
|
||||
383
sdk/java/src/main/java/io/synor/economics/Types.java
Normal file
383
sdk/java/src/main/java/io/synor/economics/Types.java
Normal file
|
|
@ -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<PricingTier> 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<PricingTier> 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<CostBreakdown> 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<CostBreakdown> 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<UsageRecord> records;
|
||||
private String totalCost;
|
||||
private String currency;
|
||||
|
||||
public BillingPeriod getPeriod() { return period; }
|
||||
public long getStartDate() { return startDate; }
|
||||
public long getEndDate() { return endDate; }
|
||||
public List<UsageRecord> 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<InvoiceLineItem> 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<InvoiceLineItem> 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<RewardRecord> history;
|
||||
|
||||
public String getTotalEarned() { return totalEarned; }
|
||||
public String getPendingClaim() { return pendingClaim; }
|
||||
public String getLastClaimDate() { return lastClaimDate; }
|
||||
public double getCurrentApy() { return currentApy; }
|
||||
public List<RewardRecord> 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<ServiceType> 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<ServiceType> 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; }
|
||||
}
|
||||
369
sdk/java/src/main/java/io/synor/governance/SynorGovernance.java
Normal file
369
sdk/java/src/main/java/io/synor/governance/SynorGovernance.java
Normal file
|
|
@ -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<ProposalStatus> 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<Proposal> createProposal(ProposalDraft proposal) {
|
||||
Map<String, Object> 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<Proposal> getProposal(String proposalId) {
|
||||
return request("GET", "/proposals/" + encode(proposalId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Proposal.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Proposal>> 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<ProposalsResponse>(){}.getType();
|
||||
ProposalsResponse result = gson.fromJson(resp, type);
|
||||
return result.proposals;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Proposal> cancelProposal(String proposalId) {
|
||||
return request("POST", "/proposals/" + encode(proposalId) + "/cancel", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Proposal.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Proposal> executeProposal(String proposalId) {
|
||||
return request("POST", "/proposals/" + encode(proposalId) + "/execute", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Proposal.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Proposal> waitForProposal(String proposalId, long pollIntervalMs, long maxWaitMs) {
|
||||
long deadline = System.currentTimeMillis() + maxWaitMs;
|
||||
return waitForProposalInternal(proposalId, pollIntervalMs, deadline);
|
||||
}
|
||||
|
||||
private CompletableFuture<Proposal> 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<VoteReceipt> vote(String proposalId, Vote vote, String weight) {
|
||||
Map<String, Object> 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<List<VoteReceipt>> getVotes(String proposalId) {
|
||||
return request("GET", "/proposals/" + encode(proposalId) + "/votes", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<VotesResponse>(){}.getType();
|
||||
VotesResponse result = gson.fromJson(resp, type);
|
||||
return result.votes;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<VoteReceipt> getMyVote(String proposalId) {
|
||||
return request("GET", "/proposals/" + encode(proposalId) + "/votes/me", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, VoteReceipt.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<DelegationReceipt> delegate(String delegatee, String amount) {
|
||||
Map<String, Object> 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<DelegationReceipt> undelegate(String delegatee) {
|
||||
return request("POST", "/voting/undelegate", Map.of("delegatee", delegatee))
|
||||
.thenApply(resp -> gson.fromJson(resp, DelegationReceipt.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<VotingPower> getVotingPower(String address) {
|
||||
return request("GET", "/voting/power/" + encode(address), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, VotingPower.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<DelegationReceipt>> getDelegations(String address) {
|
||||
return request("GET", "/voting/delegations/" + encode(address), null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<DelegationsResponse>(){}.getType();
|
||||
DelegationsResponse result = gson.fromJson(resp, type);
|
||||
return result.delegations;
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== DAO Operations ====================
|
||||
|
||||
public CompletableFuture<Dao> createDao(DaoConfig daoConfig) {
|
||||
Map<String, Object> 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<Dao> getDao(String daoId) {
|
||||
return request("GET", "/daos/" + encode(daoId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Dao.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Dao>> 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<DaosResponse>(){}.getType();
|
||||
DaosResponse result = gson.fromJson(resp, type);
|
||||
return result.daos;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<DaoTreasury> getDaoTreasury(String daoId) {
|
||||
return request("GET", "/daos/" + encode(daoId) + "/treasury", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, DaoTreasury.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<String>> getDaoMembers(String daoId) {
|
||||
return request("GET", "/daos/" + encode(daoId) + "/members", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<MembersResponse>(){}.getType();
|
||||
MembersResponse result = gson.fromJson(resp, type);
|
||||
return result.members;
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Vesting Operations ====================
|
||||
|
||||
public CompletableFuture<VestingContract> createVestingSchedule(VestingSchedule schedule) {
|
||||
Map<String, Object> 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<VestingContract> getVestingContract(String contractId) {
|
||||
return request("GET", "/vesting/" + encode(contractId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, VestingContract.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<List<VestingContract>> 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<VestingContractsResponse>(){}.getType();
|
||||
VestingContractsResponse result = gson.fromJson(resp, type);
|
||||
return result.contracts;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<ClaimReceipt> claimVested(String contractId) {
|
||||
return request("POST", "/vesting/" + encode(contractId) + "/claim", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, ClaimReceipt.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<VestingContract> revokeVesting(String contractId) {
|
||||
return request("POST", "/vesting/" + encode(contractId) + "/revoke", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, VestingContract.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<String> getReleasableAmount(String contractId) {
|
||||
return request("GET", "/vesting/" + encode(contractId) + "/releasable", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return (String) map.get("amount");
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed.set(true);
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> healthCheck() {
|
||||
return request("GET", "/health", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return "healthy".equals(map.get("status"));
|
||||
})
|
||||
.exceptionally(e -> false);
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
private CompletableFuture<String> 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<String> 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<String, Object> errorBody = gson.fromJson(response.body(),
|
||||
new TypeToken<Map<String, Object>>(){}.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<Proposal> proposals; }
|
||||
private static class VotesResponse { List<VoteReceipt> votes; }
|
||||
private static class DelegationsResponse { List<DelegationReceipt> delegations; }
|
||||
private static class DaosResponse { List<Dao> daos; }
|
||||
private static class MembersResponse { List<String> members; }
|
||||
private static class VestingContractsResponse { List<VestingContract> contracts; }
|
||||
}
|
||||
407
sdk/java/src/main/java/io/synor/governance/Types.java
Normal file
407
sdk/java/src/main/java/io/synor/governance/Types.java
Normal file
|
|
@ -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<ProposalAction> 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<ProposalAction> 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<ProposalAction> 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<ProposalAction> 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<ProposalAction> 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<String> 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<String> 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<String> 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<TreasuryAsset> assets;
|
||||
|
||||
public String getAddress() { return address; }
|
||||
public String getBalance() { return balance; }
|
||||
public String getCurrency() { return currency; }
|
||||
public List<TreasuryAsset> 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; }
|
||||
}
|
||||
329
sdk/java/src/main/java/io/synor/mining/SynorMining.java
Normal file
329
sdk/java/src/main/java/io/synor/mining/SynorMining.java
Normal file
|
|
@ -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<StratumConnection> 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<StratumConnection> connect(PoolConfig pool) {
|
||||
Map<String, Object> 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<Void> 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<StratumConnection> getConnection() {
|
||||
return request("GET", "/pool/connection", null)
|
||||
.thenApply(resp -> {
|
||||
StratumConnection conn = gson.fromJson(resp, StratumConnection.class);
|
||||
activeConnection.set(conn);
|
||||
return conn;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<PoolStats> 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<BlockTemplate> getBlockTemplate() {
|
||||
return request("GET", "/mining/template", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, BlockTemplate.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<SubmitResult> submitWork(MinedWork work) {
|
||||
Map<String, Object> 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<Void> startMining(String algorithm) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
if (algorithm != null) body.put("algorithm", algorithm);
|
||||
return request("POST", "/mining/start", body)
|
||||
.thenApply(resp -> null);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> stopMining() {
|
||||
return request("POST", "/mining/stop", null)
|
||||
.thenApply(resp -> null);
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> isMining() {
|
||||
return request("GET", "/mining/status", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return Boolean.TRUE.equals(map.get("mining"));
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Stats Operations ====================
|
||||
|
||||
public CompletableFuture<Hashrate> getHashrate() {
|
||||
return request("GET", "/stats/hashrate", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, Hashrate.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<MiningStats> getStats() {
|
||||
return request("GET", "/stats", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, MiningStats.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Earnings> 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<ShareStats> getShareStats() {
|
||||
return request("GET", "/stats/shares", null)
|
||||
.thenApply(resp -> gson.fromJson(resp, ShareStats.class));
|
||||
}
|
||||
|
||||
// ==================== Device Operations ====================
|
||||
|
||||
public CompletableFuture<List<MiningDevice>> listDevices() {
|
||||
return request("GET", "/devices", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<DevicesResponse>(){}.getType();
|
||||
DevicesResponse result = gson.fromJson(resp, type);
|
||||
return result.devices;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<MiningDevice> getDevice(String deviceId) {
|
||||
return request("GET", "/devices/" + encode(deviceId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, MiningDevice.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<MiningDevice> setDeviceConfig(String deviceId, DeviceConfig deviceConfig) {
|
||||
Map<String, Object> 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<Void> enableDevice(String deviceId) {
|
||||
DeviceConfig cfg = new DeviceConfig();
|
||||
cfg.setEnabled(true);
|
||||
return setDeviceConfig(deviceId, cfg).thenApply(d -> null);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> disableDevice(String deviceId) {
|
||||
DeviceConfig cfg = new DeviceConfig();
|
||||
cfg.setEnabled(false);
|
||||
return setDeviceConfig(deviceId, cfg).thenApply(d -> null);
|
||||
}
|
||||
|
||||
// ==================== Worker Operations ====================
|
||||
|
||||
public CompletableFuture<List<WorkerInfo>> listWorkers() {
|
||||
return request("GET", "/workers", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<WorkersResponse>(){}.getType();
|
||||
WorkersResponse result = gson.fromJson(resp, type);
|
||||
return result.workers;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<WorkerInfo> getWorker(String workerId) {
|
||||
return request("GET", "/workers/" + encode(workerId), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, WorkerInfo.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> removeWorker(String workerId) {
|
||||
return request("DELETE", "/workers/" + encode(workerId), null)
|
||||
.thenApply(resp -> null);
|
||||
}
|
||||
|
||||
// ==================== Algorithm Operations ====================
|
||||
|
||||
public CompletableFuture<List<MiningAlgorithm>> listAlgorithms() {
|
||||
return request("GET", "/algorithms", null)
|
||||
.thenApply(resp -> {
|
||||
Type type = new TypeToken<AlgorithmsResponse>(){}.getType();
|
||||
AlgorithmsResponse result = gson.fromJson(resp, type);
|
||||
return result.algorithms;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<MiningAlgorithm> getAlgorithm(String name) {
|
||||
return request("GET", "/algorithms/" + encode(name), null)
|
||||
.thenApply(resp -> gson.fromJson(resp, MiningAlgorithm.class));
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> switchAlgorithm(String algorithm) {
|
||||
return request("POST", "/algorithms/switch", Map.of("algorithm", algorithm))
|
||||
.thenApply(resp -> null);
|
||||
}
|
||||
|
||||
public CompletableFuture<MiningAlgorithm> 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<Boolean> healthCheck() {
|
||||
return request("GET", "/health", null)
|
||||
.thenApply(resp -> {
|
||||
Map<String, Object> map = gson.fromJson(resp, new TypeToken<Map<String, Object>>(){}.getType());
|
||||
return "healthy".equals(map.get("status"));
|
||||
})
|
||||
.exceptionally(e -> false);
|
||||
}
|
||||
|
||||
// ==================== Private Methods ====================
|
||||
|
||||
private CompletableFuture<String> 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<String> 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<String, Object> errorBody = gson.fromJson(response.body(),
|
||||
new TypeToken<Map<String, Object>>(){}.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<MiningDevice> devices; }
|
||||
private static class WorkersResponse { List<WorkerInfo> workers; }
|
||||
private static class AlgorithmsResponse { List<MiningAlgorithm> algorithms; }
|
||||
}
|
||||
425
sdk/java/src/main/java/io/synor/mining/Types.java
Normal file
425
sdk/java/src/main/java/io/synor/mining/Types.java
Normal file
|
|
@ -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<TemplateTransaction> 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<TemplateTransaction> 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<EarningsBreakdown> 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<EarningsBreakdown> 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<MiningDevice> 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<MiningDevice> 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; }
|
||||
}
|
||||
415
sdk/kotlin/src/main/kotlin/io/synor/economics/SynorEconomics.kt
Normal file
415
sdk/kotlin/src/main/kotlin/io/synor/economics/SynorEconomics.kt
Normal file
|
|
@ -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<PricingTier> {
|
||||
val resp = request("GET", "/pricing/tiers/${service.name.lowercase()}")
|
||||
val result = gson.fromJson<TiersResponse>(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<Invoice> {
|
||||
val resp = request("GET", "/billing/invoices")
|
||||
val result = gson.fromJson<InvoicesResponse>(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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<String, Any>("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<StakeInfo> {
|
||||
val resp = request("GET", "/staking/stakes")
|
||||
val result = gson.fromJson<StakesResponse>(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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.type)
|
||||
return map["txHash"] as String
|
||||
}
|
||||
|
||||
suspend fun getCurrentApy(): Double {
|
||||
val resp = request("GET", "/staking/apy")
|
||||
val map = gson.fromJson<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<Discount> {
|
||||
val resp = request("GET", "/discounts/available")
|
||||
val result = gson.fromJson<DiscountsResponse>(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<AppliedDiscount> {
|
||||
val resp = request("GET", "/discounts/applied")
|
||||
val result = gson.fromJson<AppliedDiscountsResponse>(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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.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<PricingTier>? = 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<CostBreakdown>? = 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<UsageRecord>,
|
||||
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<InvoiceLineItem>,
|
||||
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<RewardRecord>
|
||||
)
|
||||
|
||||
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<ServiceType>? = 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<PricingTier>?)
|
||||
internal data class InvoicesResponse(val invoices: List<Invoice>?)
|
||||
internal data class StakesResponse(val stakes: List<StakeInfo>?)
|
||||
internal data class DiscountsResponse(val discounts: List<Discount>?)
|
||||
internal data class AppliedDiscountsResponse(val discounts: List<AppliedDiscount>?)
|
||||
|
||||
class EconomicsException(
|
||||
message: String,
|
||||
val code: String? = null,
|
||||
val statusCode: Int = 0
|
||||
) : RuntimeException(message)
|
||||
|
|
@ -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<String, Any>(
|
||||
"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<Proposal> {
|
||||
val params = mutableListOf<String>()
|
||||
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<ProposalsResponse>(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<String, Any>("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<VoteReceipt> {
|
||||
val resp = request("GET", "/proposals/${proposalId.urlEncode()}/votes")
|
||||
val result = gson.fromJson<VotesResponse>(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<String, Any>("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<DelegationReceipt> {
|
||||
val resp = request("GET", "/voting/delegations/${address.urlEncode()}")
|
||||
val result = gson.fromJson<DelegationsResponse>(resp, DelegationsResponse::class.java)
|
||||
return result.delegations ?: emptyList()
|
||||
}
|
||||
|
||||
// ==================== DAO Operations ====================
|
||||
|
||||
suspend fun createDao(daoConfig: DaoConfig): Dao {
|
||||
val body = mutableMapOf<String, Any>(
|
||||
"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<Dao> {
|
||||
val params = mutableListOf<String>()
|
||||
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<DaosResponse>(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<String> {
|
||||
val resp = request("GET", "/daos/${daoId.urlEncode()}/members")
|
||||
val result = gson.fromJson<MembersResponse>(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<VestingContract> {
|
||||
val path = if (beneficiary != null) "/vesting?beneficiary=${beneficiary.urlEncode()}" else "/vesting"
|
||||
val resp = request("GET", path)
|
||||
val result = gson.fromJson<VestingContractsResponse>(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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.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<ProposalAction>? = 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<ProposalAction>? = 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<String>? = 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<TreasuryAsset>
|
||||
)
|
||||
|
||||
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<Proposal>?)
|
||||
internal data class VotesResponse(val votes: List<VoteReceipt>?)
|
||||
internal data class DelegationsResponse(val delegations: List<DelegationReceipt>?)
|
||||
internal data class DaosResponse(val daos: List<Dao>?)
|
||||
internal data class MembersResponse(val members: List<String>?)
|
||||
internal data class VestingContractsResponse(val contracts: List<VestingContract>?)
|
||||
|
||||
class GovernanceException(
|
||||
message: String,
|
||||
val code: String? = null,
|
||||
val statusCode: Int = 0
|
||||
) : RuntimeException(message)
|
||||
464
sdk/kotlin/src/main/kotlin/io/synor/mining/SynorMining.kt
Normal file
464
sdk/kotlin/src/main/kotlin/io/synor/mining/SynorMining.kt
Normal file
|
|
@ -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<StratumConnection?>(null)
|
||||
|
||||
constructor(apiKey: String) : this(MiningConfig(apiKey))
|
||||
|
||||
// ==================== Pool Operations ====================
|
||||
|
||||
suspend fun connect(pool: PoolConfig): StratumConnection {
|
||||
val body = mutableMapOf<String, Any>("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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<MiningDevice> {
|
||||
val resp = request("GET", "/devices")
|
||||
val result = gson.fromJson<DevicesResponse>(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<String, Any>("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<WorkerInfo> {
|
||||
val resp = request("GET", "/workers")
|
||||
val result = gson.fromJson<WorkersResponse>(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<MiningAlgorithm> {
|
||||
val resp = request("GET", "/algorithms")
|
||||
val result = gson.fromJson<AlgorithmsResponse>(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<Map<String, Any>>(resp, object : TypeToken<Map<String, Any>>() {}.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<Map<String, Any>>(responseBody, object : TypeToken<Map<String, Any>>() {}.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<TemplateTransaction>,
|
||||
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<EarningsBreakdown>
|
||||
)
|
||||
|
||||
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<MiningDevice>,
|
||||
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<MiningDevice>?)
|
||||
internal data class WorkersResponse(val workers: List<WorkerInfo>?)
|
||||
internal data class AlgorithmsResponse(val algorithms: List<MiningAlgorithm>?)
|
||||
|
||||
class MiningException(
|
||||
message: String,
|
||||
val code: String? = null,
|
||||
val statusCode: Int = 0
|
||||
) : RuntimeException(message)
|
||||
20
sdk/ruby/lib/synor_economics.rb
Normal file
20
sdk/ruby/lib/synor_economics.rb
Normal file
|
|
@ -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
|
||||
366
sdk/ruby/lib/synor_economics/client.rb
Normal file
366
sdk/ruby/lib/synor_economics/client.rb
Normal file
|
|
@ -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
|
||||
85
sdk/ruby/lib/synor_economics/types.rb
Normal file
85
sdk/ruby/lib/synor_economics/types.rb
Normal file
|
|
@ -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
|
||||
5
sdk/ruby/lib/synor_economics/version.rb
Normal file
5
sdk/ruby/lib/synor_economics/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorEconomics
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
20
sdk/ruby/lib/synor_governance.rb
Normal file
20
sdk/ruby/lib/synor_governance.rb
Normal file
|
|
@ -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
|
||||
422
sdk/ruby/lib/synor_governance/client.rb
Normal file
422
sdk/ruby/lib/synor_governance/client.rb
Normal file
|
|
@ -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
|
||||
74
sdk/ruby/lib/synor_governance/types.rb
Normal file
74
sdk/ruby/lib/synor_governance/types.rb
Normal file
|
|
@ -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
|
||||
5
sdk/ruby/lib/synor_governance/version.rb
Normal file
5
sdk/ruby/lib/synor_governance/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorGovernance
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
20
sdk/ruby/lib/synor_mining.rb
Normal file
20
sdk/ruby/lib/synor_mining.rb
Normal file
|
|
@ -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
|
||||
437
sdk/ruby/lib/synor_mining/client.rb
Normal file
437
sdk/ruby/lib/synor_mining/client.rb
Normal file
|
|
@ -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
|
||||
89
sdk/ruby/lib/synor_mining/types.rb
Normal file
89
sdk/ruby/lib/synor_mining/types.rb
Normal file
|
|
@ -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
|
||||
5
sdk/ruby/lib/synor_mining/version.rb
Normal file
5
sdk/ruby/lib/synor_mining/version.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SynorMining
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
286
sdk/swift/Sources/Synor/Economics/SynorEconomics.swift
Normal file
286
sdk/swift/Sources/Synor/Economics/SynorEconomics.swift
Normal file
|
|
@ -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<T: Decodable>(path: String) async throws -> T {
|
||||
return try await request(method: "GET", path: path)
|
||||
}
|
||||
|
||||
private func post<T: Decodable>(path: String, body: Any) async throws -> T {
|
||||
return try await request(method: "POST", path: path, body: body)
|
||||
}
|
||||
|
||||
private func delete<T: Decodable>(path: String) async throws -> T {
|
||||
return try await request(method: "DELETE", path: path)
|
||||
}
|
||||
|
||||
private func request<T: Decodable>(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..<config.retries {
|
||||
do {
|
||||
return try await doRequest(method: method, path: path, body: body)
|
||||
} catch {
|
||||
lastError = error
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? EconomicsError(message: "Unknown error")
|
||||
}
|
||||
|
||||
private func doRequest<T: Decodable>(method: String, path: String, body: Any?) async throws -> T {
|
||||
guard let url = URL(string: config.endpoint + path) else {
|
||||
throw 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
|
||||
}
|
||||
}
|
||||
314
sdk/swift/Sources/Synor/Economics/Types.swift
Normal file
314
sdk/swift/Sources/Synor/Economics/Types.swift
Normal file
|
|
@ -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 }
|
||||
}
|
||||
286
sdk/swift/Sources/Synor/Governance/SynorGovernance.swift
Normal file
286
sdk/swift/Sources/Synor/Governance/SynorGovernance.swift
Normal file
|
|
@ -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<ProposalStatus> = [.passed, .rejected, .executed, .cancelled]
|
||||
|
||||
public init(config: GovernanceConfig) {
|
||||
self.config = config
|
||||
|
||||
let sessionConfig = URLSessionConfiguration.default
|
||||
sessionConfig.timeoutIntervalForRequest = config.timeout
|
||||
sessionConfig.timeoutIntervalForResource = config.timeout * 2
|
||||
self.session = URLSession(configuration: sessionConfig)
|
||||
|
||||
self.decoder = JSONDecoder()
|
||||
self.encoder = JSONEncoder()
|
||||
}
|
||||
|
||||
// MARK: - Proposal Operations
|
||||
|
||||
/// Create a proposal.
|
||||
public func createProposal(_ proposal: ProposalDraft) async throws -> Proposal {
|
||||
var body: [String: Any] = [
|
||||
"title": proposal.title,
|
||||
"description": proposal.description
|
||||
]
|
||||
if let url = proposal.discussionUrl { body["discussionUrl"] = url }
|
||||
if let start = proposal.votingStartTime { body["votingStartTime"] = start }
|
||||
if let end = proposal.votingEndTime { body["votingEndTime"] = end }
|
||||
if let daoId = proposal.daoId { body["daoId"] = daoId }
|
||||
if let actions = proposal.actions {
|
||||
body["actions"] = actions.map { ["target": $0.target, "method": $0.method, "data": $0.data, "value": $0.value as Any] }
|
||||
}
|
||||
return try await post(path: "/proposals", body: body)
|
||||
}
|
||||
|
||||
/// Get proposal by ID.
|
||||
public func getProposal(proposalId: String) async throws -> Proposal {
|
||||
return try await get(path: "/proposals/\(proposalId.urlEncoded)")
|
||||
}
|
||||
|
||||
/// List proposals.
|
||||
public func listProposals(filter: ProposalFilter? = nil) async throws -> [Proposal] {
|
||||
var params: [String] = []
|
||||
if let status = filter?.status { params.append("status=\(status.rawValue)") }
|
||||
if let proposer = filter?.proposer { params.append("proposer=\(proposer.urlEncoded)") }
|
||||
if let daoId = filter?.daoId { params.append("daoId=\(daoId.urlEncoded)") }
|
||||
if let limit = filter?.limit { params.append("limit=\(limit)") }
|
||||
if let offset = filter?.offset { params.append("offset=\(offset)") }
|
||||
|
||||
let path = params.isEmpty ? "/proposals" : "/proposals?\(params.joined(separator: "&"))"
|
||||
let response: ProposalsResponse = try await get(path: path)
|
||||
return response.proposals ?? []
|
||||
}
|
||||
|
||||
/// Cancel a proposal.
|
||||
public func cancelProposal(proposalId: String) async throws -> Proposal {
|
||||
return try await post(path: "/proposals/\(proposalId.urlEncoded)/cancel", body: [:] as [String: String])
|
||||
}
|
||||
|
||||
/// Execute a passed proposal.
|
||||
public func executeProposal(proposalId: String) async throws -> Proposal {
|
||||
return try await post(path: "/proposals/\(proposalId.urlEncoded)/execute", body: [:] as [String: String])
|
||||
}
|
||||
|
||||
/// Wait for proposal to reach a final state.
|
||||
public func waitForProposal(proposalId: String, pollInterval: TimeInterval = 60, maxWait: TimeInterval = 604800) async throws -> Proposal {
|
||||
let deadline = Date().addingTimeInterval(maxWait)
|
||||
while Date() < deadline {
|
||||
let proposal = try await getProposal(proposalId: proposalId)
|
||||
if Self.finalStatuses.contains(proposal.status) { return proposal }
|
||||
try await Task.sleep(nanoseconds: UInt64(pollInterval * 1_000_000_000))
|
||||
}
|
||||
throw GovernanceError(message: "Timeout waiting for proposal completion")
|
||||
}
|
||||
|
||||
// MARK: - Voting Operations
|
||||
|
||||
/// Vote on a proposal.
|
||||
public func vote(proposalId: String, vote: Vote, weight: String? = nil) async throws -> VoteReceipt {
|
||||
var body: [String: Any] = ["choice": vote.choice.rawValue]
|
||||
if let reason = vote.reason { body["reason"] = reason }
|
||||
if let weight = weight { body["weight"] = weight }
|
||||
return try await post(path: "/proposals/\(proposalId.urlEncoded)/vote", body: body)
|
||||
}
|
||||
|
||||
/// Get votes for a proposal.
|
||||
public func getVotes(proposalId: String) async throws -> [VoteReceipt] {
|
||||
let response: VotesResponse = try await get(path: "/proposals/\(proposalId.urlEncoded)/votes")
|
||||
return response.votes ?? []
|
||||
}
|
||||
|
||||
/// Get my vote on a proposal.
|
||||
public func getMyVote(proposalId: String) async throws -> VoteReceipt {
|
||||
return try await get(path: "/proposals/\(proposalId.urlEncoded)/votes/me")
|
||||
}
|
||||
|
||||
/// Delegate voting power.
|
||||
public func delegate(delegatee: String, amount: String? = nil) async throws -> DelegationReceipt {
|
||||
var body: [String: Any] = ["delegatee": delegatee]
|
||||
if let amount = amount { body["amount"] = amount }
|
||||
return try await post(path: "/voting/delegate", body: body)
|
||||
}
|
||||
|
||||
/// Undelegate voting power.
|
||||
public func undelegate(delegatee: String) async throws -> DelegationReceipt {
|
||||
return try await post(path: "/voting/undelegate", body: ["delegatee": delegatee])
|
||||
}
|
||||
|
||||
/// Get voting power for an address.
|
||||
public func getVotingPower(address: String) async throws -> VotingPower {
|
||||
return try await get(path: "/voting/power/\(address.urlEncoded)")
|
||||
}
|
||||
|
||||
/// Get delegations for an address.
|
||||
public func getDelegations(address: String) async throws -> [DelegationReceipt] {
|
||||
let response: DelegationsResponse = try await get(path: "/voting/delegations/\(address.urlEncoded)")
|
||||
return response.delegations ?? []
|
||||
}
|
||||
|
||||
// MARK: - DAO Operations
|
||||
|
||||
/// Create a DAO.
|
||||
public func createDao(_ daoConfig: DaoConfig) async throws -> Dao {
|
||||
var body: [String: Any] = [
|
||||
"name": daoConfig.name,
|
||||
"description": daoConfig.description,
|
||||
"type": daoConfig.type.rawValue,
|
||||
"votingPeriodDays": daoConfig.votingPeriodDays,
|
||||
"timelockDays": daoConfig.timelockDays
|
||||
]
|
||||
if let token = daoConfig.tokenAddress { body["tokenAddress"] = token }
|
||||
if let quorum = daoConfig.quorumPercent { body["quorumPercent"] = quorum }
|
||||
if let threshold = daoConfig.proposalThreshold { body["proposalThreshold"] = threshold }
|
||||
if let members = daoConfig.multisigMembers { body["multisigMembers"] = members }
|
||||
if let threshold = daoConfig.multisigThreshold { body["multisigThreshold"] = threshold }
|
||||
return try await post(path: "/daos", body: body)
|
||||
}
|
||||
|
||||
/// Get DAO by ID.
|
||||
public func getDao(daoId: String) async throws -> Dao {
|
||||
return try await get(path: "/daos/\(daoId.urlEncoded)")
|
||||
}
|
||||
|
||||
/// List DAOs.
|
||||
public func listDaos(limit: Int? = nil, offset: Int? = nil) async throws -> [Dao] {
|
||||
var params: [String] = []
|
||||
if let limit = limit { params.append("limit=\(limit)") }
|
||||
if let offset = offset { params.append("offset=\(offset)") }
|
||||
let path = params.isEmpty ? "/daos" : "/daos?\(params.joined(separator: "&"))"
|
||||
let response: DaosResponse = try await get(path: path)
|
||||
return response.daos ?? []
|
||||
}
|
||||
|
||||
/// Get DAO treasury.
|
||||
public func getDaoTreasury(daoId: String) async throws -> DaoTreasury {
|
||||
return try await get(path: "/daos/\(daoId.urlEncoded)/treasury")
|
||||
}
|
||||
|
||||
/// Get DAO members.
|
||||
public func getDaoMembers(daoId: String) async throws -> [String] {
|
||||
let response: MembersResponse = try await get(path: "/daos/\(daoId.urlEncoded)/members")
|
||||
return response.members ?? []
|
||||
}
|
||||
|
||||
// MARK: - Vesting Operations
|
||||
|
||||
/// Create a vesting schedule.
|
||||
public func createVestingSchedule(_ schedule: VestingSchedule) async throws -> VestingContract {
|
||||
let body: [String: Any] = [
|
||||
"beneficiary": schedule.beneficiary,
|
||||
"totalAmount": schedule.totalAmount,
|
||||
"startTime": schedule.startTime,
|
||||
"cliffDuration": schedule.cliffDuration,
|
||||
"vestingDuration": schedule.vestingDuration,
|
||||
"revocable": schedule.revocable
|
||||
]
|
||||
return try await post(path: "/vesting", body: body)
|
||||
}
|
||||
|
||||
/// Get vesting contract.
|
||||
public func getVestingContract(contractId: String) async throws -> VestingContract {
|
||||
return try await get(path: "/vesting/\(contractId.urlEncoded)")
|
||||
}
|
||||
|
||||
/// List vesting contracts.
|
||||
public func listVestingContracts(beneficiary: String? = nil) async throws -> [VestingContract] {
|
||||
let path = beneficiary != nil ? "/vesting?beneficiary=\(beneficiary!.urlEncoded)" : "/vesting"
|
||||
let response: VestingContractsResponse = try await get(path: path)
|
||||
return response.contracts ?? []
|
||||
}
|
||||
|
||||
/// Claim vested tokens.
|
||||
public func claimVested(contractId: String) async throws -> ClaimReceipt {
|
||||
return try await post(path: "/vesting/\(contractId.urlEncoded)/claim", body: [:] as [String: String])
|
||||
}
|
||||
|
||||
/// Revoke vesting.
|
||||
public func revokeVesting(contractId: String) async throws -> VestingContract {
|
||||
return try await post(path: "/vesting/\(contractId.urlEncoded)/revoke", body: [:] as [String: String])
|
||||
}
|
||||
|
||||
/// Get releasable amount.
|
||||
public func getReleasableAmount(contractId: String) async throws -> String {
|
||||
let response: [String: String] = try await get(path: "/vesting/\(contractId.urlEncoded)/releasable")
|
||||
return response["amount"] ?? "0"
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
public func close() { closed = true }
|
||||
public var isClosed: Bool { closed }
|
||||
|
||||
public func healthCheck() async -> Bool {
|
||||
do {
|
||||
let response: [String: String] = try await get(path: "/health")
|
||||
return response["status"] == "healthy"
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func get<T: Decodable>(path: String) async throws -> T {
|
||||
return try await request(method: "GET", path: path)
|
||||
}
|
||||
|
||||
private func post<T: Decodable>(path: String, body: Any) async throws -> T {
|
||||
return try await request(method: "POST", path: path, body: body)
|
||||
}
|
||||
|
||||
private func request<T: Decodable>(method: String, path: String, body: Any? = nil) async throws -> T {
|
||||
guard !closed else { throw GovernanceError(message: "Client has been closed") }
|
||||
|
||||
var lastError: Error?
|
||||
for attempt in 0..<config.retries {
|
||||
do {
|
||||
return try await doRequest(method: method, path: path, body: body)
|
||||
} catch {
|
||||
lastError = error
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? GovernanceError(message: "Unknown error")
|
||||
}
|
||||
|
||||
private func doRequest<T: Decodable>(method: String, path: String, body: Any?) async throws -> T {
|
||||
guard let url = URL(string: config.endpoint + path) else { throw GovernanceError(message: "Invalid URL") }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method
|
||||
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
||||
|
||||
if let body = body { request.httpBody = try JSONSerialization.data(withJSONObject: body) }
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else { throw GovernanceError(message: "Invalid response") }
|
||||
|
||||
if httpResponse.statusCode >= 400 {
|
||||
let errorInfo = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
throw GovernanceError(
|
||||
message: errorInfo?["message"] as? String ?? "HTTP \(httpResponse.statusCode)",
|
||||
code: errorInfo?["code"] as? String,
|
||||
statusCode: httpResponse.statusCode
|
||||
)
|
||||
}
|
||||
return try decoder.decode(T.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
fileprivate var urlEncoded: String {
|
||||
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
|
||||
}
|
||||
}
|
||||
374
sdk/swift/Sources/Synor/Governance/Types.swift
Normal file
374
sdk/swift/Sources/Synor/Governance/Types.swift
Normal file
|
|
@ -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 }
|
||||
}
|
||||
279
sdk/swift/Sources/Synor/Mining/SynorMining.swift
Normal file
279
sdk/swift/Sources/Synor/Mining/SynorMining.swift
Normal file
|
|
@ -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<T: Decodable>(path: String) async throws -> T {
|
||||
return try await request(method: "GET", path: path)
|
||||
}
|
||||
|
||||
private func post<T: Decodable>(path: String, body: Any) async throws -> T {
|
||||
return try await request(method: "POST", path: path, body: body)
|
||||
}
|
||||
|
||||
private func put<T: Decodable>(path: String, body: Any) async throws -> T {
|
||||
return try await request(method: "PUT", path: path, body: body)
|
||||
}
|
||||
|
||||
private func delete<T: Decodable>(path: String) async throws -> T {
|
||||
return try await request(method: "DELETE", path: path)
|
||||
}
|
||||
|
||||
private func request<T: Decodable>(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..<config.retries {
|
||||
do {
|
||||
return try await doRequest(method: method, path: path, body: body)
|
||||
} catch {
|
||||
lastError = error
|
||||
if attempt < config.retries - 1 {
|
||||
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt))) * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError ?? MiningError(message: "Unknown error")
|
||||
}
|
||||
|
||||
private func doRequest<T: Decodable>(method: String, path: String, body: Any?) async throws -> T {
|
||||
guard let url = URL(string: config.endpoint + path) else { throw 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
|
||||
}
|
||||
}
|
||||
352
sdk/swift/Sources/Synor/Mining/Types.swift
Normal file
352
sdk/swift/Sources/Synor/Mining/Types.swift
Normal file
|
|
@ -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 }
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue