github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/rpc/client/local/local.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/badrootd/nibiru-cometbft/libs/bytes"
     9  	"github.com/badrootd/nibiru-cometbft/libs/log"
    10  	cmtpubsub "github.com/badrootd/nibiru-cometbft/libs/pubsub"
    11  	cmtquery "github.com/badrootd/nibiru-cometbft/libs/pubsub/query"
    12  	nm "github.com/badrootd/nibiru-cometbft/node"
    13  	rpcclient "github.com/badrootd/nibiru-cometbft/rpc/client"
    14  	"github.com/badrootd/nibiru-cometbft/rpc/core"
    15  	ctypes "github.com/badrootd/nibiru-cometbft/rpc/core/types"
    16  	rpctypes "github.com/badrootd/nibiru-cometbft/rpc/jsonrpc/types"
    17  	"github.com/badrootd/nibiru-cometbft/types"
    18  )
    19  
    20  /*
    21  Local is a Client implementation that directly executes the rpc
    22  functions on a given node, without going through HTTP or GRPC.
    23  
    24  This implementation is useful for:
    25  
    26  * Running tests against a node in-process without the overhead
    27  of going through an http server
    28  * Communication between an ABCI app and CometBFT when they
    29  are compiled in process.
    30  
    31  For real clients, you probably want to use client.HTTP.  For more
    32  powerful control during testing, you probably want the "client/mock" package.
    33  
    34  You can subscribe for any event published by CometBFT using Subscribe method.
    35  Note delivery is best-effort. If you don't read events fast enough, CometBFT
    36  might cancel the subscription. The client will attempt to resubscribe (you
    37  don't need to do anything). It will keep trying indefinitely with exponential
    38  backoff (10ms -> 20ms -> 40ms) until successful.
    39  */
    40  type Local struct {
    41  	*types.EventBus
    42  	Logger log.Logger
    43  	ctx    *rpctypes.Context
    44  }
    45  
    46  // NewLocal configures a client that calls the Node directly.
    47  //
    48  // Note that given how rpc/core works with package singletons, that
    49  // you can only have one node per process.  So make sure test cases
    50  // don't run in parallel, or try to simulate an entire network in
    51  // one process...
    52  func New(node *nm.Node) *Local {
    53  	if err := node.ConfigureRPC(); err != nil {
    54  		node.Logger.Error("Error configuring RPC", "err", err)
    55  	}
    56  	return &Local{
    57  		EventBus: node.EventBus(),
    58  		Logger:   log.NewNopLogger(),
    59  		ctx:      &rpctypes.Context{},
    60  	}
    61  }
    62  
    63  var _ rpcclient.Client = (*Local)(nil)
    64  
    65  // SetLogger allows to set a logger on the client.
    66  func (c *Local) SetLogger(l log.Logger) {
    67  	c.Logger = l
    68  }
    69  
    70  func (c *Local) Status(ctx context.Context) (*ctypes.ResultStatus, error) {
    71  	return core.Status(c.ctx)
    72  }
    73  
    74  func (c *Local) ABCIInfo(ctx context.Context) (*ctypes.ResultABCIInfo, error) {
    75  	return core.ABCIInfo(c.ctx)
    76  }
    77  
    78  func (c *Local) ABCIQuery(ctx context.Context, path string, data bytes.HexBytes) (*ctypes.ResultABCIQuery, error) {
    79  	return c.ABCIQueryWithOptions(ctx, path, data, rpcclient.DefaultABCIQueryOptions)
    80  }
    81  
    82  func (c *Local) ABCIQueryWithOptions(
    83  	ctx context.Context,
    84  	path string,
    85  	data bytes.HexBytes,
    86  	opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) {
    87  	return core.ABCIQuery(c.ctx, path, data, opts.Height, opts.Prove)
    88  }
    89  
    90  func (c *Local) BroadcastTxCommit(ctx context.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
    91  	return core.BroadcastTxCommit(c.ctx, tx)
    92  }
    93  
    94  func (c *Local) BroadcastTxAsync(ctx context.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
    95  	return core.BroadcastTxAsync(c.ctx, tx)
    96  }
    97  
    98  func (c *Local) BroadcastTxSync(ctx context.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
    99  	return core.BroadcastTxSync(c.ctx, tx)
   100  }
   101  
   102  func (c *Local) UnconfirmedTxs(ctx context.Context, limit *int) (*ctypes.ResultUnconfirmedTxs, error) {
   103  	return core.UnconfirmedTxs(c.ctx, limit)
   104  }
   105  
   106  func (c *Local) NumUnconfirmedTxs(ctx context.Context) (*ctypes.ResultUnconfirmedTxs, error) {
   107  	return core.NumUnconfirmedTxs(c.ctx)
   108  }
   109  
   110  func (c *Local) CheckTx(ctx context.Context, tx types.Tx) (*ctypes.ResultCheckTx, error) {
   111  	return core.CheckTx(c.ctx, tx)
   112  }
   113  
   114  func (c *Local) NetInfo(ctx context.Context) (*ctypes.ResultNetInfo, error) {
   115  	return core.NetInfo(c.ctx)
   116  }
   117  
   118  func (c *Local) DumpConsensusState(ctx context.Context) (*ctypes.ResultDumpConsensusState, error) {
   119  	return core.DumpConsensusState(c.ctx)
   120  }
   121  
   122  func (c *Local) ConsensusState(ctx context.Context) (*ctypes.ResultConsensusState, error) {
   123  	return core.ConsensusState(c.ctx)
   124  }
   125  
   126  func (c *Local) ConsensusParams(ctx context.Context, height *int64) (*ctypes.ResultConsensusParams, error) {
   127  	return core.ConsensusParams(c.ctx, height)
   128  }
   129  
   130  func (c *Local) Health(ctx context.Context) (*ctypes.ResultHealth, error) {
   131  	return core.Health(c.ctx)
   132  }
   133  
   134  func (c *Local) DialSeeds(ctx context.Context, seeds []string) (*ctypes.ResultDialSeeds, error) {
   135  	return core.UnsafeDialSeeds(c.ctx, seeds)
   136  }
   137  
   138  func (c *Local) DialPeers(
   139  	ctx context.Context,
   140  	peers []string,
   141  	persistent,
   142  	unconditional,
   143  	private bool,
   144  ) (*ctypes.ResultDialPeers, error) {
   145  	return core.UnsafeDialPeers(c.ctx, peers, persistent, unconditional, private)
   146  }
   147  
   148  func (c *Local) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) {
   149  	return core.BlockchainInfo(c.ctx, minHeight, maxHeight)
   150  }
   151  
   152  func (c *Local) Genesis(ctx context.Context) (*ctypes.ResultGenesis, error) {
   153  	return core.Genesis(c.ctx)
   154  }
   155  
   156  func (c *Local) GenesisChunked(ctx context.Context, id uint) (*ctypes.ResultGenesisChunk, error) {
   157  	return core.GenesisChunked(c.ctx, id)
   158  }
   159  
   160  func (c *Local) Block(ctx context.Context, height *int64) (*ctypes.ResultBlock, error) {
   161  	return core.Block(c.ctx, height)
   162  }
   163  
   164  func (c *Local) BlockByHash(ctx context.Context, hash []byte) (*ctypes.ResultBlock, error) {
   165  	return core.BlockByHash(c.ctx, hash)
   166  }
   167  
   168  func (c *Local) BlockResults(ctx context.Context, height *int64) (*ctypes.ResultBlockResults, error) {
   169  	return core.BlockResults(c.ctx, height)
   170  }
   171  
   172  func (c *Local) Header(ctx context.Context, height *int64) (*ctypes.ResultHeader, error) {
   173  	return core.Header(c.ctx, height)
   174  }
   175  
   176  func (c *Local) HeaderByHash(ctx context.Context, hash bytes.HexBytes) (*ctypes.ResultHeader, error) {
   177  	return core.HeaderByHash(c.ctx, hash)
   178  }
   179  
   180  func (c *Local) Commit(ctx context.Context, height *int64) (*ctypes.ResultCommit, error) {
   181  	return core.Commit(c.ctx, height)
   182  }
   183  
   184  func (c *Local) Validators(ctx context.Context, height *int64, page, perPage *int) (*ctypes.ResultValidators, error) {
   185  	return core.Validators(c.ctx, height, page, perPage)
   186  }
   187  
   188  func (c *Local) Tx(ctx context.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) {
   189  	return core.Tx(c.ctx, hash, prove)
   190  }
   191  
   192  func (c *Local) TxSearch(
   193  	_ context.Context,
   194  	query string,
   195  	prove bool,
   196  	page,
   197  	perPage *int,
   198  	orderBy string,
   199  ) (*ctypes.ResultTxSearch, error) {
   200  	return core.TxSearch(c.ctx, query, prove, page, perPage, orderBy)
   201  }
   202  
   203  func (c *Local) BlockSearch(
   204  	_ context.Context,
   205  	query string,
   206  	page, perPage *int,
   207  	orderBy string,
   208  ) (*ctypes.ResultBlockSearch, error) {
   209  	return core.BlockSearch(c.ctx, query, page, perPage, orderBy)
   210  }
   211  
   212  func (c *Local) BroadcastEvidence(ctx context.Context, ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) {
   213  	return core.BroadcastEvidence(c.ctx, ev)
   214  }
   215  
   216  func (c *Local) Subscribe(
   217  	ctx context.Context,
   218  	subscriber,
   219  	query string,
   220  	outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) {
   221  	q, err := cmtquery.New(query)
   222  	if err != nil {
   223  		return nil, fmt.Errorf("failed to parse query: %w", err)
   224  	}
   225  
   226  	outCap := 1
   227  	if len(outCapacity) > 0 {
   228  		outCap = outCapacity[0]
   229  	}
   230  
   231  	var sub types.Subscription
   232  	if outCap > 0 {
   233  		sub, err = c.EventBus.Subscribe(ctx, subscriber, q, outCap)
   234  	} else {
   235  		sub, err = c.EventBus.SubscribeUnbuffered(ctx, subscriber, q)
   236  	}
   237  	if err != nil {
   238  		return nil, fmt.Errorf("failed to subscribe: %w", err)
   239  	}
   240  
   241  	outc := make(chan ctypes.ResultEvent, outCap)
   242  	go c.eventsRoutine(sub, subscriber, q, outc)
   243  
   244  	return outc, nil
   245  }
   246  
   247  func (c *Local) eventsRoutine(
   248  	sub types.Subscription,
   249  	subscriber string,
   250  	q cmtpubsub.Query,
   251  	outc chan<- ctypes.ResultEvent) {
   252  	for {
   253  		select {
   254  		case msg := <-sub.Out():
   255  			result := ctypes.ResultEvent{Query: q.String(), Data: msg.Data(), Events: msg.Events()}
   256  			if cap(outc) == 0 {
   257  				outc <- result
   258  			} else {
   259  				select {
   260  				case outc <- result:
   261  				default:
   262  					c.Logger.Error("wanted to publish ResultEvent, but out channel is full", "result", result, "query", result.Query)
   263  				}
   264  			}
   265  		case <-sub.Cancelled():
   266  			if sub.Err() == cmtpubsub.ErrUnsubscribed {
   267  				return
   268  			}
   269  
   270  			c.Logger.Error("subscription was canceled, resubscribing...", "err", sub.Err(), "query", q.String())
   271  			sub = c.resubscribe(subscriber, q)
   272  			if sub == nil { // client was stopped
   273  				return
   274  			}
   275  		case <-c.Quit():
   276  			return
   277  		}
   278  	}
   279  }
   280  
   281  // Try to resubscribe with exponential backoff.
   282  func (c *Local) resubscribe(subscriber string, q cmtpubsub.Query) types.Subscription {
   283  	attempts := 0
   284  	for {
   285  		if !c.IsRunning() {
   286  			return nil
   287  		}
   288  
   289  		sub, err := c.EventBus.Subscribe(context.Background(), subscriber, q)
   290  		if err == nil {
   291  			return sub
   292  		}
   293  
   294  		attempts++
   295  		time.Sleep((10 << uint(attempts)) * time.Millisecond) // 10ms -> 20ms -> 40ms
   296  	}
   297  }
   298  
   299  func (c *Local) Unsubscribe(ctx context.Context, subscriber, query string) error {
   300  	q, err := cmtquery.New(query)
   301  	if err != nil {
   302  		return fmt.Errorf("failed to parse query: %w", err)
   303  	}
   304  	return c.EventBus.Unsubscribe(ctx, subscriber, q)
   305  }
   306  
   307  func (c *Local) UnsubscribeAll(ctx context.Context, subscriber string) error {
   308  	return c.EventBus.UnsubscribeAll(ctx, subscriber)
   309  }