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

     1  package scrapers
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"math/big"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	ConverterRegistry "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/bancor"
    14  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/bancor/BancorNetwork"
    15  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/bancor/ConverterTypeFour"
    16  	ConvertertypeOne "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/bancor/ConverterTypeOne"
    17  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/bancor/ConverterTypeThree"
    18  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/bancor/ConverterTypeZero"
    19  	uniswapcontract "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/uniswap"
    20  
    21  	"github.com/diadata-org/diadata/pkg/dia"
    22  	"github.com/diadata-org/diadata/pkg/utils"
    23  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    24  	"github.com/ethereum/go-ethereum/common"
    25  	"github.com/ethereum/go-ethereum/ethclient"
    26  )
    27  
    28  type BancorPool struct {
    29  	Reserves []struct {
    30  		DltID   string `json:"dlt_id"`
    31  		Symbol  string `json:"symbol"`
    32  		Name    string `json:"name"`
    33  		Balance struct {
    34  			Usd string `json:"usd"`
    35  		} `json:"balance"`
    36  		Weight int `json:"weight"`
    37  		Price  struct {
    38  			Usd string `json:"usd"`
    39  		} `json:"price"`
    40  		Price24HAgo struct {
    41  			Usd string `json:"usd"`
    42  		} `json:"price_24h_ago"`
    43  		Volume24H struct {
    44  			Usd  string `json:"usd"`
    45  			Base string `json:"base"`
    46  		} `json:"volume_24h"`
    47  	} `json:"reserves"`
    48  	DltType        string `json:"dlt_type"`
    49  	DltID          string `json:"dlt_id"`
    50  	Type           int    `json:"type"`
    51  	Version        int    `json:"version"`
    52  	Symbol         string `json:"symbol"`
    53  	Name           string `json:"name"`
    54  	Supply         string `json:"supply"`
    55  	ConverterDltID string `json:"converter_dlt_id"`
    56  	ConversionFee  string `json:"conversion_fee"`
    57  	Liquidity      struct {
    58  		Usd string `json:"usd"`
    59  	} `json:"liquidity"`
    60  	Volume24H struct {
    61  		Usd string `json:"usd"`
    62  	} `json:"volume_24h"`
    63  	Fees24H struct {
    64  		Usd string `json:"usd"`
    65  	} `json:"fees_24h"`
    66  }
    67  
    68  type BancorPools struct {
    69  	Data      []BancorPool `json:"data"`
    70  	Timestamp struct {
    71  		Ethereum struct {
    72  			Block     int   `json:"block"`
    73  			Timestamp int64 `json:"timestamp"`
    74  		} `json:"ethereum"`
    75  	} `json:"timestamp"`
    76  }
    77  
    78  type BancorSwap struct {
    79  	Pair       dia.ExchangePair
    80  	FromAmount float64
    81  	ToAmount   float64
    82  	ID         string
    83  	Timestamp  int64
    84  }
    85  
    86  type BancorScraper struct {
    87  	WsClient   *ethclient.Client
    88  	RestClient *ethclient.Client
    89  
    90  	exchangeName string
    91  
    92  	// channels to signal events
    93  	run          bool
    94  	initDone     chan nothing
    95  	shutdown     chan nothing
    96  	shutdownDone chan nothing
    97  
    98  	errorLock sync.RWMutex
    99  	error     error
   100  	closed    bool
   101  
   102  	pairScrapers   map[string]*BancorPairScraper
   103  	productPairIds map[string]int
   104  	chanTrades     chan *dia.Trade
   105  }
   106  
   107  func NewBancorScraper(exchange dia.Exchange, scrape bool) *BancorScraper {
   108  	var wsClient, restClient *ethclient.Client
   109  	var err error
   110  
   111  	restClient, err = ethclient.Dial(utils.Getenv("ETH_URI_REST", restDialEth))
   112  	if err != nil {
   113  		log.Fatal("init rest client: ", err)
   114  	}
   115  
   116  	wsClient, err = ethclient.Dial(utils.Getenv("ETH_URI_WS", wsDialEth))
   117  	if err != nil {
   118  		log.Fatal("init ws client: ", err)
   119  	}
   120  
   121  	scraper := &BancorScraper{
   122  		exchangeName:   exchange.Name,
   123  		WsClient:       wsClient,
   124  		RestClient:     restClient,
   125  		initDone:       make(chan nothing),
   126  		shutdown:       make(chan nothing),
   127  		shutdownDone:   make(chan nothing),
   128  		productPairIds: make(map[string]int),
   129  		pairScrapers:   make(map[string]*BancorPairScraper),
   130  		chanTrades:     make(chan *dia.Trade),
   131  	}
   132  
   133  	if scrape {
   134  		go scraper.mainLoop()
   135  	}
   136  	return scraper
   137  }
   138  
   139  func (scraper *BancorScraper) mainLoop() {
   140  
   141  	scraper.GetpoolAddress()
   142  
   143  	sink, err := scraper.GetConversion()
   144  	if err != nil {
   145  		log.Errorln("Error GetConversion", err)
   146  	}
   147  
   148  	go func() {
   149  		for {
   150  
   151  			rawSwap := <-sink
   152  			revRawSwap := reverseBNTSwap(*rawSwap)
   153  
   154  			var address []common.Address
   155  			swap, err := scraper.normalizeSwap(revRawSwap)
   156  			if err != nil {
   157  				log.Error("error normalizeSwap: ", err)
   158  
   159  			}
   160  
   161  			price, volume := scraper.getSwapData(swap)
   162  			address = append(address, revRawSwap.FromToken)
   163  			address = append(address, revRawSwap.ToToken)
   164  
   165  			pair := scraper.GetPair(address)
   166  
   167  			trade := &dia.Trade{
   168  				Symbol:         pair.Symbol,
   169  				Pair:           pair.ForeignName,
   170  				Price:          price,
   171  				Volume:         volume,
   172  				Time:           time.Now(),
   173  				ForeignTradeID: revRawSwap.Raw.TxHash.String(),
   174  				Source:         scraper.exchangeName,
   175  				BaseToken:      pair.UnderlyingPair.BaseToken,
   176  				QuoteToken:     pair.UnderlyingPair.QuoteToken,
   177  				VerifiedPair:   true,
   178  			}
   179  
   180  			log.Info("Got Trade: ", trade)
   181  			scraper.chanTrades <- trade
   182  
   183  		}
   184  	}()
   185  	if scraper.error == nil {
   186  		scraper.error = errors.New("Main loop terminated by Close().")
   187  	}
   188  	scraper.cleanup(nil)
   189  }
   190  
   191  // Reverse swap involving BNT such that pair is always XXX-BNT.
   192  func reverseBNTSwap(rawSwap BancorNetwork.BancorNetworkConversion) BancorNetwork.BancorNetworkConversion {
   193  	var revRawSwap BancorNetwork.BancorNetworkConversion
   194  	fmt.Println("raw swap ToToken: ", rawSwap.ToToken.Hex())
   195  	if rawSwap.FromToken.Hex() == "0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C" {
   196  		revRawSwap.ToAmount = rawSwap.FromAmount
   197  		revRawSwap.FromAmount = rawSwap.ToAmount
   198  		revRawSwap.FromToken = rawSwap.ToToken
   199  		revRawSwap.ToToken = rawSwap.FromToken
   200  		revRawSwap.Raw = rawSwap.Raw
   201  		return revRawSwap
   202  	}
   203  	return rawSwap
   204  }
   205  
   206  func (scraper *BancorScraper) GetpoolAddress() {
   207  	// Get All Anchors
   208  
   209  	converTerRegistryAddress := common.HexToAddress("0xC0205e203F423Bcd8B2a4d6f8C8A154b0Aa60F19")
   210  
   211  	converter, err := ConverterRegistry.NewConverterRegistryCaller(converTerRegistryAddress, scraper.RestClient)
   212  	if err != nil {
   213  		log.Errorln("Error Getting Anchors", err)
   214  	}
   215  
   216  	_, err = converter.GetAnchors(&bind.CallOpts{})
   217  	if err != nil {
   218  		log.Errorln("Error Getting Anchors", err)
   219  	}
   220  
   221  }
   222  
   223  func (scraper *BancorScraper) GetConversion() (chan *BancorNetwork.BancorNetworkConversion, error) {
   224  
   225  	sink := make(chan *BancorNetwork.BancorNetworkConversion)
   226  
   227  	var conversionFiltererContract *BancorNetwork.BancorNetworkFilterer
   228  
   229  	address := common.HexToAddress("0x2F9EC37d6CcFFf1caB21733BdaDEdE11c823cCB0") // bancor Network
   230  	conversionFiltererContract, err := BancorNetwork.NewBancorNetworkFilterer(address, scraper.WsClient)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  
   235  	subs, err := conversionFiltererContract.WatchConversion(&bind.WatchOpts{}, sink, []common.Address{}, []common.Address{}, []common.Address{})
   236  	if err != nil {
   237  		log.Error("error in get swaps channel: ", err)
   238  	}
   239  	log.Infoln("Subscribed", subs)
   240  
   241  	return sink, nil
   242  
   243  }
   244  
   245  // normalizeUniswapSwap takes a swap as returned by the swap contract's channel and converts it to a UniswapSwap type
   246  func (scrapper *BancorScraper) normalizeSwap(swap BancorNetwork.BancorNetworkConversion) (BancorSwap, error) {
   247  	var normalizedSwap BancorSwap
   248  	if swap.FromToken.Hex() == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" {
   249  		amount0, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.FromAmount), new(big.Float).SetFloat64(math.Pow10(18))).Float64()
   250  		normalizedSwap.FromAmount = amount0
   251  	} else {
   252  		fromToken, err := uniswapcontract.NewIERC20(swap.FromToken, scrapper.RestClient)
   253  		if err != nil {
   254  			return normalizedSwap, err
   255  		}
   256  		fromTokenDecimal, err := fromToken.Decimals(&bind.CallOpts{})
   257  		if err != nil {
   258  			return normalizedSwap, err
   259  		}
   260  		decimals0 := int(fromTokenDecimal)
   261  		amount0, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.FromAmount), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64()
   262  		normalizedSwap.FromAmount = amount0
   263  	}
   264  
   265  	if swap.ToToken.Hex() == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" {
   266  		amount1, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.ToAmount), new(big.Float).SetFloat64(math.Pow10(18))).Float64()
   267  		normalizedSwap.ToAmount = amount1
   268  	} else {
   269  		toToken, err := uniswapcontract.NewIERC20(swap.ToToken, scrapper.RestClient)
   270  		if err != nil {
   271  			return normalizedSwap, err
   272  		}
   273  		toTokenDecimal, err := toToken.Decimals(&bind.CallOpts{})
   274  		if err != nil {
   275  			return normalizedSwap, err
   276  		}
   277  		decimals1 := int(toTokenDecimal)
   278  		amount1, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.ToAmount), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64()
   279  		normalizedSwap.ToAmount = amount1
   280  	}
   281  
   282  	pair := scrapper.GetPair([]common.Address{swap.ToToken, swap.FromToken})
   283  	pair, _ = scrapper.NormalizePair(pair)
   284  	normalizedSwap.Pair = pair
   285  	normalizedSwap.ID = swap.Raw.TxHash.Hex()
   286  	normalizedSwap.Timestamp = time.Now().Unix()
   287  
   288  	return normalizedSwap, nil
   289  }
   290  
   291  func (scrapper *BancorScraper) getSwapData(swap BancorSwap) (price float64, volume float64) {
   292  	volume = swap.FromAmount
   293  	price = swap.ToAmount / swap.FromAmount
   294  	return
   295  }
   296  
   297  func (scraper *BancorScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   298  	if pair.UnderlyingPair.BaseToken.Address == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" {
   299  		pair.UnderlyingPair.BaseToken.Address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
   300  	}
   301  	if pair.UnderlyingPair.QuoteToken.Address == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" {
   302  		pair.UnderlyingPair.QuoteToken.Address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
   303  	}
   304  	return pair, nil
   305  }
   306  
   307  func (scraper *BancorScraper) ConverterTypeZero(address common.Address) (tokenAddress []common.Address, err error) {
   308  
   309  	contract, err := ConverterTypeZero.NewConverterTypeZeroCaller(address, scraper.RestClient)
   310  	if err != nil {
   311  		return
   312  	}
   313  
   314  	tokenCount, err := contract.ConnectorTokenCount(&bind.CallOpts{})
   315  	if err != nil {
   316  		return
   317  	}
   318  
   319  	if tokenCount == 2 {
   320  		token1, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(1))
   321  		if err != nil {
   322  			log.Println("Error", err)
   323  		}
   324  		tokenAddress = append(tokenAddress, token1)
   325  		token2, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(0))
   326  		if err != nil {
   327  			log.Println("Error", err)
   328  		}
   329  		tokenAddress = append(tokenAddress, token2)
   330  
   331  	}
   332  
   333  	return
   334  
   335  }
   336  
   337  func (scraper *BancorScraper) ConverterTypeOne(address common.Address) (tokenAddress []common.Address, err error) {
   338  
   339  	contract, err := ConvertertypeOne.NewConvertertypeOne(address, scraper.RestClient)
   340  	if err != nil {
   341  		return
   342  	}
   343  
   344  	tokenCount, err := contract.ConnectorTokenCount(&bind.CallOpts{})
   345  	if err != nil {
   346  		return
   347  	}
   348  
   349  	if tokenCount == 2 {
   350  		token1, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(1))
   351  		if err != nil {
   352  			log.Println("Error", err)
   353  		}
   354  		tokenAddress = append(tokenAddress, token1)
   355  		token2, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(0))
   356  		if err != nil {
   357  			log.Println("Error", err)
   358  		}
   359  		tokenAddress = append(tokenAddress, token2)
   360  
   361  	}
   362  
   363  	return
   364  
   365  }
   366  
   367  func (scraper *BancorScraper) ConverterTypeThree(address common.Address) (tokenAddress []common.Address, err error) {
   368  
   369  	contract, err := ConverterTypeThree.NewConverterTypeThree(address, scraper.RestClient)
   370  	if err != nil {
   371  		return
   372  	}
   373  
   374  	tokenCount, err := contract.ConnectorTokenCount(&bind.CallOpts{})
   375  	if err != nil {
   376  		return
   377  	}
   378  
   379  	log.Println("tokenCount", tokenCount)
   380  
   381  	if tokenCount == 2 {
   382  		token1, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(1))
   383  		if err != nil {
   384  			log.Println("Error", err)
   385  		}
   386  		tokenAddress = append(tokenAddress, token1)
   387  		token2, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(0))
   388  		if err != nil {
   389  			log.Println("Error", err)
   390  		}
   391  		tokenAddress = append(tokenAddress, token2)
   392  
   393  	}
   394  
   395  	return
   396  
   397  }
   398  
   399  func (scraper *BancorScraper) ConverterTypeFour(address common.Address) (tokenAddress []common.Address, err error) {
   400  
   401  	contract, err := ConverterTypeFour.NewConverterTypeFour(address, scraper.RestClient)
   402  	if err != nil {
   403  		return
   404  	}
   405  
   406  	tokenCount, err := contract.ConnectorTokenCount(&bind.CallOpts{})
   407  	if err != nil {
   408  		return
   409  	}
   410  
   411  	if tokenCount == 2 {
   412  		token1, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(1))
   413  		if err != nil {
   414  			log.Println("Error", err)
   415  		}
   416  		tokenAddress = append(tokenAddress, token1)
   417  		token2, err := contract.ConnectorTokens(&bind.CallOpts{}, big.NewInt(0))
   418  		if err != nil {
   419  			log.Println("Error", err)
   420  		}
   421  		tokenAddress = append(tokenAddress, token2)
   422  
   423  	}
   424  
   425  	return
   426  
   427  }
   428  
   429  func (scraper *BancorScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   430  	return dia.Asset{Symbol: symbol}, nil
   431  }
   432  
   433  func (scraper *BancorScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   434  	pools, err := scraper.readPools()
   435  	if err != nil {
   436  		log.Error("Couldn't obtain Bancor product ids:", err)
   437  	}
   438  	for _, pool := range pools {
   439  		var address []common.Address
   440  
   441  		switch pool.Type {
   442  		case 0:
   443  			{
   444  				address, err = scraper.ConverterTypeZero(common.HexToAddress(pool.ConverterDltID))
   445  				if err != nil {
   446  					log.Errorln("Error getting Address", err)
   447  				}
   448  			}
   449  		case 1:
   450  			{
   451  				address, err = scraper.ConverterTypeOne(common.HexToAddress(pool.ConverterDltID))
   452  				if err != nil {
   453  					log.Errorln("Error getting Address", err)
   454  				}
   455  			}
   456  		case 3:
   457  			{
   458  				address, err = scraper.ConverterTypeThree(common.HexToAddress(pool.ConverterDltID))
   459  				if err != nil {
   460  					log.Errorln("Error getting Address", err)
   461  				}
   462  			}
   463  		case 4:
   464  			{
   465  				address, err = scraper.ConverterTypeFour(common.HexToAddress(pool.ConverterDltID))
   466  				if err != nil {
   467  					log.Errorln("Error getting Address", err)
   468  				}
   469  			}
   470  		}
   471  
   472  		pair := scraper.GetPair(address)
   473  
   474  		if pair.Symbol != "" && strings.Split(pair.ForeignName, "-")[1] != "" {
   475  			log.Println("found pair: ", pair)
   476  			pairs = append(pairs, pair)
   477  		}
   478  
   479  	}
   480  
   481  	return
   482  }
   483  
   484  func (scraper *BancorScraper) GetPair(address []common.Address) dia.ExchangePair {
   485  	var (
   486  		symbol0 string
   487  		symbol1 string
   488  	)
   489  
   490  	token0, err := uniswapcontract.NewIERC20Caller(address[0], scraper.RestClient)
   491  	if err != nil {
   492  		log.Errorln("Error Getting token 0 ", err)
   493  	}
   494  	token1, err := uniswapcontract.NewIERC20Caller(address[1], scraper.RestClient)
   495  	if err != nil {
   496  		log.Errorln("Error Getting token 1 ", err)
   497  	}
   498  
   499  	if address[0].Hex() == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" {
   500  		address[0] = common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
   501  		symbol0 = "WETH"
   502  	} else {
   503  		symbol0, err = token0.Symbol(&bind.CallOpts{})
   504  		if err != nil {
   505  			log.Error(err)
   506  		}
   507  	}
   508  	if address[1].Hex() == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" {
   509  		address[0] = common.HexToAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")
   510  		symbol1 = "WETH"
   511  	} else {
   512  		symbol1, err = token1.Symbol(&bind.CallOpts{})
   513  		if err != nil {
   514  			log.Error(err)
   515  		}
   516  	}
   517  
   518  	basetoken := dia.Asset{
   519  		Symbol:     symbol1,
   520  		Address:    address[1].Hex(),
   521  		Blockchain: dia.ETHEREUM,
   522  	}
   523  	quotetoken := dia.Asset{
   524  		Symbol:     symbol0,
   525  		Address:    address[0].Hex(),
   526  		Blockchain: dia.ETHEREUM,
   527  	}
   528  
   529  	return dia.ExchangePair{
   530  		ForeignName:    symbol0 + "-" + symbol1,
   531  		Symbol:         symbol0,
   532  		Exchange:       scraper.exchangeName,
   533  		UnderlyingPair: dia.Pair{BaseToken: basetoken, QuoteToken: quotetoken},
   534  	}
   535  }
   536  
   537  func (scraper *BancorScraper) readPools() ([]BancorPool, error) {
   538  	var bpools BancorPools
   539  	pairs, _, err := utils.GetRequest("https://api-v2.bancor.network/pools")
   540  	if err != nil {
   541  		return nil, err
   542  	}
   543  	err = json.Unmarshal(pairs, &bpools)
   544  	if err != nil {
   545  		log.Error("Error reading json", err)
   546  
   547  	}
   548  	return bpools.Data, nil
   549  
   550  }
   551  
   552  func (scraper *BancorScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   553  	scraper.errorLock.RLock()
   554  	defer scraper.errorLock.RUnlock()
   555  
   556  	if scraper.error != nil {
   557  		return nil, scraper.error
   558  	}
   559  
   560  	if scraper.closed {
   561  		return nil, errors.New("BancorScraper is closed")
   562  	}
   563  
   564  	pairScraper := &BancorPairScraper{
   565  		parent: scraper,
   566  		pair:   pair,
   567  	}
   568  
   569  	scraper.pairScrapers[pair.ForeignName] = pairScraper
   570  
   571  	return pairScraper, nil
   572  }
   573  func (scraper *BancorScraper) cleanup(err error) {
   574  	scraper.errorLock.Lock()
   575  	defer scraper.errorLock.Unlock()
   576  	if err != nil {
   577  		scraper.error = err
   578  	}
   579  	scraper.closed = true
   580  	close(scraper.shutdownDone)
   581  }
   582  
   583  func (scraper *BancorScraper) Close() error {
   584  	// close the pair scraper channels
   585  	scraper.run = false
   586  	for _, pairScraper := range scraper.pairScrapers {
   587  		pairScraper.closed = true
   588  	}
   589  
   590  	close(scraper.shutdown)
   591  	<-scraper.shutdownDone
   592  	return nil
   593  }
   594  
   595  type BancorPairScraper struct {
   596  	parent *BancorScraper
   597  	pair   dia.ExchangePair
   598  	closed bool
   599  }
   600  
   601  func (pairScraper *BancorPairScraper) Pair() dia.ExchangePair {
   602  	return pairScraper.pair
   603  }
   604  
   605  func (scraper *BancorScraper) Channel() chan *dia.Trade {
   606  	return scraper.chanTrades
   607  }
   608  
   609  func (pairScraper *BancorPairScraper) Error() error {
   610  	s := pairScraper.parent
   611  	s.errorLock.RLock()
   612  	defer s.errorLock.RUnlock()
   613  	return s.error
   614  }
   615  
   616  func (pairScraper *BancorPairScraper) Close() error {
   617  	pairScraper.parent.errorLock.RLock()
   618  	defer pairScraper.parent.errorLock.RUnlock()
   619  	pairScraper.closed = true
   620  	return nil
   621  }