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 ×tamp, 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 ×tamp, 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 }