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).
422 lines
12 KiB
Ruby
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
|