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

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/diadata-org/diadata/pkg/dia"
    11  	models "github.com/diadata-org/diadata/pkg/model"
    12  	"github.com/ethereum/go-ethereum"
    13  
    14  	"github.com/ethereum/go-ethereum/accounts/abi"
    15  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    16  	"github.com/ethereum/go-ethereum/common"
    17  	"github.com/ethereum/go-ethereum/core/types"
    18  	"github.com/ethereum/go-ethereum/crypto"
    19  	"github.com/ethereum/go-ethereum/ethclient"
    20  
    21  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/anyerc20"
    22  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/uniswap"
    23  
    24  	"math/big"
    25  
    26  	"sync"
    27  )
    28  
    29  type BridgeSwapToken struct {
    30  	Address  common.Address
    31  	Symbol   string
    32  	Decimals uint8
    33  	Name     string
    34  }
    35  
    36  type BridgeSwapPair struct {
    37  	Token0      BridgeSwapToken
    38  	Token1      BridgeSwapToken
    39  	ForeignName string
    40  	Address     common.Address
    41  }
    42  
    43  type BridgeSwapSwap struct {
    44  	TransactionHash string
    45  	TokenAddress    common.Address
    46  	ToAddress       common.Address
    47  	Amount          *big.Int
    48  	FromChainId     *big.Int
    49  	ToChainId       *big.Int
    50  }
    51  
    52  type BridgeSwapScraper struct {
    53  	WsClient   *ethclient.Client
    54  	RestClient *ethclient.Client
    55  
    56  	// signaling channels for session initialization and finishing
    57  	//initDone     chan nothing
    58  	//run          bool
    59  	shutdown     chan nothing
    60  	shutdownDone chan nothing
    61  	// error handling; to read error or closed, first acquire read lock
    62  	// only cleanup method should hold write lock
    63  	errorLock sync.RWMutex
    64  	error     error
    65  	closed    bool
    66  	// used to keep track of trading pairs that we subscribed to
    67  	pairScrapers map[string]*BridgeSwapPairScraper
    68  	//exchangeName string
    69  	chanTrades chan *dia.Trade
    70  	// waitTime     int
    71  	// If true, only pairs given in config file are scraped. Default is false.
    72  	//listenByAddress bool
    73  	relDB *models.RelDB
    74  }
    75  
    76  type MultiChainConfig struct {
    77  	restURL                string
    78  	wsURL                  string
    79  	contractAddress        string
    80  	contratDeployedAtBlock int64
    81  }
    82  
    83  var (
    84  	restClients       map[string]*ethclient.Client
    85  	wsClients         map[string]*ethclient.Client
    86  	multichainconfigs map[string]MultiChainConfig
    87  )
    88  
    89  const (
    90  	ftmHTTP   = "https://rpc.ftm.tools/"
    91  	ethHTTP   = "https://eth-mainnet.alchemyapi.io/v2/UpWALFqrTh5m8bojhDcgtBIif-Ug5UUE"
    92  	abiString = `[{
    93  		"anonymous": false,
    94  		"inputs": [{
    95  			"indexed": true,
    96  			"internalType": "bytes32",
    97  			"name": "txhash",
    98  			"type": "bytes32"
    99  		}, {
   100  			"indexed": true,
   101  			"internalType": "address",
   102  			"name": "token",
   103  			"type": "address"
   104  		}, {
   105  			"indexed": true,
   106  			"internalType": "address",
   107  			"name": "to",
   108  			"type": "address"
   109  		}, {
   110  			"indexed": false,
   111  			"internalType": "uint256",
   112  			"name": "amount",
   113  			"type": "uint256"
   114  		}, {
   115  			"indexed": false,
   116  			"internalType": "uint256",
   117  			"name": "fromChainID",
   118  			"type": "uint256"
   119  		}, {
   120  			"indexed": false,
   121  			"internalType": "uint256",
   122  			"name": "toChainID",
   123  			"type": "uint256"
   124  		}],
   125  		"name": "LogAnySwapIn",
   126  		"type": "event"
   127  	}, {
   128  		"anonymous": false,
   129  		"inputs": [{
   130  			"indexed": true,
   131  			"internalType": "address",
   132  			"name": "token",
   133  			"type": "address"
   134  		}, {
   135  			"indexed": true,
   136  			"internalType": "address",
   137  			"name": "from",
   138  			"type": "address"
   139  		}, {
   140  			"indexed": true,
   141  			"internalType": "address",
   142  			"name": "to",
   143  			"type": "address"
   144  		}, {
   145  			"indexed": false,
   146  			"internalType": "uint256",
   147  			"name": "amount",
   148  			"type": "uint256"
   149  		}, {
   150  			"indexed": false,
   151  			"internalType": "uint256",
   152  			"name": "fromChainID",
   153  			"type": "uint256"
   154  		}, {
   155  			"indexed": false,
   156  			"internalType": "uint256",
   157  			"name": "toChainID",
   158  			"type": "uint256"
   159  		}],
   160  		"name": "LogAnySwapOut",
   161  		"type": "event"
   162  	}]`
   163  )
   164  
   165  // NewBridgeSwapScraper returns a new BridgeSwapScraper for the given pair
   166  func NewBridgeSwapScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BridgeSwapScraper {
   167  	var s *BridgeSwapScraper
   168  	// var waitgroup sync.WaitGroup
   169  	multichainconfigs = make(map[string]MultiChainConfig)
   170  
   171  	multichainconfigs["1"] = MultiChainConfig{restURL: chainConfigs["1"].RestURL, wsURL: chainConfigs["1"].WSURL, contratDeployedAtBlock: 12242619, contractAddress: "0x765277eebeca2e31912c9946eae1021199b39c61"}
   172  	multichainconfigs["56"] = MultiChainConfig{restURL: chainConfigs["56"].RestURL, wsURL: chainConfigs["56"].WSURL, contratDeployedAtBlock: 7910338, contractAddress: "0xd1c5966f9f5ee6881ff6b261bbeda45972b1b5f3"}
   173  	multichainconfigs["137"] = MultiChainConfig{restURL: chainConfigs["137"].RestURL, wsURL: chainConfigs["137"].WSURL, contratDeployedAtBlock: 17355461, contractAddress: "0x6ff0609046a38d76bd40c5863b4d1a2dce687f73"}
   174  	multichainconfigs["250"] = MultiChainConfig{restURL: chainConfigs["250"].RestURL, wsURL: chainConfigs["250"].WSURL, contratDeployedAtBlock: 8475644, contractAddress: "0x1ccca1ce62c62f7be95d4a67722a8fdbed6eecb4"}
   175  	multichainconfigs["42161"] = MultiChainConfig{restURL: chainConfigs["42161"].RestURL, wsURL: chainConfigs["42161"].WSURL, contratDeployedAtBlock: 15315466, contractAddress: "0x650af55d5877f289837c30b94af91538a7504b76"}
   176  	multichainconfigs["43114"] = MultiChainConfig{restURL: chainConfigs["43114"].RestURL, wsURL: chainConfigs["43114"].WSURL, contratDeployedAtBlock: 3397229, contractAddress: "0xB0731d50C681C45856BFc3f7539D5f61d4bE81D8"}
   177  
   178  	log.Info("NewBridgeSwapScraper: ", exchange.Name)
   179  	log.Infof("Init rest and ws client for %s.", exchange.BlockChain.Name)
   180  
   181  	s = &BridgeSwapScraper{
   182  		chanTrades:   make(chan *dia.Trade),
   183  		shutdown:     make(chan nothing),
   184  		shutdownDone: make(chan nothing),
   185  		pairScrapers: make(map[string]*BridgeSwapPairScraper),
   186  		relDB:        relDB,
   187  	}
   188  
   189  	if scrape {
   190  		s.loop()
   191  	}
   192  
   193  	return s
   194  }
   195  
   196  func (s *BridgeSwapScraper) loop() {
   197  
   198  	InitialiseRestClientsMap()
   199  	InitialiseWsClientsMap()
   200  
   201  	events := getFilteredEvents()
   202  	s.checkTransactionOnChain(events)
   203  
   204  }
   205  
   206  func (s *BridgeSwapScraper) checkTransactionOnChain(events chan types.Log) {
   207  	log.Infoln("Listening swaps")
   208  
   209  	//get hex value for swap function name for the transaction
   210  	LogAnySwapInbyteHex := crypto.Keccak256Hash([]byte("LogAnySwapIn(bytes32,address,address,uint256,uint256,uint256)"))
   211  
   212  	for {
   213  		msg := <-events
   214  		switch msg.Topics[0].Hex() {
   215  
   216  		case LogAnySwapInbyteHex.Hex():
   217  			log.Infoln("msg TxHash", msg.TxHash)
   218  
   219  			event, contractAbi := getEventDetailsAbi("LogAnySwapIn", msg)
   220  
   221  			outAmount, _ := event[0].(*big.Int)
   222  			fromChainIdValue, _ := event[1].(*big.Int)
   223  			toChainIdValue, _ := event[2].(*big.Int)
   224  
   225  			tokenAddress := getMultichainUnderlyingToken(common.HexToAddress(msg.Topics[2].Hex()), toChainIdValue.String())
   226  
   227  			if tokenAddress == common.HexToAddress("0x0000000000000000000000000000000000000000") {
   228  				tokenAddress = common.HexToAddress(msg.Topics[2].Hex())
   229  			}
   230  			bs := BridgeSwapSwap{
   231  				TransactionHash: msg.Topics[1].Hex(),
   232  				TokenAddress:    tokenAddress,
   233  				ToAddress:       common.HexToAddress(msg.Topics[3].Hex()),
   234  				Amount:          outAmount,
   235  				FromChainId:     fromChainIdValue,
   236  				ToChainId:       toChainIdValue,
   237  			}
   238  			log.Debugln("BridgeSwap", bs)
   239  			tokenbridged, inAmount, err := getDetailsFromTransactionHash(msg, fromChainIdValue, contractAbi)
   240  			if err != nil {
   241  				continue
   242  			}
   243  
   244  			quoteTokenName, err := GetName(tokenAddress, toChainIdValue.String())
   245  			if err != nil {
   246  				log.Warnf("Error getting GetName token %s of chain id %s", tokenAddress, toChainIdValue.String())
   247  			}
   248  			quoteTokenSymbol, err := GetSymbol(tokenAddress, toChainIdValue.String())
   249  			if err != nil {
   250  				log.Warnf("Error getting GetSymbol token %s of chain id %s", tokenAddress, toChainIdValue.String())
   251  			}
   252  			quoteTokenDecimal, err := GetDecimals(tokenAddress, toChainIdValue.String())
   253  			if err != nil {
   254  				log.Warnf("Error getting GetDecimals token %s of chain id %s", tokenAddress, toChainIdValue.String())
   255  				continue
   256  			}
   257  
   258  			baseBlockchain, isAvailable := evmID[fromChainIdValue.String()]
   259  			if !isAvailable {
   260  				log.Warn("Blockchain configs not available for chain with ID ", fromChainIdValue)
   261  				continue
   262  
   263  			}
   264  
   265  			quoteBlockchain, isAvailable := evmID[toChainIdValue.String()]
   266  			if !isAvailable {
   267  				log.Warn("Blockchain configs not available for chain with ID ", toChainIdValue)
   268  				continue
   269  
   270  			}
   271  
   272  			quoteToken := dia.Asset{
   273  				Address:    tokenAddress.Hex(),
   274  				Symbol:     quoteTokenSymbol,
   275  				Name:       quoteTokenName,
   276  				Decimals:   quoteTokenDecimal,
   277  				Blockchain: Blockchains[quoteBlockchain].Name,
   278  			}
   279  
   280  			baseTokenName, err := GetName(tokenbridged, fromChainIdValue.String())
   281  			if err != nil {
   282  				log.Warnf("Error getting GetName token %s of chain id %s", tokenbridged, fromChainIdValue.String())
   283  			}
   284  			baseTokenSymbol, err := GetSymbol(tokenbridged, fromChainIdValue.String())
   285  			if err != nil {
   286  				log.Warnf("Error getting GetSymbol token %s of chain id %s", tokenbridged, fromChainIdValue.String())
   287  			}
   288  			baseTokenDecimal, err := GetDecimals(tokenbridged, fromChainIdValue.String())
   289  			if err != nil {
   290  				log.Warnf("Error getting GetDecimals token %s of chain id %s", tokenbridged, fromChainIdValue.String())
   291  				continue
   292  			}
   293  
   294  			baseToken := dia.Asset{
   295  				Address:    tokenbridged.Hex(),
   296  				Symbol:     baseTokenName,
   297  				Name:       baseTokenSymbol,
   298  				Decimals:   baseTokenDecimal,
   299  				Blockchain: Blockchains[baseBlockchain].Name,
   300  			}
   301  
   302  			inAmountt := inAmount.Quo(inAmount, inAmount.Exp(big.NewInt(10), big.NewInt(int64(baseTokenDecimal)), nil))
   303  
   304  			outAmountt := outAmount.Div(outAmount, big.NewInt(int64(quoteTokenDecimal)))
   305  
   306  			priceamt := inAmountt.Div(inAmountt, outAmountt)
   307  
   308  			priceamtfloat, _ := new(big.Float).SetInt(priceamt).Float64()
   309  			inAmountfloat, _ := new(big.Float).SetInt(inAmountt).Float64()
   310  
   311  			t := &dia.Trade{
   312  				Symbol:         baseTokenName + "" + quoteTokenName,
   313  				Pair:           baseTokenName + "" + quoteTokenName,
   314  				Price:          priceamtfloat,
   315  				Volume:         inAmountfloat,
   316  				BaseToken:      baseToken,
   317  				QuoteToken:     quoteToken,
   318  				Time:           time.Now(),
   319  				ForeignTradeID: msg.TxHash.Hex(),
   320  				// Source:         s.exchangeName,
   321  				VerifiedPair: true,
   322  			}
   323  			log.Println("trade", t)
   324  			s.mapasset(*t)
   325  			// s.chanTrades <- t
   326  
   327  		}
   328  	}
   329  
   330  }
   331  
   332  func (s *BridgeSwapScraper) mapasset(t dia.Trade) {
   333  	//check if quote token exists
   334  
   335  	quoteToken_id, err := s.relDB.GetAssetID(t.QuoteToken)
   336  	if err != nil {
   337  		log.Errorln("Error getting quotetoken asset id", err)
   338  	}
   339  	baseToken_id, err := s.relDB.GetAssetID(t.BaseToken)
   340  	if err != nil {
   341  		log.Errorln("Error getting basetoken asset id", err)
   342  	}
   343  
   344  	quote_group_id, err := s.relDB.GetAssetMap(quoteToken_id)
   345  	if err != nil {
   346  		log.Errorln("quotetoken not exists", quoteToken_id)
   347  	} else if quote_group_id != "" {
   348  		log.Errorln("InsertAssetMap1 ", quote_group_id, baseToken_id)
   349  		errInsertAssetMap := s.relDB.InsertAssetMap(quote_group_id, baseToken_id)
   350  		log.Errorln("err InsertAssetMap1", errInsertAssetMap)
   351  		return
   352  	}
   353  
   354  	base_group_id, err := s.relDB.GetAssetMap(baseToken_id)
   355  	if err != nil {
   356  		log.Errorln("base does not exists ")
   357  	} else if quote_group_id != "" {
   358  		log.Errorln("InsertAssetMap2 ", quote_group_id, baseToken_id)
   359  		errInsertAssetMap := s.relDB.InsertAssetMap(base_group_id, quoteToken_id)
   360  		log.Errorln("err InsertAssetMap2", errInsertAssetMap)
   361  
   362  		return
   363  	}
   364  	log.Errorln("InsertAssetMap3 ", quoteToken_id)
   365  
   366  	err = s.relDB.InsertNewAssetMap(quoteToken_id)
   367  	log.Errorln("err InsertAssetMap3", err)
   368  
   369  	gpid, err := s.relDB.GetAssetMap(quoteToken_id)
   370  	if err != nil {
   371  		log.Errorln("gpid generated err ", err)
   372  	}
   373  	s.relDB.InsertAssetMap(gpid, baseToken_id)
   374  	log.Infoln("quote_group_id, base_group_id", baseToken_id, gpid)
   375  }
   376  
   377  func GetDecimals(tokenAddress common.Address, chainid string) (decimals uint8, err error) {
   378  
   379  	var contract *uniswap.IERC20Caller
   380  	contract, err = uniswap.NewIERC20Caller(tokenAddress, restClients[chainid])
   381  	if err != nil {
   382  		log.Error(err)
   383  		return
   384  	}
   385  	decimals, err = contract.Decimals(&bind.CallOpts{})
   386  
   387  	return
   388  }
   389  
   390  func GetName(tokenAddress common.Address, chainid string) (name string, err error) {
   391  
   392  	var contract *uniswap.IERC20Caller
   393  	contract, err = uniswap.NewIERC20Caller(tokenAddress, restClients[chainid])
   394  	if err != nil {
   395  		log.Error(err)
   396  		return
   397  	}
   398  	name, err = contract.Name(&bind.CallOpts{})
   399  
   400  	return
   401  }
   402  
   403  func GetSymbol(tokenAddress common.Address, chainid string) (name string, err error) {
   404  
   405  	var contract *uniswap.IERC20Caller
   406  	contract, err = uniswap.NewIERC20Caller(tokenAddress, restClients[chainid])
   407  	if err != nil {
   408  		log.Error(err)
   409  		return
   410  	}
   411  	name, err = contract.Symbol(&bind.CallOpts{})
   412  
   413  	return
   414  }
   415  
   416  func getEventDetailsAbi(funcName string, msg types.Log) ([]interface{}, abi.ABI) {
   417  	var event []interface{}
   418  
   419  	contractAbi, err := abi.JSON(strings.NewReader(abiString))
   420  	if err != nil {
   421  		log.Fatal("read contract abi: ", err)
   422  	}
   423  	event, err = contractAbi.Unpack(funcName, msg.Data)
   424  	if err != nil {
   425  		log.Fatal("unpack event: ", err)
   426  	}
   427  	return event, contractAbi
   428  }
   429  
   430  func getDetailsFromTransactionHash(msg types.Log, fromChainIdValue *big.Int, contractAbi abi.ABI) (tokenmoved common.Address, inAmount *big.Int, err error) {
   431  	//check if chain address value is present in map
   432  	restClient, ok := restClients[fromChainIdValue.String()]
   433  	//LogAnySwapOut (index_topic_1 address token, index_topic_2 address from, index_topic_3 address to, uint256 amount, uint256 fromChainID, uint256 toChainID)
   434  	LogAnySwapOutbyteHex := crypto.Keccak256Hash([]byte("LogAnySwapOut(address,address,address,uint256,uint256,uint256)"))
   435  	if ok {
   436  		//get transaction receipt from transaction hash
   437  		var receipt *types.Receipt
   438  		receipt, err = restClient.TransactionReceipt(context.Background(), common.HexToHash(msg.Topics[1].Hex()))
   439  		if err != nil {
   440  			log.Errorf("fetch transaction receipt of tx %s on chain with ID %v: %v", msg.Topics[1].Hex(), fromChainIdValue.Int64(), err)
   441  			return
   442  		}
   443  
   444  		for _, txlog := range receipt.Logs {
   445  			switch txlog.Topics[0].Hex() {
   446  
   447  			case LogAnySwapOutbyteHex.Hex():
   448  				fmt.Println("token swapped between chains ", common.HexToAddress(txlog.Topics[1].Hex()))
   449  				tokenmoved = getMultichainUnderlyingToken(common.HexToAddress(txlog.Topics[1].Hex()), fromChainIdValue.String())
   450  				// fmt.Println("underlyingtoken", underlyingtoken)
   451  				event, errUnpack := contractAbi.Unpack("LogAnySwapOut", txlog.Data)
   452  				if errUnpack != nil {
   453  					log.Fatal("unpack event LogAnySwapOut: ", errUnpack)
   454  				}
   455  				fmt.Println("------", event[0].(*big.Int))
   456  
   457  				inAmount = event[0].(*big.Int)
   458  			}
   459  		}
   460  		//putDetailsInChanel()
   461  	} else {
   462  		fmt.Println("-------Client not available for this chain---------", fromChainIdValue)
   463  		err = errors.New("client not available for chain" + fromChainIdValue.String())
   464  		return
   465  	}
   466  
   467  	return
   468  }
   469  
   470  func getMultichainUnderlyingToken(multichainTokenAddress common.Address, chainid string) (tokenAddress common.Address) {
   471  
   472  	anyerc20caller, err := anyerc20.NewAnyerc20Caller(multichainTokenAddress, restClients[chainid])
   473  	if err != nil {
   474  		log.Errorln(err)
   475  		return
   476  	}
   477  
   478  	tokenAddress, _ = anyerc20caller.Underlying(&bind.CallOpts{})
   479  
   480  	return
   481  
   482  }
   483  
   484  func InitialiseRestClientsMap() {
   485  	var err error
   486  	restClients = make(map[string]*ethclient.Client)
   487  
   488  	restClients["250"], err = ethclient.Dial(ftmHTTP)
   489  	if err != nil {
   490  		log.Fatal("init Fantom rest client: ", err)
   491  	}
   492  
   493  	restClients["1"], err = ethclient.Dial(ethHTTP)
   494  	if err != nil {
   495  		log.Fatal("init Ethereum rest client: ", err)
   496  	}
   497  
   498  	restClients["137"], err = ethclient.Dial(multichainconfigs["137"].restURL)
   499  	if err != nil {
   500  		log.Fatal("init Polygon rest client: ", err)
   501  	}
   502  	restClients["56"], err = ethclient.Dial(multichainconfigs["56"].restURL)
   503  	if err != nil {
   504  		log.Fatal("init Binance Smart Chain rest client: ", err)
   505  	}
   506  
   507  	restClients["25"], err = ethclient.Dial("https://cronosrpc-1.xstaking.sg")
   508  	if err != nil {
   509  		log.Fatal("init Cronos rest client: ", err)
   510  	}
   511  
   512  	restClients["43114"], err = ethclient.Dial("https://rpc.ankr.com/avalanche")
   513  	if err != nil {
   514  		log.Fatal("init Avalanche rest client: ", err)
   515  	}
   516  
   517  	restClients["10"], err = ethclient.Dial("https://mainnet.optimism.io")
   518  	if err != nil {
   519  		log.Fatal("init Optimism rest client: ", err)
   520  	}
   521  
   522  	restClients["1285"], err = ethclient.Dial("https://rpc.api.moonriver.moonbeam.network")
   523  	if err != nil {
   524  		log.Fatal("init Moonbeam rest client: ", err)
   525  	}
   526  
   527  	restClients["66"], err = ethclient.Dial("https://exchainrpc.okex.org")
   528  	if err != nil {
   529  		log.Fatal("init OKXChain rest client: ", err)
   530  	}
   531  
   532  	restClients["42161"], err = ethclient.Dial("https://arb1.arbitrum.io/rpc")
   533  	if err != nil {
   534  		log.Fatal("init Arbitrum rest client: ", err)
   535  	}
   536  
   537  }
   538  
   539  func InitialiseWsClientsMap() {
   540  	var err error
   541  
   542  	wsClients = make(map[string]*ethclient.Client)
   543  
   544  	for chainID, chainconfig := range multichainconfigs {
   545  		wsClients[chainID], err = ethclient.Dial(chainconfig.wsURL)
   546  		if err != nil {
   547  			log.Errorf("init ws client on chain with id %s: %v", chainID, err)
   548  		}
   549  
   550  	}
   551  
   552  }
   553  
   554  func getFilteredEvents() chan types.Log {
   555  
   556  	var channels []chan types.Log
   557  	out := make(chan types.Log)
   558  
   559  	for chainID, config := range multichainconfigs {
   560  
   561  		//filter query by block number
   562  		query := ethereum.FilterQuery{
   563  			FromBlock: big.NewInt(config.contratDeployedAtBlock),
   564  			Addresses: []common.Address{
   565  				common.HexToAddress(config.contractAddress),
   566  			},
   567  		}
   568  
   569  		events := make(chan types.Log)
   570  		_, err := wsClients[chainID].SubscribeFilterLogs(context.Background(), query, events)
   571  		if err != nil {
   572  			log.Fatal("error connecting to wsclient", err)
   573  		}
   574  
   575  		channels = append(channels, events)
   576  
   577  	}
   578  
   579  	// merge all events in single channel
   580  
   581  	for _, c := range channels {
   582  		go func(c <-chan types.Log) {
   583  			for v := range c {
   584  				out <- v
   585  			}
   586  		}(c)
   587  	}
   588  
   589  	return out
   590  }
   591  
   592  // BridgeSwapPairScraper implements PairScraper for Uniswap
   593  type BridgeSwapPairScraper struct {
   594  	parent *BridgeSwapScraper
   595  	pair   dia.ExchangePair
   596  	closed bool
   597  }
   598  
   599  func (s *BridgeSwapScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   600  
   601  	s.errorLock.RLock()
   602  	defer s.errorLock.RUnlock()
   603  	if s.error != nil {
   604  		return nil, s.error
   605  	}
   606  	if s.closed {
   607  		return nil, errors.New("UniswapScraper: Call ScrapePair on closed scraper")
   608  	}
   609  	ps := &BridgeSwapPairScraper{
   610  		parent: s,
   611  		pair:   pair,
   612  	}
   613  	s.pairScrapers[pair.ForeignName] = ps
   614  	return ps, nil
   615  
   616  }
   617  
   618  func (b *BridgeSwapScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   619  
   620  	return []dia.ExchangePair{}, nil
   621  
   622  }
   623  
   624  func (ps *BridgeSwapScraper) Channel() chan *dia.Trade {
   625  	return ps.chanTrades
   626  
   627  }
   628  
   629  func (s *BridgeSwapScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   630  	return dia.Asset{Symbol: symbol}, nil
   631  }
   632  
   633  func (up *BridgeSwapScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   634  	return pair, nil
   635  }
   636  
   637  // Close closes any existing API connections, as well as channels of
   638  // PairScrapers from calls to ScrapePair
   639  func (s *BridgeSwapScraper) Close() error {
   640  	if s.closed {
   641  		return errors.New("BridgeSwapScraper: Already closed")
   642  	}
   643  	s.WsClient.Close()
   644  	s.RestClient.Close()
   645  	close(s.shutdown)
   646  	<-s.shutdownDone
   647  	s.errorLock.RLock()
   648  	defer s.errorLock.RUnlock()
   649  	return s.error
   650  }
   651  
   652  // Close stops listening for trades of the pair associated with s
   653  func (ps *BridgeSwapPairScraper) Close() error {
   654  	ps.closed = true
   655  	return nil
   656  }
   657  
   658  // Error returns an error when the channel Channel() is closed
   659  // and nil otherwise
   660  func (ps *BridgeSwapPairScraper) Error() error {
   661  	s := ps.parent
   662  	s.errorLock.RLock()
   663  	defer s.errorLock.RUnlock()
   664  	return s.error
   665  }
   666  
   667  // Pair returns the pair this scraper is subscribed to
   668  func (ps *BridgeSwapPairScraper) Pair() dia.ExchangePair {
   669  	return ps.pair
   670  }