github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/VelarScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/diadata-org/diadata/pkg/dia" 12 stacks "github.com/diadata-org/diadata/pkg/dia/helpers/stackshelper" 13 velar "github.com/diadata-org/diadata/pkg/dia/helpers/velarhelper" 14 models "github.com/diadata-org/diadata/pkg/model" 15 "github.com/diadata-org/diadata/pkg/utils" 16 "github.com/sirupsen/logrus" 17 ) 18 19 type VelarScraper struct { 20 logger *logrus.Entry 21 pairScrapers map[string]*VelarPairScraper // pc.ExchangePair -> pairScraperSet 22 shutdown chan nothing 23 shutdownDone chan nothing 24 errorLock sync.RWMutex 25 error error 26 closed bool 27 ticker *time.Ticker 28 exchangeName string 29 blockchain string 30 chanTrades chan *dia.Trade 31 api *stacks.StacksClient 32 db *models.RelDB 33 currentHeight int 34 initialBlockHeight int 35 } 36 37 // NewVelarScraper returns a new VelarScraper initialized with default values. 38 // The instance is asynchronously scraping as soon as it is created. 39 // ENV values: 40 // 41 // VELAR_SLEEP_TIMEOUT - (optional, millisecond), make timeout between API calls, default "stackshelper.DefaultSleepBetweenCalls" value 42 // VELAR_REFRESH_DELAY - (optional, millisecond) refresh data after each poll, default "stackshelper.DefaultRefreshDelay" value 43 // VELAR_HIRO_API_KEY - (optional, string), Hiro Stacks API key, improves scraping performance, default = "" 44 // VELAR_INITIAL_BLOCK_HEIGHT (optional, int), useful for debug, default = 0 45 // VELAR_DEBUG - (optional, bool), make stdout output with alephium client http call, default = false 46 func NewVelarScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *VelarScraper { 47 envPrefix := strings.ToUpper(exchange.Name) 48 49 sleepBetweenCalls := utils.GetTimeDurationFromIntAsMilliseconds( 50 utils.GetenvInt( 51 envPrefix+"_SLEEP_TIMEOUT", 52 stacks.DefaultSleepBetweenCalls, 53 ), 54 ) 55 refreshDelay := utils.GetTimeDurationFromIntAsMilliseconds( 56 utils.GetenvInt(envPrefix+"_REFRESH_DELAY", stacks.DefaultRefreshDelay), 57 ) 58 hiroAPIKey := utils.Getenv(envPrefix+"_HIRO_API_KEY", "") 59 initialBlockHeight := utils.GetenvInt(envPrefix+"_INITIAL_BLOCK_HEIGHT", 0) 60 isDebug := utils.GetenvBool(envPrefix+"_DEBUG", false) 61 62 stacksClient := stacks.NewStacksClient( 63 log.WithContext(context.Background()).WithField("context", "StacksClient"), 64 sleepBetweenCalls, 65 hiroAPIKey, 66 isDebug, 67 ) 68 69 s := &VelarScraper{ 70 shutdown: make(chan nothing), 71 shutdownDone: make(chan nothing), 72 pairScrapers: make(map[string]*VelarPairScraper), 73 ticker: time.NewTicker(refreshDelay), 74 chanTrades: make(chan *dia.Trade), 75 api: stacksClient, 76 db: relDB, 77 exchangeName: exchange.Name, 78 blockchain: exchange.BlockChain.Name, 79 initialBlockHeight: initialBlockHeight, 80 } 81 82 s.logger = logrus. 83 New(). 84 WithContext(context.Background()). 85 WithField("context", "VelarDEXScraper") 86 87 if scrape { 88 go s.mainLoop() 89 } 90 return s 91 } 92 93 func (s *VelarScraper) mainLoop() { 94 if s.initialBlockHeight <= 0 { 95 latestBlock, err := s.api.GetLatestBlock() 96 if err != nil { 97 s.logger.WithError(err).Error("failed to GetLatestBlock") 98 s.cleanup(err) 99 return 100 } 101 s.currentHeight = latestBlock.Height 102 } else { 103 s.currentHeight = s.initialBlockHeight 104 } 105 106 for { 107 select { 108 case <-s.ticker.C: 109 err := s.Update() 110 if err != nil { 111 s.logger.WithError(err).Error("failed to run Update") 112 } 113 case <-s.shutdown: 114 s.logger.Println("shutting down") 115 s.cleanup(nil) 116 return 117 } 118 } 119 } 120 121 func (s *VelarScraper) Update() error { 122 txs, err := s.api.GetAllBlockTransactions(s.currentHeight) 123 if err != nil { 124 return err 125 } 126 127 if len(txs) == 0 { 128 return nil 129 } 130 s.currentHeight += 1 131 132 swapEvents, err := s.getSwapEvents(txs) 133 if err != nil { 134 return err 135 } 136 if len(swapEvents) == 0 { 137 return nil 138 } 139 140 pools, err := s.getPools() 141 if err != nil { 142 s.logger.WithError(err).Error("failed to GetAllPoolsExchange") 143 return err 144 } 145 146 for _, pool := range pools { 147 if len(pool.Assetvolumes) != 2 { 148 s.logger.WithField("poolAddress", pool.Address).Error("pool is missing required asset volumes") 149 continue 150 } 151 152 for _, e := range swapEvents { 153 if pool.Address != e.TickerID { 154 continue 155 } 156 157 diaTrade := s.handleTrade(&pool, &e) 158 s.logger. 159 WithField("parentAddress", pool.Address). 160 WithField("height", s.currentHeight-1). 161 WithField("diaTrade", diaTrade). 162 Info("trade") 163 s.chanTrades <- diaTrade 164 } 165 } 166 167 return nil 168 } 169 170 func (s *VelarScraper) getPools() ([]dia.Pool, error) { 171 return s.db.GetAllPoolsExchange(s.exchangeName, 0) 172 } 173 174 func (s *VelarScraper) getSwapEvents(txs []stacks.Transaction) ([]velar.SwapEvent, error) { 175 result := make([]velar.SwapEvent, 0) 176 177 for _, tx := range txs { 178 if tx.TxStatus != "success" || tx.TxType != "contract_call" || !s.isSwapTransaction(&tx) { 179 continue 180 } 181 182 // This is a temporary workaround introduced due to a bug in hiro stacks API. 183 // Results returned from /blocks/{block_height}/transactions route have empty 184 // `name` field in `contract_call.function_args` list. 185 // TODO: remove this as soon as the issue is fixed. 186 normalizedTx, err := s.api.GetTransactionAt(tx.TxID) 187 if err != nil { 188 return nil, err 189 } 190 191 events, err := velar.DecodeSwapEvents(normalizedTx) 192 if err != nil { 193 return nil, err 194 } 195 result = append(result, events...) 196 } 197 198 return result, nil 199 } 200 201 func (s *VelarScraper) isSwapTransaction(tx *stacks.Transaction) bool { 202 return strings.HasPrefix(tx.ContractCall.FunctionName, "swap") || 203 tx.ContractCall.FunctionName == "apply" || 204 tx.ContractCall.FunctionName == "r4" 205 } 206 207 func (s *VelarScraper) handleTrade(pool *dia.Pool, event *velar.SwapEvent) *dia.Trade { 208 var volume, price float64 209 210 token0 := pool.Assetvolumes[0].Asset 211 token1 := pool.Assetvolumes[1].Asset 212 213 amountIn := event.AmountIn.String() 214 amountOut := event.AmountOut.String() 215 216 var trade dia.Trade 217 218 if event.TokenIn == token0.Address { 219 trade.Pair = fmt.Sprintf("%s-%s", token1.Symbol, token0.Symbol) 220 trade.Symbol = token1.Symbol 221 trade.BaseToken = token0 222 trade.QuoteToken = token1 223 224 amount0In, _ := utils.StringToFloat64(amountIn, int64(token0.Decimals)) 225 amount1Out, _ := utils.StringToFloat64(amountOut, int64(token1.Decimals)) 226 volume = amount1Out 227 price = amount0In / amount1Out 228 } else { 229 trade.Pair = fmt.Sprintf("%s-%s", token0.Symbol, token1.Symbol) 230 trade.Symbol = token0.Symbol 231 trade.BaseToken = token1 232 trade.QuoteToken = token0 233 234 amount1In, _ := utils.StringToFloat64(amountIn, int64(token1.Decimals)) 235 amount0Out, _ := utils.StringToFloat64(amountOut, int64(token0.Decimals)) 236 volume = amount0Out 237 price = amount1In / amount0Out 238 } 239 240 trade.Time = time.Unix(int64(event.Timestamp), 0) 241 trade.ForeignTradeID = event.TxID 242 trade.Source = s.exchangeName 243 trade.Price = price 244 trade.Volume = volume 245 trade.VerifiedPair = true 246 247 trade.PoolAddress = pool.Address 248 return &trade 249 } 250 251 func (s *VelarScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) { 252 return []dia.ExchangePair{}, nil 253 } 254 255 func (s *VelarScraper) FillSymbolData(symbol string) (dia.Asset, error) { 256 return dia.Asset{Symbol: symbol}, nil 257 } 258 259 func (s *VelarScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 260 return pair, nil 261 } 262 263 func (s *VelarScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 264 s.errorLock.RLock() 265 defer s.errorLock.RUnlock() 266 if s.error != nil { 267 return nil, s.error 268 } 269 if s.closed { 270 return nil, errors.New("VelarScraper: Call ScrapePair on closed scraper") 271 } 272 ps := &VelarPairScraper{ 273 parent: s, 274 pair: pair, 275 lastRecord: 0, 276 } 277 278 s.pairScrapers[pair.Symbol] = ps 279 280 return ps, nil 281 } 282 283 func (s *VelarScraper) cleanup(err error) { 284 s.errorLock.Lock() 285 defer s.errorLock.Unlock() 286 287 s.ticker.Stop() 288 289 if err != nil { 290 s.error = err 291 } 292 s.closed = true 293 close(s.shutdownDone) 294 } 295 296 // Close gracefully shuts down the VelarScraper. 297 func (s *VelarScraper) Close() error { 298 if s.closed { 299 return errors.New("VelarScraper: Already closed") 300 } 301 close(s.shutdown) 302 <-s.shutdownDone 303 s.errorLock.RLock() 304 defer s.errorLock.RUnlock() 305 return s.error 306 } 307 308 // Channel returns the channel used to receive trades/pricing information. 309 func (s *VelarScraper) Channel() chan *dia.Trade { 310 return s.chanTrades 311 } 312 313 type VelarPairScraper struct { 314 parent *VelarScraper 315 pair dia.ExchangePair 316 closed bool 317 lastRecord int64 318 } 319 320 func (ps *VelarPairScraper) Pair() dia.ExchangePair { 321 return ps.pair 322 } 323 324 func (ps *VelarPairScraper) Close() error { 325 ps.closed = true 326 return nil 327 } 328 329 // Error returns an error when the channel Channel() is closed 330 // and nil otherwise 331 func (ps *VelarPairScraper) Error() error { 332 s := ps.parent 333 s.errorLock.RLock() 334 defer s.errorLock.RUnlock() 335 return s.error 336 }