From 593b8826bd2b7b52d459c240345066a9b9c3a413 Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Thu, 16 Nov 2023 18:31:13 +0000 Subject: [PATCH] prettify /pool command output, fix problematic pools --- .env.example | 4 ++ bots/__main__.py | 3 +- bots/commander.py | 6 +- bots/data.py | 40 ++++++++----- bots/helpers.py | 7 ++- bots/settings.py | 3 + bots/ui/pool_stats.py | 134 +++++++++++++++--------------------------- tests/test_data.py | 25 ++++++++ 8 files changed, 117 insertions(+), 105 deletions(-) diff --git a/.env.example b/.env.example index a811188..a14835d 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,10 @@ DISCORD_TOKEN_PRICING= DISCORD_TOKEN_TVL= DISCORD_TOKEN_FEES= DISCORD_TOKEN_REWARDS= +DISCORD_TOKEN_COMMANDER= WEB3_PROVIDER_URI=https://mainnet.optimism.io PROTOCOL_NAME=Velodrome +APP_BASE_URL=https://velodrome.finance LP_SUGAR_ADDRESS=0xa1F09427fa89b92e9B4e4c7003508C8614F19791 PRICE_ORACLE_ADDRESS=0x07F544813E9Fb63D57a92f28FbD3FF0f7136F5cE PRICE_BATCH_SIZE=40 @@ -16,5 +18,7 @@ STABLE_TOKEN_ADDRESS=0x7F5c764cBc14f9669B88837ca1490cCa17c31607 BOT_TICKER_INTERVAL_MINUTES=1 # caching for Sugar tokens SUGAR_TOKENS_CACHE_MINUTES=10 +# caching for Sugar LPs +SUGAR_LPS_CACHE_MINUTES=10 # caching for oracle pricing ORACLE_PRICES_CACHE_MINUTES=10 \ No newline at end of file diff --git a/bots/__main__.py b/bots/__main__.py index 792323c..520c11c 100644 --- a/bots/__main__.py +++ b/bots/__main__.py @@ -6,6 +6,7 @@ DISCORD_TOKEN_TVL, DISCORD_TOKEN_FEES, DISCORD_TOKEN_REWARDS, + DISCORD_TOKEN_COMMANDER, TOKEN_ADDRESS, STABLE_TOKEN_ADDRESS, PROTOCOL_NAME, @@ -44,7 +45,7 @@ async def main(): fees_bot.start(DISCORD_TOKEN_FEES), tvl_bot.start(DISCORD_TOKEN_TVL), rewards_bot.start(DISCORD_TOKEN_REWARDS), - commander_bot.start(DISCORD_TOKEN_TVL), + commander_bot.start(DISCORD_TOKEN_COMMANDER), ) diff --git a/bots/commander.py b/bots/commander.py index a8829e8..ecfd6e7 100644 --- a/bots/commander.py +++ b/bots/commander.py @@ -35,9 +35,9 @@ async def on_select_pool( else address_or_pool ) tvl = await LiquidityPool.tvl([pool]) - embed = await PoolStats().render(pool, tvl) - - await response.send_message(embed=embed) + await response.send_message( + await PoolStats().render(pool, tvl), suppress_embeds=True + ) @bot.tree.command(name="pool", description="Get data for specific pool") diff --git a/bots/data.py b/bots/data.py index c00b47f..930bc8a 100644 --- a/bots/data.py +++ b/bots/data.py @@ -4,7 +4,7 @@ from web3 import AsyncWeb3, AsyncHTTPProvider from web3.constants import ADDRESS_ZERO from dataclasses import dataclass -from typing import Tuple, List, Dict +from typing import Tuple, List, Dict, Optional from .settings import ( WEB3_PROVIDER_URI, @@ -15,6 +15,7 @@ CONNECTOR_TOKENS_ADDRESSES, STABLE_TOKEN_ADDRESS, SUGAR_TOKENS_CACHE_MINUTES, + SUGAR_LPS_CACHE_MINUTES, ORACLE_PRICES_CACHE_MINUTES, PRICE_BATCH_SIZE, GOOD_ENOUGH_PAGINATION_LIMIT, @@ -53,16 +54,20 @@ def from_tuple(cls, t: Tuple) -> "Token": @classmethod @cache_in_seconds(SUGAR_TOKENS_CACHE_MINUTES * 60) async def get_all_listed_tokens(cls) -> List["Token"]: + tokens = await cls.get_all_tokens() + return list(filter(lambda t: t.listed, tokens)) + + @classmethod + @cache_in_seconds(SUGAR_TOKENS_CACHE_MINUTES * 60) + async def get_all_tokens(cls) -> List["Token"]: sugar = w3.eth.contract(address=LP_SUGAR_ADDRESS, abi=LP_SUGAR_ABI) tokens = await sugar.functions.tokens( GOOD_ENOUGH_PAGINATION_LIMIT, 0, ADDRESS_ZERO ).call() - return list( - filter(lambda t: t.listed, map(lambda t: Token.from_tuple(t), tokens)) - ) + return list(map(lambda t: Token.from_tuple(t), tokens)) @classmethod - async def get_by_token_address(cls, token_address: str) -> "Token": + async def get_by_token_address(cls, token_address: str) -> Optional["Token"]: """Get details for specific token Args: @@ -238,8 +243,9 @@ def from_tuple( ) @classmethod + @cache_in_seconds(SUGAR_LPS_CACHE_MINUTES * 60) async def get_pools(cls) -> List["LiquidityPool"]: - tokens = await Token.get_all_listed_tokens() + tokens = await Token.get_all_tokens() prices = await Price.get_prices(tokens) tokens = {t.token_address: t for t in tokens} @@ -257,6 +263,7 @@ async def get_pools(cls) -> List["LiquidityPool"]: ) @classmethod + @cache_in_seconds(SUGAR_LPS_CACHE_MINUTES * 60) async def by_address(cls, address: str) -> "LiquidityPool": pools = await cls.get_pools() try: @@ -317,25 +324,29 @@ def volume_pct(self) -> float: @property def volume(self) -> float: - return self.volume_pct * ( - self.token0_fees.amount_in_stable + self.token1_fees.amount_in_stable - ) + t0 = self.token0_fees.amount_in_stable if self.token0_fees else 0 + t1 = self.token1_fees.amount_in_stable if self.token1_fees else 0 + return self.volume_pct * (t0 + t1) @property def token0_volume(self) -> float: - return self.token0_fees.amount * self.volume_pct + return self.token0_fees.amount * self.volume_pct if self.token0_fees else 0 @property def token1_volume(self) -> float: - return self.token1_fees.amount * self.volume_pct + return self.token1_fees.amount * self.volume_pct if self.token1_fees else 0 def apr(self, tvl: float) -> float: day_seconds = 24 * 60 * 60 - reward_value = self.emissions.amount_in_stable + reward_value = self.emissions.amount_in_stable if self.emissions else 0 reward = reward_value * day_seconds - staked_pct = 100 * self.gauge_total_supply / self.total_supply + staked_pct = ( + 100 * self.gauge_total_supply / self.total_supply + if self.total_supply != 0 + else 0 + ) staked_tvl = tvl * staked_pct / 100 - return (reward / staked_tvl) * (100 * 365) + return (reward / staked_tvl) * (100 * 365) if staked_tvl != 0 else 0 @dataclass(frozen=True) @@ -350,6 +361,7 @@ class LiquidityPoolEpoch: fees: List[Amount] @classmethod + @cache_in_seconds(SUGAR_LPS_CACHE_MINUTES * 60) async def fetch_latest(cls): tokens = await Token.get_all_listed_tokens() prices = await Price.get_prices(tokens) diff --git a/bots/helpers.py b/bots/helpers.py index 85b4ef5..dba9049 100644 --- a/bots/helpers.py +++ b/bots/helpers.py @@ -1,8 +1,9 @@ import logging import os import sys +import urllib -from typing import List +from typing import List, Dict from web3 import Web3 from async_lru import alru_cache @@ -53,6 +54,10 @@ def amount_to_m_string(amount: float) -> str: return f"{round(amount/1000000, 2)}M" +def make_app_url(base_url: str, path: str, params: Dict) -> str: + return f"{base_url}{path}?{urllib.parse.urlencode(params)}" + + # logging LOGGING_LEVEL = os.getenv("LOGGING_LEVEL", "DEBUG") LOGGING_HANDLER = logging.StreamHandler(sys.stdout) diff --git a/bots/settings.py b/bots/settings.py index 2fa45d3..0cad638 100644 --- a/bots/settings.py +++ b/bots/settings.py @@ -12,6 +12,7 @@ DISCORD_TOKEN_TVL = os.environ["DISCORD_TOKEN_TVL"] DISCORD_TOKEN_FEES = os.environ["DISCORD_TOKEN_FEES"] DISCORD_TOKEN_REWARDS = os.environ["DISCORD_TOKEN_REWARDS"] +DISCORD_TOKEN_COMMANDER = os.environ["DISCORD_TOKEN_COMMANDER"] WEB3_PROVIDER_URI = os.environ["WEB3_PROVIDER_URI"] LP_SUGAR_ADDRESS = os.environ["LP_SUGAR_ADDRESS"] @@ -19,6 +20,7 @@ PRICE_BATCH_SIZE = int(os.environ["PRICE_BATCH_SIZE"]) PROTOCOL_NAME = os.environ["PROTOCOL_NAME"] +APP_BASE_URL = os.environ["APP_BASE_URL"] # token we are converting from TOKEN_ADDRESS = normalize_address(os.environ["TOKEN_ADDRESS"]) @@ -35,6 +37,7 @@ BOT_TICKER_INTERVAL_MINUTES = int(os.environ["BOT_TICKER_INTERVAL_MINUTES"]) SUGAR_TOKENS_CACHE_MINUTES = int(os.environ["SUGAR_TOKENS_CACHE_MINUTES"]) +SUGAR_LPS_CACHE_MINUTES = int(os.environ["SUGAR_LPS_CACHE_MINUTES"]) ORACLE_PRICES_CACHE_MINUTES = int(os.environ["ORACLE_PRICES_CACHE_MINUTES"]) GOOD_ENOUGH_PAGINATION_LIMIT = 2000 diff --git a/bots/ui/pool_stats.py b/bots/ui/pool_stats.py index ec94a89..cbddfc6 100644 --- a/bots/ui/pool_stats.py +++ b/bots/ui/pool_stats.py @@ -1,91 +1,53 @@ -import discord from ..data import LiquidityPool -from ..helpers import format_percentage, format_currency +from ..helpers import format_percentage, format_currency, make_app_url +from ..settings import APP_BASE_URL class PoolStats: - async def render(self, pool: LiquidityPool, tvl: float): - embed = discord.Embed( - title=f"{pool.symbol}", - description=" | ".join( - [ - f"{'Stable Pool' if pool.is_stable else 'Volatile Pool'}", - f"Trading fee: {format_percentage(pool.pool_fee_percentage)}", - f"TVL: ~{format_currency(tvl)}", - f"APR: {format_percentage(pool.apr(tvl))}", - ] - ), - color=0xFFFFFF, - ) - - embed.add_field(name="", value="", inline=False) - - # Volume - - embed.add_field(name="Volume", value="", inline=False) - embed.add_field( - name=" ", - value=format_currency(pool.volume), - inline=True, - ) - embed.add_field( - name=" ", - value=format_currency( - pool.token0_volume, symbol=pool.token0.symbol, prefix=False - ), - inline=True, - ) - embed.add_field( - name=" ", - value=format_currency( - pool.token1_volume, symbol=pool.token1.symbol, prefix=False - ), - inline=True, - ) - embed.add_field(name="", value="", inline=False) - - # Fees - - embed.add_field(name="Fees", value="", inline=False) - embed.add_field( - name=" ", - value=format_currency( - pool.token0_fees.amount_in_stable + pool.token1_fees.amount_in_stable - ), - inline=True, - ) - embed.add_field( - name=" ", - value=format_currency( - pool.token0_fees.amount, symbol=pool.token0.symbol, prefix=False - ), - inline=True, - ) - embed.add_field( - name=" ", - value=format_currency( - pool.token1_fees.amount, symbol=pool.token1.symbol, prefix=False - ), - inline=True, - ) - embed.add_field(name="", value="", inline=False) - - # Pool balance - - embed.add_field(name="Pool Balance", value="", inline=False) - embed.add_field( - name=" ", - value=format_currency( - pool.reserve0.amount, symbol=pool.token0.symbol, prefix=False - ), - inline=True, + async def render(self, pool: LiquidityPool, tvl: float) -> str: + token0_fees = pool.token0_fees.amount_in_stable if pool.token0_fees else 0 + token1_fees = pool.token1_fees.amount_in_stable if pool.token1_fees else 0 + + template_args = { + "pool_symbol": pool.symbol, + "pool_fee_percentage": format_percentage(pool.pool_fee_percentage), + "apr": format_percentage(pool.apr(tvl)), + "tvl": format_currency(tvl), + "token0_volume": format_currency( + pool.reserve0.amount if pool.reserve0 else 0, + symbol=pool.token0.symbol, + prefix=False, + ), + "token1_volume": format_currency( + pool.reserve1.amount if pool.reserve1 else 0, + symbol=pool.token1.symbol, + prefix=False, + ), + "volume": format_currency(pool.volume), + "fees": format_currency(token0_fees + token1_fees), + "deposit_url": make_app_url( + APP_BASE_URL, + "/deposit", + { + "token0": pool.token0.token_address, + "token1": pool.token1.token_address, + "stable": str(pool.is_stable).lower(), + }, + ), + "incentivize_url": make_app_url( + APP_BASE_URL, "/incentivize", {"pool": pool.lp} + ), + } + + return """ +> **{pool_symbol} ● Fee {pool_fee_percentage} ● {apr} APR** +> - ~{tvl} TVL +> - {token0_volume} +> - {token1_volume} +> - ~{volume} volume this epoch +> - ~{fees} fees this epoch +> +> [Deposit 🐖]({deposit_url}) ● [Incentivize 🙋]({incentivize_url}) +""".format( + **template_args ) - embed.add_field( - name=" ", - value=format_currency( - pool.reserve1.amount, symbol=pool.token1.symbol, prefix=False - ), - inline=True, - ) - - return embed diff --git a/tests/test_data.py b/tests/test_data.py index e134b9d..f5d5363 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -45,3 +45,28 @@ async def test_rewards(): assert fees != 0 assert bribes != 0 + + +@pytest.mark.asyncio +async def test_liquidity_pool_stats(): + pools = await LiquidityPool.get_pools() + for pool in pools: + tvl = await LiquidityPool.tvl([pool]) + fields = [ + pool.token0, + pool.token1, + pool.is_stable, + pool.pool_fee_percentage, + pool.apr(tvl), + pool.volume, + pool.token0_volume, + pool.token1_volume, + pool.token0_fees.amount_in_stable if pool.token0_fees else 0, + pool.token1_fees.amount_in_stable if pool.token1_fees else 0, + pool.token0_fees.amount if pool.token0_fees else 0, + pool.token1_fees.amount if pool.token1_fees else 0, + pool.reserve0.amount if pool.reserve0 else 0, + pool.reserve1.amount if pool.reserve1 else 0, + ] + for field in fields: + assert field is not None