github.com/diadata-org/diadata@v1.4.593/pkg/model/pools.go (about)

     1  package models
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	clientInfluxdb "github.com/influxdata/influxdb1-client/v2"
    14  	"github.com/jackc/pgx/v4"
    15  )
    16  
    17  // SavePoolInflux stores a DEX pool in influx.
    18  func (datastore *DB) SavePoolInflux(p dia.Pool) error {
    19  
    20  	assetvolumesEncoded, err := json.Marshal(p.Assetvolumes)
    21  	if err != nil {
    22  		log.Error("marshal volumes: ", err)
    23  	}
    24  
    25  	// Create a point and add to batch
    26  	tags := map[string]string{
    27  		"exchange":   p.Exchange.Name,
    28  		"blockchain": p.Blockchain.Name,
    29  		"address":    p.Address,
    30  	}
    31  	fields := map[string]interface{}{
    32  		"volumes": string(assetvolumesEncoded),
    33  	}
    34  
    35  	pt, err := clientInfluxdb.NewPoint(influxDbDEXPoolTable, tags, fields, p.Time)
    36  	if err != nil {
    37  		log.Errorln("NewTradeInflux:", err)
    38  	} else {
    39  		datastore.addPoint(pt)
    40  	}
    41  
    42  	err = datastore.WriteBatchInflux()
    43  	if err != nil {
    44  		log.Errorln("Write influx batch: ", err)
    45  	}
    46  
    47  	return err
    48  }
    49  
    50  // GetPoolInflux returns all info/liquidities of pool with @poolAddress in the time-range [starttime, endtime).
    51  func (datastore *DB) GetPoolInflux(poolAddress string, starttime time.Time, endtime time.Time) ([]dia.Pool, error) {
    52  
    53  	pools := []dia.Pool{}
    54  	queryString := "SELECT \"exchange\",\"blockchain\",volumes FROM %s WHERE address='%s' AND time >= %d AND time < %d ORDER BY DESC"
    55  	q := fmt.Sprintf(queryString, influxDbDEXPoolTable, poolAddress, starttime.UnixNano(), endtime.UnixNano())
    56  
    57  	res, err := queryInfluxDB(datastore.influxClient, q)
    58  	if err != nil {
    59  		return pools, err
    60  	}
    61  	if len(res) > 0 && len(res[0].Series) > 0 {
    62  		for i := 0; i < len(res[0].Series[0].Values); i++ {
    63  			var pool dia.Pool
    64  			pool.Time, err = time.Parse(time.RFC3339, res[0].Series[0].Values[i][0].(string))
    65  			if err != nil {
    66  				return pools, err
    67  			}
    68  			pool.Exchange.Name = res[0].Series[0].Values[i][1].(string)
    69  			if err != nil {
    70  				return pools, err
    71  			}
    72  			pool.Blockchain.Name = res[0].Series[0].Values[i][2].(string)
    73  			stat := res[0].Series[0].Values[i][3].(string)
    74  			if err := json.Unmarshal([]byte(stat), &pool.Assetvolumes); err != nil {
    75  				log.Error("unmarshal: ", err)
    76  			}
    77  			pool.Address = poolAddress
    78  			pools = append(pools, pool)
    79  		}
    80  	} else {
    81  		return pools, errors.New("parsing pool from database")
    82  	}
    83  	return pools, nil
    84  }
    85  
    86  // SetPool writes pool data into pool table and the underlying asset and liquidity data into the poolasset table.
    87  func (rdb *RelDB) SetPool(pool dia.Pool) error {
    88  	if len(pool.Assetvolumes) < 2 {
    89  		return errors.New("not enough asset data on pool")
    90  	}
    91  
    92  	query0 := fmt.Sprintf(
    93  		`INSERT INTO %s (exchange,blockchain,address) VALUES ($1,$2,$3) ON CONFLICT (blockchain,address) DO NOTHING`,
    94  		poolTable,
    95  	)
    96  	_, err := rdb.postgresClient.Exec(
    97  		context.Background(),
    98  		query0,
    99  		pool.Exchange.Name,
   100  		pool.Blockchain.Name,
   101  		pool.Address,
   102  	)
   103  	if err != nil {
   104  		if !strings.Contains(err.Error(), "duplicate") {
   105  			return err
   106  		} else {
   107  			log.Warn("pool already exists, update liquidity")
   108  		}
   109  	}
   110  
   111  	// Add assets and liquidity to the underlying poolasset table.
   112  	var query1 string
   113  	for i := 0; i < len(pool.Assetvolumes); i++ {
   114  		query1 = fmt.Sprintf(
   115  			`INSERT INTO %s (pool_id,asset_id,liquidity,liquidity_usd,time_stamp,token_index)
   116  				VALUES (
   117  					(SELECT pool_id from %s where address=$1 and blockchain=$2),
   118  				    (SELECT asset_id from %s where address=$3 and blockchain=$4),$5,$6,$7,$8
   119  				)
   120  				ON CONFLICT (pool_id,asset_id)
   121  				DO UPDATE
   122  				    SET liquidity=EXCLUDED.liquidity,
   123  				    	liquidity_usd=EXCLUDED.liquidity_usd,
   124  				    	time_stamp=EXCLUDED.time_stamp,
   125  				    	token_index=EXCLUDED.token_index`,
   126  			poolassetTable,
   127  			poolTable,
   128  			assetTable,
   129  		)
   130  		_, err := rdb.postgresClient.Exec(
   131  			context.Background(),
   132  			query1,
   133  			pool.Address,
   134  			pool.Blockchain.Name,
   135  			pool.Assetvolumes[i].Asset.Address,
   136  			pool.Assetvolumes[i].Asset.Blockchain,
   137  			pool.Assetvolumes[i].Volume,
   138  			pool.Assetvolumes[i].VolumeUSD,
   139  			pool.Time,
   140  			pool.Assetvolumes[i].Index,
   141  		)
   142  		if err != nil {
   143  			return err
   144  		}
   145  	}
   146  	return nil
   147  }
   148  
   149  // GetAllDEXPoolsCount returns a map which maps a DEX onto the number of pools on the DEX.
   150  func (rdb *RelDB) GetAllDEXPoolsCount() (map[string]int, error) {
   151  	poolsCount := make(map[string]int)
   152  
   153  	query := fmt.Sprintf("SELECT exchange,COUNT(address) FROM %s GROUP BY exchange", poolTable)
   154  	rows, err := rdb.postgresClient.Query(context.Background(), query)
   155  	if err != nil {
   156  		return poolsCount, err
   157  	}
   158  	defer rows.Close()
   159  	for rows.Next() {
   160  		var exchange string
   161  		var numPools int
   162  		err = rows.Scan(
   163  			&exchange,
   164  			&numPools,
   165  		)
   166  		if err != nil {
   167  			return poolsCount, err
   168  		}
   169  		poolsCount[exchange] = numPools
   170  	}
   171  	return poolsCount, nil
   172  }
   173  
   174  // GetPoolByAddress returns the most recent pool data, i.e. liquidity.
   175  func (rdb *RelDB) GetPoolByAddress(blockchain string, address string) (pool dia.Pool, err error) {
   176  
   177  	var rows pgx.Rows
   178  	query := fmt.Sprintf(`
   179  		SELECT pa.liquidity,pa.liquidity_usd,a.symbol,a.name,a.address,a.decimals,p.exchange,pa.time_stamp,pa.token_index
   180  		FROM %s pa
   181  		INNER JOIN %s p
   182  		ON p.pool_id=pa.pool_id
   183  		INNER JOIN %s a
   184  		ON pa.asset_id=a.asset_id
   185  		WHERE p.blockchain=$1
   186  		AND p.address=$2
   187  		ORDER BY pa.token_index ASC
   188  		`,
   189  		poolassetTable,
   190  		poolTable,
   191  		assetTable,
   192  	)
   193  
   194  	rows, err = rdb.postgresClient.Query(context.Background(), query, blockchain, address)
   195  	if err != nil {
   196  		return
   197  	}
   198  	defer rows.Close()
   199  
   200  	for rows.Next() {
   201  		var (
   202  			decimals     sql.NullInt64
   203  			index        sql.NullInt64
   204  			timestamp    sql.NullTime
   205  			liquidity    sql.NullFloat64
   206  			liquidityUSD sql.NullFloat64
   207  			assetvolume  dia.AssetVolume
   208  		)
   209  		err = rows.Scan(
   210  			&liquidity,
   211  			&liquidityUSD,
   212  			&assetvolume.Asset.Symbol,
   213  			&assetvolume.Asset.Name,
   214  			&assetvolume.Asset.Address,
   215  			&decimals,
   216  			&pool.Exchange.Name,
   217  			&timestamp,
   218  			&index,
   219  		)
   220  		if err != nil {
   221  			return
   222  		}
   223  		if decimals.Valid {
   224  			assetvolume.Asset.Decimals = uint8(decimals.Int64)
   225  		}
   226  		if index.Valid {
   227  			assetvolume.Index = uint8(index.Int64)
   228  		}
   229  		if timestamp.Valid {
   230  			pool.Time = timestamp.Time
   231  		}
   232  		if liquidity.Valid {
   233  			assetvolume.Volume = liquidity.Float64
   234  		}
   235  		if liquidityUSD.Valid {
   236  			assetvolume.VolumeUSD = liquidityUSD.Float64
   237  		}
   238  		assetvolume.Asset.Blockchain = blockchain
   239  		pool.Assetvolumes = append(pool.Assetvolumes, assetvolume)
   240  	}
   241  
   242  	pool.Blockchain.Name = blockchain
   243  	pool.Address = address
   244  
   245  	return
   246  }
   247  
   248  // GetAllPoolAddrsExchange returns all pool addresses available for @exchange.
   249  func (rdb *RelDB) GetAllPoolAddrsExchange(exchange string, liquiThreshold float64) (addresses []string, err error) {
   250  	var (
   251  		rows  pgx.Rows
   252  		query string
   253  	)
   254  	if liquiThreshold == float64(0) {
   255  		query = fmt.Sprintf("SELECT address FROM %s WHERE exchange=$1", poolTable)
   256  		rows, err = rdb.postgresClient.Query(context.Background(), query, exchange)
   257  	} else {
   258  		query = fmt.Sprintf(`
   259  		SELECT DISTINCT p.address
   260  		FROM %s p
   261  		INNER JOIN %s pa
   262  		ON p.pool_id=pa.pool_id
   263  		WHERE p.exchange=$1
   264  		AND pa.liquidity>=$2
   265  		`, poolTable, poolassetTable)
   266  		rows, err = rdb.postgresClient.Query(context.Background(), query, exchange, liquiThreshold)
   267  	}
   268  	if err != nil {
   269  		return
   270  	}
   271  	defer rows.Close()
   272  
   273  	for rows.Next() {
   274  		var poolAddr string
   275  		err := rows.Scan(&poolAddr)
   276  		if err != nil {
   277  			log.Error(err)
   278  		}
   279  		addresses = append(addresses, poolAddr)
   280  	}
   281  	return
   282  }
   283  
   284  // GetAllPoolsExchange returns all pool addresses available for @exchange.
   285  // Remark that it returns each pool n times where n is the number of assets in the pool.
   286  func (rdb *RelDB) GetAllPoolsExchange(exchange string, liquiThreshold float64) (pools []dia.Pool, err error) {
   287  	var (
   288  		rows  pgx.Rows
   289  		query string
   290  	)
   291  
   292  	query = fmt.Sprintf(`
   293  		SELECT exch_pools.address,a.address,a.blockchain,a.decimals,a.symbol,a.name,pa.token_index,pa.liquidity,pa.liquidity_usd
   294  		FROM (
   295  			SELECT p.pool_id,p.address, SUM(CASE WHEN pa.liquidity<$1 THEN 1 ELSE 0 END) AS no_liqui
   296  			FROM %s p
   297  			INNER JOIN %s pa
   298  			ON p.pool_id=pa.pool_id
   299  			WHERE p.exchange=$2
   300  			GROUP BY p.pool_id,p.address
   301  			) exch_pools
   302  		INNER JOIN %s pa
   303  		ON exch_pools.pool_id=pa.pool_id
   304  		INNER JOIN %s a
   305  		ON pa.asset_id=a.asset_id
   306  		WHERE exch_pools.no_liqui=0
   307  		ORDER BY pa.token_index ASC;
   308  	`,
   309  		poolTable,
   310  		poolassetTable,
   311  		poolassetTable,
   312  		assetTable,
   313  	)
   314  	rows, err = rdb.postgresClient.Query(context.Background(), query, liquiThreshold, exchange)
   315  	if err != nil {
   316  		return
   317  	}
   318  	defer rows.Close()
   319  
   320  	poolIndexMap := make(map[string]int)
   321  
   322  	for rows.Next() {
   323  		var (
   324  			poolAddress  string
   325  			av           dia.AssetVolume
   326  			decimals     sql.NullInt64
   327  			index        sql.NullInt64
   328  			liquidity    sql.NullFloat64
   329  			liquidityUSD sql.NullFloat64
   330  		)
   331  		err := rows.Scan(
   332  			&poolAddress,
   333  			&av.Asset.Address,
   334  			&av.Asset.Blockchain,
   335  			&decimals,
   336  			&av.Asset.Symbol,
   337  			&av.Asset.Name,
   338  			&index,
   339  			&liquidity,
   340  			&liquidityUSD,
   341  		)
   342  		if err != nil {
   343  			log.Error(err)
   344  		}
   345  		if decimals.Valid {
   346  			av.Asset.Decimals = uint8(decimals.Int64)
   347  		}
   348  		if index.Valid {
   349  			av.Index = uint8(index.Int64)
   350  		}
   351  		if liquidity.Valid {
   352  			av.Volume = liquidity.Float64
   353  		}
   354  		if liquidityUSD.Valid {
   355  			av.VolumeUSD = liquidityUSD.Float64
   356  		}
   357  
   358  		// map poolasset to pool if pool address already exists.
   359  		if _, ok := poolIndexMap[poolAddress]; !ok {
   360  			// Pool does not exist yet, so initialize.
   361  			pool := dia.Pool{Exchange: dia.Exchange{Name: exchange}, Address: poolAddress, Blockchain: dia.BlockChain{Name: av.Asset.Blockchain}}
   362  			pool.Assetvolumes = append(pool.Assetvolumes, av)
   363  			pools = append(pools, pool)
   364  			poolIndexMap[poolAddress] = len(pools) - 1
   365  		} else {
   366  			// Pool already exists, just add pool asset.
   367  			pools[poolIndexMap[poolAddress]].Assetvolumes = append(pools[poolIndexMap[poolAddress]].Assetvolumes, av)
   368  		}
   369  
   370  	}
   371  	return
   372  }
   373  
   374  // GetPoolsByAsset returns all pools with @asset as a pool asset and both assets have liquidity above @liquiThreshold.
   375  // If @liquidityThresholdUSD>0 AND @liquiThreshold=0, only pools where total liquidity is available
   376  // AND above @liquidityThresholdUSD are returned.
   377  func (rdb *RelDB) GetPoolsByAsset(asset dia.Asset, liquidityThreshold float64, liquidityThresholdUSD float64) ([]dia.Pool, error) {
   378  	var (
   379  		query string
   380  		pools []dia.Pool
   381  	)
   382  
   383  	query = fmt.Sprintf(`
   384  		SELECT exch_pools.exchange,exch_pools.address,a.address,a.blockchain,a.decimals,a.symbol,a.name,pa.token_index,pa.liquidity,pa.liquidity_usd,pa.time_stamp
   385  		FROM (
   386  			SELECT p.exchange,p.pool_id,p.address, SUM(CASE WHEN pa.liquidity>=$1 THEN 0 ELSE 1 END) AS no_liqui, SUM(CASE WHEN a.address=$2 THEN 1 ELSE 0 END) AS correct_asset
   387  			FROM %s p
   388  			INNER JOIN %s pa
   389  			ON p.pool_id=pa.pool_id
   390  			INNER JOIN %s a
   391  			ON pa.asset_id=a.asset_id
   392  			WHERE p.blockchain=$3
   393  			GROUP BY p.exchange,p.pool_id,p.address
   394  			) exch_pools
   395  		INNER JOIN %s pa
   396  		ON exch_pools.pool_id=pa.pool_id
   397  		INNER JOIN %s a ON pa.asset_id=a.asset_id
   398  		WHERE exch_pools.no_liqui=0
   399  		AND exch_pools.correct_asset=1
   400  		AND pa.time_stamp IS NOT NULL;
   401  	`,
   402  		poolTable,
   403  		poolassetTable,
   404  		assetTable,
   405  		poolassetTable,
   406  		assetTable,
   407  	)
   408  	rows, err := rdb.postgresClient.Query(context.Background(), query, liquidityThreshold, asset.Address, asset.Blockchain)
   409  	if err != nil {
   410  		return pools, err
   411  	}
   412  	defer rows.Close()
   413  
   414  	poolIndexMap := make(map[string]int)
   415  
   416  	for rows.Next() {
   417  		var (
   418  			exchange     string
   419  			poolAddress  string
   420  			av           dia.AssetVolume
   421  			decimals     sql.NullInt64
   422  			index        sql.NullInt64
   423  			liquidity    sql.NullFloat64
   424  			liquidityUSD sql.NullFloat64
   425  			timestamp    sql.NullTime
   426  		)
   427  		err := rows.Scan(
   428  			&exchange,
   429  			&poolAddress,
   430  			&av.Asset.Address,
   431  			&av.Asset.Blockchain,
   432  			&decimals,
   433  			&av.Asset.Symbol,
   434  			&av.Asset.Name,
   435  			&index,
   436  			&liquidity,
   437  			&liquidityUSD,
   438  			&timestamp,
   439  		)
   440  		if err != nil {
   441  			log.Error(err)
   442  		}
   443  		if decimals.Valid {
   444  			av.Asset.Decimals = uint8(decimals.Int64)
   445  		}
   446  		if index.Valid {
   447  			av.Index = uint8(index.Int64)
   448  		}
   449  		if liquidity.Valid {
   450  			av.Volume = liquidity.Float64
   451  		}
   452  		if liquidityUSD.Valid {
   453  			av.VolumeUSD = liquidityUSD.Float64
   454  		}
   455  
   456  		// map poolasset to pool if pool address already exists.
   457  		if _, ok := poolIndexMap[poolAddress]; !ok {
   458  			// Pool does not exist yet, so initialize.
   459  			pool := dia.Pool{Exchange: dia.Exchange{Name: exchange}, Address: poolAddress, Blockchain: dia.BlockChain{Name: av.Asset.Blockchain}}
   460  			if timestamp.Valid {
   461  				pool.Time = timestamp.Time
   462  			}
   463  			pool.Assetvolumes = append(pool.Assetvolumes, av)
   464  			pools = append(pools, pool)
   465  			poolIndexMap[poolAddress] = len(pools) - 1
   466  		} else {
   467  			// Pool already exists, just add pool asset.
   468  			pools[poolIndexMap[poolAddress]].Assetvolumes = append(pools[poolIndexMap[poolAddress]].Assetvolumes, av)
   469  		}
   470  
   471  	}
   472  
   473  	if liquidityThresholdUSD > 0 {
   474  		var filteredPools []dia.Pool
   475  		for _, pool := range pools {
   476  			totalLiquidity, lowerBound := pool.GetPoolLiquidityUSD()
   477  			if totalLiquidity > liquidityThresholdUSD && !lowerBound {
   478  				filteredPools = append(filteredPools, pool)
   479  			}
   480  		}
   481  		return filteredPools, nil
   482  	}
   483  
   484  	return pools, nil
   485  }
   486  
   487  // GetPoolLiquiditiesUSD attempts to fill the field @VolumeUSD by fetching the price
   488  // of the corresponding asset.
   489  // @priceCache acts as a poor man's cache for repeated requests.
   490  func (datastore *DB) GetPoolLiquiditiesUSD(p *dia.Pool, priceCache map[string]float64) {
   491  	for i, av := range p.Assetvolumes {
   492  		var price float64
   493  		// For some pools, for instance on BalancerV2 type contracts, the pool contains itself as an asset.
   494  		if av.Asset.Address == p.Address {
   495  			log.Warnf("%s: Pool token %s has the same address as pool itself.", p.Exchange.Name, p.Address)
   496  			continue
   497  		}
   498  		if _, ok := priceCache[av.Asset.Identifier()]; !ok {
   499  			assetQuotation, err := datastore.GetAssetQuotationLatest(av.Asset, time.Now().Add(-time.Duration(assetQuotationLookbackHours)*time.Hour))
   500  			if err != nil {
   501  				log.Errorf("GetAssetQuotationLatest on %s with address %s: %v", av.Asset.Blockchain, av.Asset.Address, err)
   502  				continue
   503  			}
   504  			price = assetQuotation.Price
   505  			priceCache[av.Asset.Identifier()] = price
   506  		} else {
   507  			price = priceCache[av.Asset.Identifier()]
   508  		}
   509  		p.Assetvolumes[i].VolumeUSD = price * p.Assetvolumes[i].Volume
   510  	}
   511  }