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 }