package http
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/tendermint/tendermint/lite2/provider"
|
|
rpcclient "github.com/tendermint/tendermint/rpc/client"
|
|
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
|
|
"github.com/tendermint/tendermint/types"
|
|
)
|
|
|
|
// SignStatusClient combines a SignClient and StatusClient.
|
|
type SignStatusClient interface {
|
|
rpcclient.SignClient
|
|
rpcclient.StatusClient
|
|
// Remote returns the remote network address in a string form.
|
|
Remote() string
|
|
}
|
|
|
|
// http provider uses an RPC client (or SignStatusClient more generally) to
|
|
// obtain the necessary information.
|
|
type http struct {
|
|
SignStatusClient // embed so interface can be converted to SignStatusClient for tests
|
|
chainID string
|
|
}
|
|
|
|
// New creates a HTTP provider, which is using the rpchttp.HTTP client under the
|
|
// hood. If no scheme is provided in the remote URL, http will be used by default.
|
|
func New(chainID, remote string) (provider.Provider, error) {
|
|
// ensure URL scheme is set (default HTTP) when not provided
|
|
if !strings.Contains(remote, "://") {
|
|
remote = "http://" + remote
|
|
}
|
|
|
|
httpClient, err := rpchttp.New(remote, "/websocket")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewWithClient(chainID, httpClient), nil
|
|
}
|
|
|
|
// NewWithClient allows you to provide custom SignStatusClient.
|
|
func NewWithClient(chainID string, client SignStatusClient) provider.Provider {
|
|
return &http{
|
|
SignStatusClient: client,
|
|
chainID: chainID,
|
|
}
|
|
}
|
|
|
|
// ChainID returns a chainID this provider was configured with.
|
|
func (p *http) ChainID() string {
|
|
return p.chainID
|
|
}
|
|
|
|
func (p *http) String() string {
|
|
return fmt.Sprintf("http{%s}", p.Remote())
|
|
}
|
|
|
|
// SignedHeader fetches a SignedHeader at the given height and checks the
|
|
// chainID matches.
|
|
func (p *http) SignedHeader(height int64) (*types.SignedHeader, error) {
|
|
h, err := validateHeight(height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commit, err := p.SignStatusClient.Commit(h)
|
|
if err != nil {
|
|
// TODO: standartise errors on the RPC side
|
|
if strings.Contains(err.Error(), "height must be less than or equal") {
|
|
return nil, provider.ErrSignedHeaderNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if commit.Header == nil {
|
|
return nil, errors.New("header is nil")
|
|
}
|
|
|
|
// Verify we're still on the same chain.
|
|
if p.chainID != commit.Header.ChainID {
|
|
return nil, fmt.Errorf("expected chainID %s, got %s", p.chainID, commit.Header.ChainID)
|
|
}
|
|
|
|
return &commit.SignedHeader, nil
|
|
}
|
|
|
|
// ValidatorSet fetches a ValidatorSet at the given height. Multiple HTTP
|
|
// requests might be required if the validator set size is over 100.
|
|
func (p *http) ValidatorSet(height int64) (*types.ValidatorSet, error) {
|
|
h, err := validateHeight(height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
const maxPerPage = 100
|
|
res, err := p.SignStatusClient.Validators(h, 0, maxPerPage)
|
|
if err != nil {
|
|
// TODO: standartise errors on the RPC side
|
|
if strings.Contains(err.Error(), "height must be less than or equal") {
|
|
return nil, provider.ErrValidatorSetNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
vals = res.Validators
|
|
page = 1
|
|
)
|
|
|
|
// Check if there are more validators.
|
|
for len(res.Validators) == maxPerPage {
|
|
res, err = p.SignStatusClient.Validators(h, page, maxPerPage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(res.Validators) > 0 {
|
|
vals = append(vals, res.Validators...)
|
|
}
|
|
page++
|
|
}
|
|
|
|
return types.NewValidatorSet(vals), nil
|
|
}
|
|
|
|
func validateHeight(height int64) (*int64, error) {
|
|
if height < 0 {
|
|
return nil, fmt.Errorf("expected height >= 0, got height %d", height)
|
|
}
|
|
|
|
h := &height
|
|
if height == 0 {
|
|
h = nil
|
|
}
|
|
return h, nil
|
|
}
|