ecadlabs/go-tezos

View on GitHub
service.go

Summary

Maintainability
F
4 days
Test Coverage
package tezos

import (
    "context"
    "encoding/json"
    "fmt"
    "math/big"
    "net/http"
    "net/url"
    "time"

    "gopkg.in/yaml.v3"
)

// Service implements fetching of information from Tezos nodes via JSON.
type Service struct {
    Client *RPCClient
}

// NetworkStats models global network bandwidth totals and usage in B/s.
type NetworkStats struct {
    TotalBytesSent int64 `json:"total_sent,string"`
    TotalBytesRecv int64 `json:"total_recv,string"`
    CurrentInflow  int64 `json:"current_inflow"`
    CurrentOutflow int64 `json:"current_outflow"`
}

// NetworkConnection models detailed information for one network connection.
type NetworkConnection struct {
    Incoming         bool              `json:"incoming"`
    PeerID           string            `json:"peer_id"`
    IDPoint          NetworkAddress    `json:"id_point"`
    RemoteSocketPort uint16            `json:"remote_socket_port"`
    Versions         []*NetworkVersion `json:"versions"`
    Private          bool              `json:"private"`
    LocalMetadata    NetworkMetadata   `json:"local_metadata"`
    RemoteMetadata   NetworkMetadata   `json:"remote_metadata"`
}

// NetworkAddress models a point's address and port.
type NetworkAddress struct {
    Addr string `json:"addr"`
    Port uint16 `json:"port"`
}

// NetworkVersion models a network-layer version of a node.
type NetworkVersion struct {
    Name  string `json:"name"`
    Major uint16 `json:"major"`
    Minor uint16 `json:"minor"`
}

// NetworkMetadata models metadata of a node.
type NetworkMetadata struct {
    DisableMempool bool `json:"disable_mempool"`
    PrivateNode    bool `json:"private_node"`
}

// BootstrappedBlock represents bootstrapped block stream message
type BootstrappedBlock struct {
    Block     string    `json:"block"`
    Timestamp time.Time `json:"timestamp"`
}

// NetworkConnectionTimestamp represents peer address with timestamp added
type NetworkConnectionTimestamp struct {
    NetworkAddress
    Timestamp time.Time
}

// UnmarshalJSON implements json.Unmarshaler
func (n *NetworkConnectionTimestamp) UnmarshalJSON(data []byte) error {
    return unmarshalHeterogeneousJSONArray(data, &n.NetworkAddress, &n.Timestamp)
}

// NetworkPeer represents peer info
type NetworkPeer struct {
    PeerID                    string                      `json:"-"`
    Score                     int64                       `json:"score"`
    Trusted                   bool                        `json:"trusted"`
    ConnMetadata              *NetworkMetadata            `json:"conn_metadata"`
    State                     string                      `json:"state"`
    ReachableAt               *NetworkAddress             `json:"reachable_at"`
    Stat                      NetworkStats                `json:"stat"`
    LastEstablishedConnection *NetworkConnectionTimestamp `json:"last_established_connection"`
    LastSeen                  *NetworkConnectionTimestamp `json:"last_seen"`
    LastFailedConnection      *NetworkConnectionTimestamp `json:"last_failed_connection"`
    LastRejectedConnection    *NetworkConnectionTimestamp `json:"last_rejected_connection"`
    LastDisconnection         *NetworkConnectionTimestamp `json:"last_disconnection"`
    LastMiss                  *NetworkConnectionTimestamp `json:"last_miss"`
}

// networkPeerWithID is a heterogeneously encoded NetworkPeer with ID as a first array member
// See OperationAlt for details
type networkPeerWithID NetworkPeer

func (n *networkPeerWithID) UnmarshalJSON(data []byte) error {
    return unmarshalHeterogeneousJSONArray(data, &n.PeerID, (*NetworkPeer)(n))
}

// NetworkPeerLogEntry represents peer log entry
type NetworkPeerLogEntry struct {
    NetworkAddress
    Kind      string    `json:"kind"`
    Timestamp time.Time `json:"timestamp"`
}

// NetworkPoint represents network point info
type NetworkPoint struct {
    Address                   string            `json:"-"`
    Trusted                   bool              `json:"trusted"`
    GreylistedUntil           time.Time         `json:"greylisted_until"`
    State                     NetworkPointState `json:"state"`
    P2PPeerID                 string            `json:"p2p_peer_id"`
    LastFailedConnection      time.Time         `json:"last_failed_connection"`
    LastRejectedConnection    *IDTimestamp      `json:"last_rejected_connection"`
    LastEstablishedConnection *IDTimestamp      `json:"last_established_connection"`
    LastDisconnection         *IDTimestamp      `json:"last_disconnection"`
    LastSeen                  *IDTimestamp      `json:"last_seen"`
    LastMiss                  time.Time         `json:"last_miss"`
}

// networkPointAlt is a heterogeneously encoded NetworkPoint with address as a first array member
// See OperationAlt for details
type networkPointAlt NetworkPoint

func (n *networkPointAlt) UnmarshalJSON(data []byte) error {
    return unmarshalHeterogeneousJSONArray(data, &n.Address, (*NetworkPoint)(n))
}

// NetworkPointState represents point state
type NetworkPointState struct {
    EventKind string `json:"event_kind"`
    P2PPeerID string `json:"p2p_peer_id"`
}

// IDTimestamp represents peer id with timestamp
type IDTimestamp struct {
    ID        string
    Timestamp time.Time
}

// UnmarshalJSON implements json.Unmarshaler
func (i *IDTimestamp) UnmarshalJSON(data []byte) error {
    return unmarshalHeterogeneousJSONArray(data, &i.ID, &i.Timestamp)
}

// NetworkPointLogEntry represents point's log entry
type NetworkPointLogEntry struct {
    Kind      NetworkPointState `json:"kind"`
    Timestamp time.Time         `json:"timestamp"`
}

// MempoolOperations represents mempool operations
type MempoolOperations struct {
    Applied       []*Operation             `json:"applied"`
    Refused       []*OperationWithErrorAlt `json:"refused"`
    BranchRefused []*OperationWithErrorAlt `json:"branch_refused"`
    BranchDelayed []*OperationWithErrorAlt `json:"branch_delayed"`
    Unprocessed   []*OperationAlt          `json:"unprocessed"`
}

// InvalidBlock represents invalid block hash along with the errors that led to it being declared invalid
type InvalidBlock struct {
    Block string `json:"block"`
    Level int    `json:"level"`
    Error Errors `json:"error"`
}

type proposalsRPCResponse = [][]interface{}

// BigInt overrides UnmarshalJSON for big.Int
type BigInt struct {
    big.Int
}

// UnmarshalJSON implements json.Unmarshaler
func (z *BigInt) UnmarshalJSON(data []byte) error {
    var s string
    // basically unquote only
    if err := json.Unmarshal(data, &s); err != nil {
        return err
    }

    return z.UnmarshalText([]byte(s))
}

// MarshalYAML implements yaml.Marshaler
func (z *BigInt) MarshalYAML() (interface{}, error) {
    return &yaml.Node{
        Kind:  yaml.ScalarNode,
        Value: z.String(),
    }, nil
}

// GetNetworkStats returns current network stats https://tezos.gitlab.io/betanet/api/rpc.html#get-network-stat
func (s *Service) GetNetworkStats(ctx context.Context) (*NetworkStats, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/stat", nil)
    if err != nil {
        return nil, err
    }

    var stats NetworkStats
    if err = s.Client.Do(req, &stats); err != nil {
        return nil, err
    }
    return &stats, err
}

// GetNetworkConnections returns all network connections http://tezos.gitlab.io/mainnet/api/rpc.html#get-network-connections
func (s *Service) GetNetworkConnections(ctx context.Context) ([]*NetworkConnection, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/connections", nil)
    if err != nil {
        return nil, err
    }

    var conns []*NetworkConnection
    if err = s.Client.Do(req, &conns); err != nil {
        return nil, err
    }
    return conns, err
}

// GetNetworkPeers returns the list the peers the node ever met.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers
func (s *Service) GetNetworkPeers(ctx context.Context, filter string) ([]*NetworkPeer, error) {
    u := url.URL{
        Path: "/network/peers",
    }

    if filter != "" {
        q := url.Values{
            "filter": []string{filter},
        }
        u.RawQuery = q.Encode()
    }

    req, err := s.Client.NewRequest(ctx, http.MethodGet, u.String(), nil)
    if err != nil {
        return nil, err
    }

    var peers []*networkPeerWithID
    if err = s.Client.Do(req, &peers); err != nil {
        return nil, err
    }

    ret := make([]*NetworkPeer, len(peers))
    for i, p := range peers {
        ret[i] = (*NetworkPeer)(p)
    }

    return ret, err
}

// GetNetworkPeer returns details about a given peer.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id
func (s *Service) GetNetworkPeer(ctx context.Context, peerID string) (*NetworkPeer, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/peers/"+peerID, nil)
    if err != nil {
        return nil, err
    }

    var peer NetworkPeer
    if err = s.Client.Do(req, &peer); err != nil {
        return nil, err
    }
    peer.PeerID = peerID

    return &peer, err
}

// BanNetworkPeer blacklists the given peer.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-ban
func (s *Service) BanNetworkPeer(ctx context.Context, peerID string) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/peers/"+peerID+"/ban", nil)
    if err != nil {
        return err
    }

    if err := s.Client.Do(req, nil); err != nil {
        return err
    }
    return nil
}

// TrustNetworkPeer used to trust a given peer permanently: the peer cannot be blocked (but its host IP still can).
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-trust
func (s *Service) TrustNetworkPeer(ctx context.Context, peerID string) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/peers/"+peerID+"/trust", nil)
    if err != nil {
        return err
    }

    if err := s.Client.Do(req, nil); err != nil {
        return err
    }
    return nil
}

// GetNetworkPeerBanned checks if a given peer is blacklisted or greylisted.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-banned
func (s *Service) GetNetworkPeerBanned(ctx context.Context, peerID string) (bool, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/peers/"+peerID+"/banned", nil)
    if err != nil {
        return false, err
    }

    var banned bool
    if err = s.Client.Do(req, &banned); err != nil {
        return false, err
    }

    return banned, err
}

// GetNetworkPeerLog monitors network events related to a given peer.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-log
func (s *Service) GetNetworkPeerLog(ctx context.Context, peerID string) ([]*NetworkPeerLogEntry, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/peers/"+peerID+"/log", nil)
    if err != nil {
        return nil, err
    }

    var log []*NetworkPeerLogEntry
    if err = s.Client.Do(req, &log); err != nil {
        return nil, err
    }

    return log, err
}

// MonitorNetworkPeerLog monitors network events related to a given peer.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-log
func (s *Service) MonitorNetworkPeerLog(ctx context.Context, peerID string, results chan<- []*NetworkPeerLogEntry) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/peers/"+peerID+"/log?monitor", nil)
    if err != nil {
        return err
    }

    return s.Client.Do(req, results)
}

// GetNetworkPoints returns list the pool of known `IP:port` used for establishing P2P connections.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-points
func (s *Service) GetNetworkPoints(ctx context.Context, filter string) ([]*NetworkPoint, error) {
    u := url.URL{
        Path: "/network/points",
    }

    if filter != "" {
        q := url.Values{
            "filter": []string{filter},
        }
        u.RawQuery = q.Encode()
    }

    req, err := s.Client.NewRequest(ctx, http.MethodGet, u.String(), nil)
    if err != nil {
        return nil, err
    }

    var points []*networkPointAlt
    if err = s.Client.Do(req, &points); err != nil {
        return nil, err
    }

    ret := make([]*NetworkPoint, len(points))
    for i, p := range points {
        ret[i] = (*NetworkPoint)(p)
    }

    return ret, err
}

// GetNetworkPoint returns details about a given `IP:addr`.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-points-point
func (s *Service) GetNetworkPoint(ctx context.Context, address string) (*NetworkPoint, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/points/"+address, nil)
    if err != nil {
        return nil, err
    }

    var point NetworkPoint
    if err = s.Client.Do(req, &point); err != nil {
        return nil, err
    }
    point.Address = address

    return &point, err
}

// ConnectToNetworkPoint used to connect to a peer.
// https://tezos.gitlab.io/mainnet/api/rpc.html#put-network-points-point
func (s *Service) ConnectToNetworkPoint(ctx context.Context, address string, timeout time.Duration) error {
    u := url.URL{
        Path: "/network/points/" + address,
    }

    if timeout > 0 {
        q := url.Values{
            "timeout": []string{fmt.Sprintf("%f", float64(timeout)/float64(time.Second))},
        }
        u.RawQuery = q.Encode()
    }

    req, err := s.Client.NewRequest(ctx, http.MethodPut, u.String(), &struct{}{})
    if err != nil {
        return err
    }

    if err := s.Client.Do(req, nil); err != nil {
        return err
    }

    return nil
}

// BanNetworkPoint blacklists the given address.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-points-point-ban
func (s *Service) BanNetworkPoint(ctx context.Context, address string) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/points/"+address+"/ban", nil)
    if err != nil {
        return err
    }

    if err := s.Client.Do(req, nil); err != nil {
        return err
    }
    return nil
}

// TrustNetworkPoint used to trust a given address permanently. Connections from this address can still be closed on authentication if the peer is blacklisted or greylisted.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-points-point-trust
func (s *Service) TrustNetworkPoint(ctx context.Context, address string) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/points/"+address+"/trust", nil)
    if err != nil {
        return err
    }

    if err := s.Client.Do(req, nil); err != nil {
        return err
    }
    return nil
}

// GetNetworkPointBanned check is a given address is blacklisted or greylisted.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-points-point-banned
func (s *Service) GetNetworkPointBanned(ctx context.Context, address string) (bool, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/points/"+address+"/banned", nil)
    if err != nil {
        return false, err
    }

    var banned bool
    if err = s.Client.Do(req, &banned); err != nil {
        return false, err
    }

    return banned, err
}

// GetNetworkPointLog monitors network events related to an `IP:addr`.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-log
func (s *Service) GetNetworkPointLog(ctx context.Context, address string) ([]*NetworkPointLogEntry, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/points/"+address+"/log", nil)
    if err != nil {
        return nil, err
    }

    var log []*NetworkPointLogEntry
    if err = s.Client.Do(req, &log); err != nil {
        return nil, err
    }

    return log, err
}

// MonitorNetworkPointLog monitors network events related to an `IP:addr`.
// https://tezos.gitlab.io/mainnet/api/rpc.html#get-network-peers-peer-id-log
func (s *Service) MonitorNetworkPointLog(ctx context.Context, address string, results chan<- []*NetworkPointLogEntry) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/network/points/"+address+"/log?monitor", nil)
    if err != nil {
        return err
    }

    return s.Client.Do(req, results)
}

// GetDelegateBalance returns a delegate's balance http://tezos.gitlab.io/mainnet/api/rpc.html#get-block-id-context-delegates-pkh-balance
func (s *Service) GetDelegateBalance(ctx context.Context, chainID string, blockID string, pkh string) (*big.Int, error) {
    u := "/chains/" + chainID + "/blocks/" + blockID + "/context/delegates/" + pkh + "/balance"
    req, err := s.Client.NewRequest(ctx, http.MethodGet, u, nil)
    if err != nil {
        return nil, err
    }

    var balance BigInt
    if err := s.Client.Do(req, &balance); err != nil {
        return nil, err
    }

    return (*big.Int)(&balance.Int), nil
}

// GetContractBalance returns a contract's balance http://tezos.gitlab.io/mainnet/api/rpc.html#get-block-id-context-contracts-contract-id-balance
func (s *Service) GetContractBalance(ctx context.Context, chainID string, blockID string, contractID string) (*big.Int, error) {
    u := "/chains/" + chainID + "/blocks/" + blockID + "/context/contracts/" + contractID + "/balance"
    req, err := s.Client.NewRequest(ctx, http.MethodGet, u, nil)
    if err != nil {
        return nil, err
    }

    var balance BigInt
    if err := s.Client.Do(req, &balance); err != nil {
        return nil, err
    }

    return (*big.Int)(&balance.Int), nil
}

// MonitorBootstrapped reads from the bootstrapped blocks stream http://tezos.gitlab.io/mainnet/api/rpc.html#get-monitor-bootstrapped
func (s *Service) MonitorBootstrapped(ctx context.Context, results chan<- *BootstrappedBlock) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/monitor/bootstrapped", nil)
    if err != nil {
        return err
    }

    return s.Client.Do(req, results)
}

// MonitorHeads reads from the heads blocks stream https://tezos.gitlab.io/mainnet/api/rpc.html#get-monitor-heads-chain-id
func (s *Service) MonitorHeads(ctx context.Context, chainID string, results chan<- *BlockInfo) error {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/monitor/heads/"+chainID, nil)
    if err != nil {
        return err
    }

    return s.Client.Do(req, results)
}

// GetMempoolPendingOperations returns mempool pending operations
func (s *Service) GetMempoolPendingOperations(ctx context.Context, chainID string) (*MempoolOperations, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/mempool/pending_operations", nil)
    if err != nil {
        return nil, err
    }

    var ops MempoolOperations
    if err := s.Client.Do(req, &ops); err != nil {
        return nil, err
    }

    return &ops, nil
}

// MonitorMempoolOperations monitors mempool pending operations.
// The connection is closed after every new block.
func (s *Service) MonitorMempoolOperations(ctx context.Context, chainID, filter string, results chan<- []*Operation) error {
    if filter == "" {
        filter = "applied"
    }

    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/mempool/monitor_operations?"+filter, nil)
    if err != nil {
        return err
    }

    return s.Client.Do(req, results)
}

// GetInvalidBlocks lists blocks that have been declared invalid along with the errors that led to them being declared invalid.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-chains-chain-id-invalid-blocks
func (s *Service) GetInvalidBlocks(ctx context.Context, chainID string) ([]*InvalidBlock, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/invalid_blocks", nil)
    if err != nil {
        return nil, err
    }

    var invalidBlocks []*InvalidBlock
    if err := s.Client.Do(req, &invalidBlocks); err != nil {
        return nil, err
    }

    return invalidBlocks, nil
}

// GetBlock returns information about a Tezos block
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id
func (s *Service) GetBlock(ctx context.Context, chainID, blockID string) (*Block, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID, nil)
    if err != nil {
        return nil, err
    }

    var block Block
    if err := s.Client.Do(req, &block); err != nil {
        return nil, err
    }

    return &block, nil
}

// GetBallotList returns ballots casted so far during a voting period.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-ballot-list
func (s *Service) GetBallotList(ctx context.Context, chainID, blockID string) ([]*Ballot, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/ballot_list", nil)
    if err != nil {
        return nil, err
    }

    var ballots []*Ballot
    if err := s.Client.Do(req, &ballots); err != nil {
        return nil, err
    }

    return ballots, nil
}

// GetBallots returns sum of ballots casted so far during a voting period.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-ballots
func (s *Service) GetBallots(ctx context.Context, chainID, blockID string) (*Ballots, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/ballots", nil)
    if err != nil {
        return nil, err
    }

    var ballots Ballots
    if err := s.Client.Do(req, &ballots); err != nil {
        return nil, err
    }

    return &ballots, nil
}

// GetBallotListings returns a list of delegates with their voting weight, in number of rolls.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-listings
func (s *Service) GetBallotListings(ctx context.Context, chainID, blockID string) ([]*BallotListing, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/listings", nil)
    if err != nil {
        return nil, err
    }

    var listings []*BallotListing
    if err := s.Client.Do(req, &listings); err != nil {
        return nil, err
    }

    return listings, nil
}

// GetProposals returns a list of proposals with number of supporters.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-proposals
func (s *Service) GetProposals(ctx context.Context, chainID, blockID string) ([]*Proposal, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/proposals", nil)
    if err != nil {
        return nil, err
    }

    var proposalsResp proposalsRPCResponse
    if err := s.Client.Do(req, &proposalsResp); err != nil {
        return nil, err
    }

    proposals := make([]*Proposal, len(proposalsResp))

    for i, proposalResp := range proposalsResp {
        if len(proposalResp) == 2 {
            proposal := &Proposal{}
            if propHash, ok := proposalResp[0].(string); ok {
                proposal.ProposalHash = propHash
            } else {
                return nil, fmt.Errorf("Malformed request ProposalHash was expected to be a string but got: %v", proposalResp[0])
            }
            if supporters, ok := proposalResp[1].(float64); ok {
                proposal.SupporterCount = int(supporters)
            } else {
                return nil, fmt.Errorf("Malformed request SupporterCount was expected to be a float but got: %v", proposalResp[1])
            }
            proposals[i] = proposal
        } else {
            return nil, fmt.Errorf("Malformed request Proposal is expected to be tuple of size 2")
        }
    }

    return proposals, nil
}

// GetCurrentProposals returns the current proposal under evaluation.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-current-proposal
func (s *Service) GetCurrentProposals(ctx context.Context, chainID, blockID string) (string, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/current_proposal", nil)
    if err != nil {
        return "", err
    }

    var currentProposal string
    if err := s.Client.Do(req, &currentProposal); err != nil {
        return "", err
    }

    return currentProposal, nil
}

// GetCurrentQuorum returns the current expected quorum.
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-current-quorum
func (s *Service) GetCurrentQuorum(ctx context.Context, chainID, blockID string) (int, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/current_quorum", nil)
    if err != nil {
        return -1, err
    }

    var currentQuorum int
    if err := s.Client.Do(req, &currentQuorum); err != nil {
        return -1, err
    }

    return currentQuorum, nil
}

// GetCurrentPeriodKind returns the current period kind
// https://tezos.gitlab.io/alphanet/api/rpc.html#get-block-id-votes-current-period-kind
func (s *Service) GetCurrentPeriodKind(ctx context.Context, chainID, blockID string) (PeriodKind, error) {
    req, err := s.Client.NewRequest(ctx, http.MethodGet, "/chains/"+chainID+"/blocks/"+blockID+"/votes/current_period_kind", nil)
    if err != nil {
        return "", err
    }

    var periodKind PeriodKind
    if err := s.Client.Do(req, &periodKind); err != nil {
        return "", err
    }

    return periodKind, nil
}