github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/ZenlinkScraper.go (about) 1 package scrapers 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os/exec" 9 "strconv" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/diadata-org/diadata/pkg/dia" 15 "github.com/diadata-org/diadata/pkg/utils" 16 ws "github.com/gorilla/websocket" 17 ) 18 19 var ( 20 NodeScriptPathBifrostKusama = utils.Getenv("PATH_TO_NODE_SCRIPT", "scripts/bifrost/main.js") 21 NodeScriptPathBifrostPolkadot = utils.Getenv("PATH_TO_NODE_SCRIPT", "scripts/bifrost/zenlink-bifrost-polkadot.js") 22 ) 23 24 type ZenlinkPairResponse struct { 25 Code int `json:"code"` 26 Data []ZenlinkPair `json:"data"` 27 } 28 29 type ZenlinkPair struct { 30 Symbol string `json:"symbol"` 31 DisplayName string `json:"displayName"` 32 BaseAsset string `json:"baseAsset"` 33 QuoteAsset string `json:"quoteAsset"` 34 Status string `json:"status"` 35 MinNotional string `json:"minNotional"` 36 MaxNotional string `json:"maxNotional"` 37 MarginTradable bool `json:"marginTradable"` 38 CommissionType string `json:"commissionType"` 39 CommissionReserveRate string `json:"commissionReserveRate"` 40 TickSize string `json:"tickSize"` 41 LotSize string `json:"lotSize"` 42 } 43 44 type ZenlinkScraper struct { 45 exchange dia.Exchange 46 47 // channels to signal events 48 initDone chan nothing 49 shutdown chan nothing 50 shutdownDone chan nothing 51 52 errorLock sync.RWMutex 53 error error 54 closed bool 55 56 pairScrapers map[string]*ZenlinkPairScraper 57 // productPairIds map[string]int 58 chanTrades chan *dia.Trade 59 wsClient *ws.Conn 60 } 61 62 func NewZenlinkScraper(exchange dia.Exchange, scrape bool) *ZenlinkScraper { 63 var ( 64 socketURL string 65 nodeScriptPath string 66 ) 67 68 switch exchange.Name { 69 case dia.ZenlinkswapExchange: 70 socketURL = "wss://bifrost-rpc.liebi.com/ws" 71 nodeScriptPath = NodeScriptPathBifrostKusama 72 case dia.ZenlinkswapExchangeBifrostPolkadot: 73 socketURL = "wss://hk.p.bifrost-rpc.liebi.com/ws" 74 nodeScriptPath = NodeScriptPathBifrostPolkadot 75 } 76 // establish connection in the background 77 var wsDialer ws.Dialer 78 wsClient, _, err := wsDialer.Dial(socketURL, nil) 79 if err != nil { 80 log.Fatal("connect to websocket server: ", err) 81 } 82 83 scraper := &ZenlinkScraper{ 84 exchange: exchange, 85 wsClient: wsClient, 86 initDone: make(chan nothing), 87 shutdown: make(chan nothing), 88 shutdownDone: make(chan nothing), 89 // productPairIds: make(map[string]int), 90 pairScrapers: make(map[string]*ZenlinkPairScraper), 91 chanTrades: make(chan *dia.Trade), 92 } 93 94 if scrape { 95 go scraper.mainLoop(nodeScriptPath) 96 } 97 return scraper 98 } 99 100 func (s *ZenlinkScraper) receive(nodeScriptPath string) { 101 trades := make(chan string) 102 103 go func() { 104 cmd := exec.Command("node", nodeScriptPath) 105 stdout, _ := cmd.StdoutPipe() 106 stderr, _ := cmd.StderrPipe() 107 108 err := cmd.Start() 109 if err != nil { 110 log.Error("start main.js: ", err) 111 } else { 112 log.Info("started main.js") 113 } 114 scanner := bufio.NewScanner(stdout) 115 for scanner.Scan() { 116 if strings.HasPrefix(scanner.Text(), "Trade:") { 117 trades <- scanner.Text() 118 } 119 if strings.HasPrefix(scanner.Text(), "blockHeight:") { 120 fmt.Println(scanner.Text()) 121 } 122 } 123 scannerErr := bufio.NewScanner(stderr) 124 for scannerErr.Scan() { 125 log.Error("Run script: ", scannerErr.Text()) 126 } 127 // Wait for the script to finish 128 cmd.Wait() 129 }() 130 131 for trade := range trades { 132 after := strings.Split(trade, "Trade:")[1] 133 fields := strings.Split(after, " ") 134 if len(fields) < 8 { 135 continue 136 } 137 FromAmount, err := strconv.ParseFloat(fields[4], 64) 138 if err != nil { 139 fmt.Println("Error:", err) 140 continue 141 } 142 toAmount, err := strconv.ParseFloat(fields[3], 64) 143 if err != nil { 144 fmt.Println("Error:", err) 145 continue 146 } 147 price := FromAmount / toAmount 148 basetoken := dia.Asset{ 149 Symbol: fields[2], 150 Blockchain: s.exchange.BlockChain.Name, 151 Address: fields[6], 152 } 153 quotetoken := dia.Asset{ 154 Symbol: fields[1], 155 Blockchain: s.exchange.BlockChain.Name, 156 Address: fields[5], 157 } 158 trade := &dia.Trade{ 159 Symbol: fields[1], 160 Pair: fields[0], 161 Price: price, 162 Volume: toAmount, 163 Time: time.Now(), 164 ForeignTradeID: fields[7], 165 Source: s.exchange.Name, 166 BaseToken: basetoken, 167 QuoteToken: quotetoken, 168 VerifiedPair: true, 169 } 170 171 log.Info("Got Trade: ", trade) 172 s.chanTrades <- trade 173 } 174 } 175 176 func (s *ZenlinkScraper) mainLoop(nodeScriptPath string) { 177 for { 178 select { 179 case <-s.shutdown: 180 log.Warn("Shutting down ZenlinkScraper") 181 s.cleanup(nil) 182 return 183 default: 184 } 185 s.receive(nodeScriptPath) 186 } 187 } 188 189 func (scraper *ZenlinkScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 190 scraper.errorLock.RLock() 191 defer scraper.errorLock.RUnlock() 192 193 if scraper.error != nil { 194 return nil, scraper.error 195 } 196 197 if scraper.closed { 198 return nil, errors.New("ZenlinkScraper is closed") 199 } 200 201 pairScraper := &ZenlinkPairScraper{ 202 parent: scraper, 203 pair: pair, 204 } 205 206 scraper.pairScrapers[pair.ForeignName] = pairScraper 207 return pairScraper, nil 208 } 209 210 func (s *ZenlinkScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 211 var zenlinkResponse ZenlinkPairResponse 212 body := `{ 213 "code": 0, 214 "data": [ 215 { 216 symbol: "vKSM/KSM", 217 displayName: "vKSM/KSM", 218 baseAsset: "vKSM", 219 quoteAsset: "KSM" 220 ] 221 }` 222 err = json.Unmarshal([]byte(body), &zenlinkResponse) 223 if err != nil { 224 log.Error("unmarshal symbols: ", err) 225 } 226 227 for _, p := range zenlinkResponse.Data { 228 pairToNormalize := dia.ExchangePair{ 229 Symbol: strings.Split(p.Symbol, "/")[0], 230 ForeignName: p.Symbol, 231 Exchange: s.exchange.Name, 232 } 233 pairs = append(pairs, pairToNormalize) 234 } 235 return 236 } 237 238 func (s *ZenlinkScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 239 return dia.ExchangePair{}, nil 240 } 241 242 func (s *ZenlinkScraper) FillSymbolData(symbol string) (dia.Asset, error) { 243 return dia.Asset{Symbol: symbol}, nil 244 } 245 246 func (s *ZenlinkScraper) cleanup(err error) { 247 s.errorLock.Lock() 248 defer s.errorLock.Unlock() 249 if err != nil { 250 s.error = err 251 } 252 s.closed = true 253 close(s.shutdownDone) 254 } 255 256 func (s *ZenlinkScraper) Close() error { 257 if s.closed { 258 return errors.New("ZenlinkScraper: Already closed") 259 } 260 if err := s.wsClient.Close(); err != nil { 261 log.Error("Error closing Zenlink.wsClient", err) 262 } 263 close(s.shutdown) 264 <-s.shutdownDone 265 defer s.errorLock.RUnlock() 266 return s.error 267 } 268 269 type ZenlinkPairScraper struct { 270 parent *ZenlinkScraper 271 pair dia.ExchangePair 272 closed bool 273 } 274 275 // Close stops listening for trades of the pair associated with s 276 func (ps *ZenlinkPairScraper) Close() error { 277 var err error 278 s := ps.parent 279 // if parent already errored, return early 280 s.errorLock.RLock() 281 defer s.errorLock.RUnlock() 282 if s.error != nil { 283 return s.error 284 } 285 if ps.closed { 286 return errors.New("ZenlinkPairScraper: Already closed") 287 } 288 // TODO stop collection for the pair 289 290 ps.closed = true 291 return err 292 } 293 294 // Channel returns a channel that can be used to receive trades 295 func (ps *ZenlinkScraper) Channel() chan *dia.Trade { 296 return ps.chanTrades 297 } 298 299 // Error returns an error when the channel Channel() is closed 300 // and nil otherwise 301 func (ps *ZenlinkPairScraper) Error() error { 302 s := ps.parent 303 s.errorLock.RLock() 304 defer s.errorLock.RUnlock() 305 return s.error 306 } 307 308 // Pair returns the pair this scraper is subscribed to 309 func (ps *ZenlinkPairScraper) Pair() dia.ExchangePair { 310 return ps.pair 311 }