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  }