github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/OsmosisScraper.go (about)

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"errors"
     8  	"fmt"
     9  	"math"
    10  	"net"
    11  	"strconv"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/cosmos/cosmos-sdk/client"
    16  	"github.com/cosmos/cosmos-sdk/codec"
    17  	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
    18  	stdtypes "github.com/cosmos/cosmos-sdk/std"
    19  	sdk "github.com/cosmos/cosmos-sdk/types"
    20  	"github.com/cosmos/cosmos-sdk/x/auth/tx"
    21  	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
    22  	authztypes "github.com/cosmos/cosmos-sdk/x/authz"
    23  	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    24  	distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
    25  	govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
    26  	stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
    27  	ibctransfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types"
    28  	ibccoretypes "github.com/cosmos/ibc-go/v3/modules/core/types"
    29  	"github.com/diadata-org/diadata/pkg/dia"
    30  	models "github.com/diadata-org/diadata/pkg/model"
    31  	"github.com/diadata-org/diadata/pkg/utils"
    32  	"github.com/go-resty/resty/v2"
    33  	gammtypes "github.com/osmosis-labs/osmosis/v6/x/gamm/types"
    34  	lockuptypes "github.com/osmosis-labs/osmosis/v6/x/lockup/types"
    35  	liquiditytypes "github.com/tendermint/liquidity/x/liquidity/types"
    36  	tmjson "github.com/tendermint/tendermint/libs/json"
    37  	coretypes "github.com/tendermint/tendermint/rpc/core/types"
    38  	tendermint "github.com/tendermint/tendermint/rpc/jsonrpc/client"
    39  	rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
    40  	tmtypes "github.com/tendermint/tendermint/types"
    41  )
    42  
    43  const (
    44  	osmosisRefreshDelay = time.Second * 30 * 1
    45  )
    46  
    47  type OsmosisEncodingConfig struct {
    48  	InterfaceRegistry codectypes.InterfaceRegistry
    49  	Marshaler         codec.Codec
    50  	TxConfig          client.TxConfig
    51  	Amino             *codec.LegacyAmino
    52  }
    53  
    54  type OsmosisConfig struct {
    55  	Bech32AddrPrefix  string
    56  	Bech32ValPrefix   string
    57  	Bech32PkPrefix    string
    58  	Bech32PkValPrefix string
    59  	RpcURL            string
    60  	WsURL             string
    61  	Encoding          *OsmosisEncodingConfig
    62  }
    63  
    64  // Contains info about a transaction log event key/val attribute
    65  type Attribute struct {
    66  	Key   string `json:"key"`
    67  	Value string `json:"value"`
    68  }
    69  
    70  // Contains info about an attribute value keyed by attribute type
    71  type ValueByAttribute map[string]string
    72  
    73  // Contains info about transaction events keyed by message index
    74  type EventsByMsgIndex map[string]AttributesByEvent
    75  
    76  // Contains info about a transaction log event
    77  type Event struct {
    78  	Type       string      `json:"type"`
    79  	Attributes []Attribute `json:"attributes"`
    80  }
    81  
    82  // Contains info about event attributes keyed by event type
    83  type AttributesByEvent map[string]ValueByAttribute
    84  
    85  type OsmosisScraper struct {
    86  	// signaling channels
    87  	shutdown     chan nothing
    88  	shutdownDone chan nothing
    89  	// error handling; to read error or closed, first acquire read lock
    90  	// only cleanup method should hold write lock
    91  	errorLock            sync.RWMutex
    92  	error                error
    93  	closed               bool
    94  	pairScrapers         map[string]*OsmosisPairScraper // pc.ExchangePair -> pairScraperSet
    95  	wsClient             *tendermint.WSClient
    96  	rpcClient            *resty.Client
    97  	encoding             *OsmosisEncodingConfig
    98  	ticker               *time.Ticker
    99  	exchangeName         string
   100  	chanTrades           chan *dia.Trade
   101  	db                   *models.RelDB
   102  	blockTimestampsCache map[int64]*time.Time
   103  }
   104  
   105  // NewOsmosisScraper returns a new OsmosisScraper initialized with default values.
   106  // The instance is asynchronously scraping as soon as it is created.
   107  func NewOsmosisScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *OsmosisScraper {
   108  	encoding := NewOsmosisEncoding()
   109  
   110  	cfg := &OsmosisConfig{
   111  		Bech32AddrPrefix:  "osmo",
   112  		Bech32PkPrefix:    "osmopub",
   113  		Bech32ValPrefix:   "osmovaloper",
   114  		Bech32PkValPrefix: "osmovalpub",
   115  		Encoding:          encoding,
   116  		RpcURL:            utils.Getenv("OSMOSIS_RPC_URL", ""),
   117  		WsURL:             utils.Getenv("OSMOSIS_WS_URL", ""),
   118  	}
   119  	rpcClient, err := NewRpcClient(*cfg)
   120  	if err != nil {
   121  		panic("failed to create rpc client")
   122  	}
   123  	wsClient, err := NewWsClient(*cfg)
   124  	if err != nil {
   125  		panic("failed to create ws client")
   126  	}
   127  
   128  	s := &OsmosisScraper{
   129  		shutdown:             make(chan nothing),
   130  		shutdownDone:         make(chan nothing),
   131  		pairScrapers:         make(map[string]*OsmosisPairScraper),
   132  		blockTimestampsCache: make(map[int64]*time.Time),
   133  		wsClient:             wsClient,
   134  		rpcClient:            rpcClient,
   135  		ticker:               time.NewTicker(osmosisRefreshDelay),
   136  		exchangeName:         exchange.Name,
   137  		encoding:             encoding,
   138  		error:                nil,
   139  		chanTrades:           make(chan *dia.Trade),
   140  		db:                   relDB,
   141  	}
   142  	if scrape {
   143  		go s.mainLoop()
   144  	}
   145  	return s
   146  }
   147  
   148  // mainLoop runs in a goroutine until channel s is closed.
   149  func (s *OsmosisScraper) mainLoop() {
   150  	isWsRunning := s.wsClient.IsRunning()
   151  	if !isWsRunning {
   152  		s.Start()
   153  	}
   154  	for {
   155  		select {
   156  		case <-s.shutdown: // user requested shutdown
   157  			log.Printf("OsmosisScraper shutting down")
   158  			s.cleanup(nil)
   159  			return
   160  		}
   161  	}
   162  }
   163  
   164  // closes all connected PairScrapers
   165  // must only be called from mainLoop
   166  func (s *OsmosisScraper) cleanup(err error) {
   167  
   168  	s.errorLock.Lock()
   169  	defer s.errorLock.Unlock()
   170  	s.wsClient.Stop()
   171  
   172  	if err != nil {
   173  		s.error = err
   174  	}
   175  	s.closed = true
   176  
   177  	close(s.shutdownDone) // signal that shutdown is complete
   178  }
   179  
   180  // Close closes any existing API connections, as well as channels of
   181  // PairScrapers from calls to ScrapePair
   182  func (s *OsmosisScraper) Close() error {
   183  	if s.closed {
   184  		return errors.New("OsmosisScraper: Already closed")
   185  	}
   186  	close(s.shutdown)
   187  	<-s.shutdownDone
   188  	s.errorLock.RLock()
   189  	defer s.errorLock.RUnlock()
   190  	return s.error
   191  }
   192  
   193  // OsmosisPairScraper implements PairScraper for Osmosis
   194  type OsmosisPairScraper struct {
   195  	parent     *OsmosisScraper
   196  	pair       dia.ExchangePair
   197  	closed     bool
   198  	lastRecord int64
   199  }
   200  
   201  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   202  // this APIScraper
   203  func (s *OsmosisScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   204  
   205  	s.errorLock.RLock()
   206  	defer s.errorLock.RUnlock()
   207  	if s.error != nil {
   208  		return nil, s.error
   209  	}
   210  	if s.closed {
   211  		return nil, errors.New("OsmosisScraper: Call ScrapePair on closed scraper")
   212  	}
   213  	ps := &OsmosisPairScraper{
   214  		parent:     s,
   215  		pair:       pair,
   216  		lastRecord: 0, //TODO FIX to figure out the last we got...
   217  	}
   218  
   219  	s.pairScrapers[pair.Symbol] = ps
   220  
   221  	return ps, nil
   222  }
   223  
   224  func (s *OsmosisScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   225  	return dia.Asset{Symbol: symbol}, nil
   226  }
   227  
   228  // FetchAvailablePairs returns a list with all available trade pairs
   229  func (s *OsmosisScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   230  	return []dia.ExchangePair{}, errors.New("FetchAvailablePairs() not implemented")
   231  }
   232  
   233  // NormalizePair accounts for the par
   234  func (ps *OsmosisScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   235  	return pair, nil
   236  }
   237  
   238  // Channel returns a channel that can be used to receive trades/pricing information
   239  func (ps *OsmosisScraper) Channel() chan *dia.Trade {
   240  	return ps.chanTrades
   241  }
   242  
   243  func (ps *OsmosisPairScraper) Close() error {
   244  	ps.closed = true
   245  	return nil
   246  }
   247  
   248  // Error returns an error when the channel Channel() is closed
   249  // and nil otherwise
   250  func (ps *OsmosisPairScraper) Error() error {
   251  	s := ps.parent
   252  	s.errorLock.RLock()
   253  	defer s.errorLock.RUnlock()
   254  	return s.error
   255  }
   256  
   257  // Pair returns the pair this scraper is subscribed to
   258  func (ps *OsmosisPairScraper) Pair() dia.ExchangePair {
   259  	return ps.pair
   260  }
   261  
   262  func NewWsClient(conf OsmosisConfig) (*tendermint.WSClient, error) {
   263  	sdk.GetConfig().SetBech32PrefixForAccount(conf.Bech32AddrPrefix, conf.Bech32PkPrefix)
   264  	sdk.GetConfig().SetBech32PrefixForValidator(conf.Bech32ValPrefix, conf.Bech32PkValPrefix)
   265  
   266  	client, err := tendermint.NewWS(conf.WsURL, "/websocket")
   267  	if err != nil {
   268  		log.Fatal("failed to create websocket client: ", err)
   269  	}
   270  	client.Dialer = net.Dial
   271  	return client, nil
   272  }
   273  
   274  func NewRpcClient(conf OsmosisConfig) (*resty.Client, error) {
   275  	sdk.GetConfig().SetBech32PrefixForAccount(conf.Bech32AddrPrefix, conf.Bech32PkPrefix)
   276  	sdk.GetConfig().SetBech32PrefixForValidator(conf.Bech32ValPrefix, conf.Bech32PkValPrefix)
   277  
   278  	headers := map[string]string{"Accept": "application/json"}
   279  	client := resty.New().SetBaseURL(conf.RpcURL).SetHeaders(headers)
   280  	return client, nil
   281  }
   282  
   283  func NewOsmosisEncoding() *OsmosisEncodingConfig {
   284  	registry := codectypes.NewInterfaceRegistry()
   285  
   286  	ibctransfertypes.RegisterInterfaces(registry)
   287  	gammtypes.RegisterInterfaces(registry)
   288  	lockuptypes.RegisterInterfaces(registry)
   289  	authtypes.RegisterInterfaces(registry)
   290  	authztypes.RegisterInterfaces(registry)
   291  	banktypes.RegisterInterfaces(registry)
   292  	distributiontypes.RegisterInterfaces(registry)
   293  	govtypes.RegisterInterfaces(registry)
   294  	ibccoretypes.RegisterInterfaces(registry)
   295  	liquiditytypes.RegisterInterfaces(registry)
   296  	stakingtypes.RegisterInterfaces(registry)
   297  	stdtypes.RegisterInterfaces(registry)
   298  
   299  	marshaler := codec.NewProtoCodec(registry)
   300  
   301  	return &OsmosisEncodingConfig{
   302  		InterfaceRegistry: registry,
   303  		Marshaler:         marshaler,
   304  		TxConfig:          tx.NewTxConfig(marshaler, tx.DefaultSignModes),
   305  		Amino:             codec.NewLegacyAmino(),
   306  	}
   307  }
   308  
   309  func (s *OsmosisScraper) Start() error {
   310  	err := s.wsClient.Start()
   311  	if err != nil {
   312  		log.Warn("failed to start websocket client: ", err)
   313  		return err
   314  	}
   315  
   316  	err = s.wsClient.Subscribe(context.Background(), tmtypes.EventQueryTx.String())
   317  	if err != nil {
   318  		log.Warn(err, "failed to subscribe to txs")
   319  		return err
   320  	}
   321  
   322  	go s.listen()
   323  
   324  	return nil
   325  }
   326  
   327  func (s *OsmosisScraper) listen() {
   328  	for r := range s.wsClient.ResponsesCh {
   329  		if r.Error != nil {
   330  			// resubscribe if subscription is cancelled by the server for reason:
   331  			// client is not pulling messages fast enough
   332  			// experimental rpc config available to help mitigate this issue:
   333  			// https://github.com/tendermint/tendermint/blob/main/config/config.go#L373
   334  			if r.Error.Code == -32000 {
   335  				err := s.wsClient.UnsubscribeAll(context.Background())
   336  				if err != nil {
   337  					log.Fatal(err, "failed to unsubscribe from all subscriptions")
   338  				}
   339  
   340  				err = s.wsClient.Subscribe(context.Background(), tmtypes.EventQueryTx.String())
   341  				if err != nil {
   342  					log.Fatal(err, "failed to subscribe to txs")
   343  				}
   344  
   345  				continue
   346  			}
   347  
   348  			log.Error(r.Error.Error())
   349  			continue
   350  		}
   351  
   352  		result := &coretypes.ResultEvent{}
   353  		if err := tmjson.Unmarshal(r.Result, result); err != nil {
   354  			log.Errorf("failed to unmarshal tx message: %v", err)
   355  			continue
   356  		}
   357  
   358  		if result.Data != nil {
   359  			switch result.Data.(type) {
   360  			case tmtypes.EventDataTx:
   361  				go s.handleTx(result.Data.(tmtypes.EventDataTx))
   362  			default:
   363  				fmt.Printf("unsupported result type: %T", result.Data)
   364  			}
   365  		}
   366  	}
   367  
   368  	// if reconnect fails, ResponsesCh is closed
   369  	log.Fatal("websocket client connection closed by server")
   370  }
   371  
   372  func (s *OsmosisScraper) handleTx(tx tmtypes.EventDataTx) {
   373  	decodedTx, err := DecodeTx(*s.encoding, tx.Tx)
   374  	if err != nil {
   375  		// unsupported tx
   376  		return
   377  	}
   378  
   379  	txid := fmt.Sprintf("%X", sha256.Sum256(tx.Tx))
   380  	events := ParseEvents(tx.Result.Log)
   381  	messages := ParseMessages(decodedTx.GetMsgs(), events)
   382  	// messages var is empty for any types other than `*gammtypes.MsgSwapExactAmountIn`
   383  	if len(messages) > 0 {
   384  		quoteToken, err := s.db.GetAsset(messages[0].Token, "Osmosis")
   385  		if err != nil {
   386  			log.Error(err, ", failed to get asset: ", messages[0].Token)
   387  			return
   388  		}
   389  		baseToken, err := s.db.GetAsset(messages[1].Token, "Osmosis")
   390  		if err != nil {
   391  			log.Error(err, ", failed to get asset: ", messages[1].Token)
   392  			return
   393  		}
   394  		volumeOut, err := strconv.ParseFloat(messages[0].Value, 64)
   395  		if err != nil {
   396  			log.Error(err, ", failed to parse volume of: ", txid)
   397  			return
   398  		}
   399  		volumeIn, err := strconv.ParseFloat(messages[1].Value, 64)
   400  		if err != nil {
   401  			log.Error(err, ", failed to parse volume of: ", txid)
   402  			return
   403  		}
   404  
   405  		if volumeOut == 0 {
   406  			return
   407  		}
   408  		price := (volumeIn / math.Pow(10, float64(baseToken.Decimals))) / (volumeOut / math.Pow(10, float64(quoteToken.Decimals)))
   409  		timestamp := s.blockTimestampsCache[tx.Height]
   410  		if timestamp == nil {
   411  			// get timestamp from rpc
   412  			rpcTimestamp, err := s.GetBlock(int(tx.Height))
   413  			if err != nil {
   414  				log.Error(err, ", failed to get block timestampfor: ", txid)
   415  				return
   416  			}
   417  			s.blockTimestampsCache[tx.Height] = rpcTimestamp
   418  			timestamp = rpcTimestamp
   419  		}
   420  		t := &dia.Trade{
   421  			Symbol:         quoteToken.Symbol,
   422  			Pair:           quoteToken.Symbol + "-" + baseToken.Symbol,
   423  			Volume:         volumeOut / math.Pow(10, float64(quoteToken.Decimals)),
   424  			Price:          price,
   425  			Time:           *timestamp,
   426  			ForeignTradeID: txid,
   427  			Source:         dia.OsmosisExchange,
   428  			BaseToken:      baseToken,
   429  			QuoteToken:     quoteToken,
   430  			VerifiedPair:   true,
   431  		}
   432  		log.Info("New Trade: ", t)
   433  		s.chanTrades <- t
   434  	}
   435  }
   436  
   437  // DecodeTx will attempt to decode a raw transaction in the form of
   438  // a base64 encoded string or a protobuf encoded byte slice
   439  func DecodeTx(encoding OsmosisEncodingConfig, rawTx interface{}) (sdk.Tx, error) {
   440  	var txBytes []byte
   441  
   442  	switch rawTx := rawTx.(type) {
   443  	case string:
   444  		var err error
   445  
   446  		txBytes, err = base64.StdEncoding.DecodeString(rawTx)
   447  		if err != nil {
   448  			return nil, fmt.Errorf("error decoding transaction from base64: %s", err)
   449  		}
   450  	case []byte:
   451  		txBytes = rawTx
   452  	case tmtypes.Tx:
   453  		txBytes = rawTx
   454  	default:
   455  		return nil, fmt.Errorf("rawTx must be string or []byte")
   456  	}
   457  
   458  	tx, err := encoding.TxConfig.TxDecoder()(txBytes)
   459  	if err != nil {
   460  		return nil, fmt.Errorf("error decoding transaction from protobuf: %s", err)
   461  	}
   462  
   463  	return tx, nil
   464  }
   465  
   466  func ParseEvents(log string) EventsByMsgIndex {
   467  	events := make(EventsByMsgIndex)
   468  
   469  	logs, err := sdk.ParseABCILogs(log)
   470  	if err != nil {
   471  		// transaction error logs are not in json format and will fail to parse
   472  		// return error event with the log message
   473  		events["0"] = AttributesByEvent{"error": ValueByAttribute{"message": log}}
   474  		return events
   475  	}
   476  
   477  	for _, l := range logs {
   478  		msgIndex := strconv.Itoa(int(l.GetMsgIndex()))
   479  		events[msgIndex] = make(AttributesByEvent)
   480  
   481  		for _, e := range l.GetEvents() {
   482  			attributes := make(ValueByAttribute)
   483  			for _, a := range e.Attributes {
   484  				attributes[a.Key] = a.Value
   485  			}
   486  
   487  			events[msgIndex][e.Type] = attributes
   488  		}
   489  	}
   490  
   491  	return events
   492  }
   493  
   494  // Contains info about a transaction message
   495  type Message struct {
   496  	Value string
   497  	Token string
   498  }
   499  
   500  // ParseMessages will parse any osmosis or cosmos-sdk message types
   501  func ParseMessages(msgs []sdk.Msg, events EventsByMsgIndex) []Message {
   502  	messages := []Message{}
   503  
   504  	if _, ok := events["0"]["error"]; ok {
   505  		return messages
   506  	}
   507  
   508  	for i, msg := range msgs {
   509  		switch v := msg.(type) {
   510  		case *gammtypes.MsgSwapExactAmountIn:
   511  			swappedTokensOut := events[strconv.Itoa(i)]["token_swapped"]["tokens_out"]
   512  
   513  			tokenOut, err := sdk.ParseCoinNormalized(swappedTokensOut)
   514  			if err != nil && swappedTokensOut != "" {
   515  				log.Error(err)
   516  			}
   517  
   518  			msgs := []Message{
   519  				// token in (sell)
   520  				{
   521  					Token: (&v.TokenIn).Denom,
   522  					Value: (&v.TokenIn).Amount.String(),
   523  				},
   524  				// token out (buy)
   525  				{
   526  					Token: (&tokenOut).Denom,
   527  					Value: (&tokenOut).Amount.String(),
   528  				},
   529  			}
   530  			messages = append(messages, msgs...)
   531  		default:
   532  		}
   533  	}
   534  
   535  	return messages
   536  }
   537  
   538  func (s *OsmosisScraper) GetBlock(height int) (*time.Time, error) {
   539  	res := &rpctypes.RPCResponse{}
   540  	_, err := s.rpcClient.R().SetResult(res).SetError(res).SetQueryParam("height", strconv.Itoa(height)).Get("/block")
   541  	if err != nil {
   542  		return nil, err
   543  	}
   544  	result := &coretypes.ResultBlock{}
   545  	if err := tmjson.Unmarshal(res.Result, result); err != nil {
   546  		return nil, fmt.Errorf("failed to unmarshal block result: %v: %s", res.Result, res.Error.Error())
   547  	}
   548  
   549  	return &result.Block.Time, nil
   550  }