synor/sdk/ruby/lib/synor_governance/client.rb
Gulshan Yadav 6607223c9e 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).
2026-01-28 08:33:20 +05:30

422 lines
12 KiB
Ruby

# 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