From f6cf1bd3944beca7f3e54bfdeb254df4a5db83a8 Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Wed, 15 Nov 2023 15:56:23 +0000 Subject: [PATCH 1/6] WIP --- bots/__main__.py | 183 ++++++++++++++++++++++++++++++++++++++++++++--- bots/data.py | 140 +++++++++++++++++++++++++----------- bots/helpers.py | 9 +++ 3 files changed, 280 insertions(+), 52 deletions(-) diff --git a/bots/__main__.py b/bots/__main__.py index 60ff656..7f389eb 100644 --- a/bots/__main__.py +++ b/bots/__main__.py @@ -10,14 +10,176 @@ STABLE_TOKEN_ADDRESS, PROTOCOL_NAME, ) -from .data import Token -from .helpers import LOGGING_HANDLER, LOGGING_LEVEL +from .data import Token, LiquidityPool +from .helpers import LOGGING_HANDLER, LOGGING_LEVEL, format_currency, format_percentage from .price import PriceBot from .tvl import TVLBot from .fees import FeesBot from .rewards import RewardsBot +import discord +from discord.ext import commands + +intents = discord.Intents.default() +intents.message_content = True + + +# Defines a custom Select containing colour options +# that the user can choose. The callback function +# of this class is called when the user changes their choice +class Dropdown(discord.ui.Select): + def __init__(self): + # Set the options that will be presented inside the dropdown + options = [ + discord.SelectOption( + label="Red", description="Your favourite colour is red", emoji="🟥" + ), + discord.SelectOption( + label="Green", description="Your favourite colour is green", emoji="🟩" + ), + discord.SelectOption( + label="Blue", description="Your favourite colour is blue", emoji="🟦" + ), + ] + + # The placeholder is what will be shown when no option is chosen + # The min and max values indicate we can only pick one of the three options + # The options parameter defines the dropdown options. We defined this above + super().__init__( + placeholder="Choose your favourite colour...", + min_values=1, + max_values=1, + options=options, + ) + + async def callback(self, interaction: discord.Interaction): + # Use the interaction object to send a response message containing + # the user's favourite colour or choice. The self object refers to the + # Select object, and the values attribute gets a list of the user's + # selected options. We only want the first one. + await interaction.response.send_message( + f"Your favourite colour is {self.values[0]}" + ) + + +class DropdownView(discord.ui.View): + def __init__(self): + super().__init__() + + # Adds the dropdown to our view object. + self.add_item(Dropdown()) + + +bot = commands.Bot(command_prefix=commands.when_mentioned_or("/"), intents=intents) + + +@bot.command() +async def pool(ctx, *args): + print(">>>>>>>>>>>>>>>>>> test command") + + # Create the view containing our dropdown + view = DropdownView() + + # Sending a message containing our view + await ctx.send("Choose your pool:", view=view) + + if len(args) == 0: + await ctx.send("Missing pool address: should be /pool POOL_ADDRESS_HERE") + else: + pool = await LiquidityPool.by_address(args[0]) + if pool is None: + await ctx.send("Nothing found") + else: + print(pool) + tvl = await LiquidityPool.tvl([pool]) + embedVar = 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, + ) + + embedVar.add_field(name="", value="", inline=False) + + # Volume + + embedVar.add_field(name="Volume", value="", inline=False) + embedVar.add_field( + name=" ", + value=format_currency(pool.volume), + inline=True, + ) + embedVar.add_field( + name=" ", + value=format_currency( + pool.token0_volume, symbol=pool.token0.symbol, prefix=False + ), + inline=True, + ) + embedVar.add_field( + name=" ", + value=format_currency( + pool.token1_volume, symbol=pool.token1.symbol, prefix=False + ), + inline=True, + ) + embedVar.add_field(name="", value="", inline=False) + + # Fees + + embedVar.add_field(name="Fees", value="", inline=False) + embedVar.add_field( + name=" ", + value=format_currency( + pool.token0_fees.amount_in_stable + + pool.token1_fees.amount_in_stable + ), + inline=True, + ) + embedVar.add_field( + name=" ", + value=format_currency( + pool.token0_fees.amount, symbol=pool.token0.symbol, prefix=False + ), + inline=True, + ) + embedVar.add_field( + name=" ", + value=format_currency( + pool.token1_fees.amount, symbol=pool.token1.symbol, prefix=False + ), + inline=True, + ) + embedVar.add_field(name="", value="", inline=False) + + # Pool balance + + embedVar.add_field(name="Pool Balance", value="", inline=False) + embedVar.add_field( + name=" ", + value=format_currency( + pool.reserve0.amount, symbol=pool.token0.symbol, prefix=False + ), + inline=True, + ) + embedVar.add_field( + name=" ", + value=format_currency( + pool.reserve1.amount, symbol=pool.token1.symbol, prefix=False + ), + inline=True, + ) + + await ctx.send(embed=embedVar) + + async def main(): """Main function.""" @@ -29,16 +191,19 @@ async def main(): token = await Token.get_by_token_address(TOKEN_ADDRESS) stable = await Token.get_by_token_address(STABLE_TOKEN_ADDRESS) - price_bot = PriceBot(source_token=token, target_token=stable) + # price_bot = PriceBot(source_token=token, target_token=stable) tvl_bot = TVLBot(protocol_name=PROTOCOL_NAME) - fees_bot = FeesBot(protocol_name=PROTOCOL_NAME) - rewards_bot = RewardsBot(protocol_name=PROTOCOL_NAME) + # fees_bot = FeesBot(protocol_name=PROTOCOL_NAME) + # rewards_bot = RewardsBot(protocol_name=PROTOCOL_NAME) + + # await bot.start(DISCORD_TOKEN_REWARDS) await asyncio.gather( - price_bot.start(DISCORD_TOKEN_PRICING), - fees_bot.start(DISCORD_TOKEN_FEES), - tvl_bot.start(DISCORD_TOKEN_TVL), - rewards_bot.start(DISCORD_TOKEN_REWARDS), + bot.start(DISCORD_TOKEN_TVL), + # price_bot.start(DISCORD_TOKEN_PRICING), + # fees_bot.start(DISCORD_TOKEN_FEES), + # tvl_bot.start(DISCORD_TOKEN_TVL), + # rewards_bot.start(DISCORD_TOKEN_REWARDS), ) diff --git a/bots/data.py b/bots/data.py index 8c6b752..df50f69 100644 --- a/bots/data.py +++ b/bots/data.py @@ -70,9 +70,42 @@ async def get_by_token_address(cls, token_address: str) -> "Token": Returns: Token: target token or None """ - normalized_address = normalize_address(token_address) - tokens = await cls.get_all_listed_tokens() - return next(t for t in tokens if t.token_address == normalized_address) + try: + normalized_address = normalize_address(token_address) + tokens = await cls.get_all_listed_tokens() + return next(t for t in tokens if t.token_address == normalized_address) + except: + return None + + +@dataclass(frozen=True) +class Amount: + token: Token + amount: float + price: "Price" + + @classmethod + def build( + cls, + address: str, + amount: float, + tokens: Dict[str, Token], + prices: Dict[str, "Price"], + ) -> "Amount": + address = normalize_address(address) + + if address not in tokens or address not in prices: + return None + + token = tokens[address] + + return Amount( + token=token, amount=token.value_from_bigint(amount), price=prices[address] + ) + + @property + def amount_in_stable(self) -> float: + return self.amount * self.price.price @dataclass(frozen=True) @@ -150,36 +183,6 @@ async def get_prices( return functools.reduce(lambda l1, l2: l1 + l2, batches, []) -@dataclass(frozen=True) -class Amount: - token: Token - amount: float - price: Price - - @classmethod - def build( - cls, - address: str, - amount: float, - tokens: Dict[str, Token], - prices: Dict[str, "Price"], - ) -> "Amount": - address = normalize_address(address) - - if address not in tokens or address not in prices: - return None - - token = tokens[address] - - return Amount( - token=token, amount=token.value_from_bigint(amount), price=prices[address] - ) - - @property - def amount_in_stable(self) -> float: - return self.amount * self.price.price - - @dataclass(frozen=True) class LiquidityPool: """Data class for Liquidity Pool @@ -190,12 +193,19 @@ class LiquidityPool: lp: str symbol: str + is_stable: bool + total_supply: float + decimals: int token0: Token - reserve0: float + reserve0: Amount token1: Token - reserve1: float + reserve1: Amount token0_fees: Amount token1_fees: Amount + pool_fee: float + gauge_total_supply: float + emissions: Amount + emissions_token: Token @classmethod def from_tuple( @@ -205,16 +215,25 @@ def from_tuple( token1 = normalize_address(t[8]) token0_fees = t[23] token1_fees = t[24] + emissions_token = normalize_address(t[18]) + emissions = t[17] return LiquidityPool( lp=normalize_address(t[0]), symbol=t[1], + is_stable=t[3], + total_supply=t[4], + decimals=t[2], token0=tokens.get(token0), - reserve0=t[6], + reserve0=Amount.build(token0, t[6], tokens, prices), token1=tokens.get(token1), - reserve1=t[9], + reserve1=Amount.build(token1, t[9], tokens, prices), token0_fees=Amount.build(token0, token0_fees, tokens, prices), token1_fees=Amount.build(token1, token1_fees, tokens, prices), + pool_fee=t[22], + gauge_total_supply=t[12], + emissions_token=tokens.get(emissions_token), + emissions=Amount.build(emissions_token, emissions, tokens, prices), ) @classmethod @@ -236,6 +255,15 @@ async def get_pools(cls) -> List["LiquidityPool"]: ) ) + @classmethod + async def by_address(cls, address: str) -> "LiquidityPool": + pools = await cls.get_pools() + try: + a = normalize_address(address) + return next(pool for pool in pools if pool.lp == a) + except: + return None + @classmethod async def tvl(cls, pools) -> float: result = 0 @@ -249,14 +277,10 @@ async def tvl(cls, pools) -> float: t1 = pool.token1 if t0: - result += ( - t0.value_from_bigint(pool.reserve0) * prices[t0.token_address].price - ) + result += pool.reserve0.amount_in_stable if t1: - result += ( - t1.value_from_bigint(pool.reserve1) * prices[t1.token_address].price - ) + result += pool.reserve1.amount_in_stable return result @@ -271,6 +295,36 @@ def total_fees(self) -> float: return result + @property + def pool_fee_percentage(self) -> float: + return self.pool_fee / 100 + + @property + def volume_pct(self) -> float: + return 100 / self.pool_fee_percentage + + @property + def volume(self) -> float: + return self.volume_pct * ( + self.token0_fees.amount_in_stable + self.token1_fees.amount_in_stable + ) + + @property + def token0_volume(self) -> float: + return self.token0_fees.amount * self.volume_pct + + @property + def token1_volume(self) -> float: + return self.token1_fees.amount * self.volume_pct + + def apr(self, tvl: float) -> float: + day_seconds = 24 * 60 * 60 + reward_value = self.emissions.amount_in_stable + reward = reward_value * day_seconds + staked_pct = 100 * self.gauge_total_supply / self.total_supply + staked_tvl = tvl * staked_pct / 100 + return (reward / staked_tvl) * (100 * 365) + @dataclass(frozen=True) class LiquidityPoolEpoch: diff --git a/bots/helpers.py b/bots/helpers.py index 57b9037..b934f69 100644 --- a/bots/helpers.py +++ b/bots/helpers.py @@ -35,6 +35,15 @@ def amount_to_k_string(amount: float) -> str: return f"{round(amount/1000, 2)}K" +def format_currency(value: float, symbol: str = "$", prefix: bool = True) -> str: + v = "{:0,.2f}".format(value) + return f"{symbol}{v}" if prefix else f"{v} {symbol}" + + +def format_percentage(value: float) -> str: + return "{:0,.2f} %".format(value) + + # logging LOGGING_LEVEL = os.getenv("LOGGING_LEVEL", "DEBUG") LOGGING_HANDLER = logging.StreamHandler(sys.stdout) From a7f222c4f531fd8cd957909a1fec67c5d911a30e Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Wed, 15 Nov 2023 22:58:56 +0000 Subject: [PATCH 2/6] commander bot setup - WIP --- bots/__main__.py | 189 ++++-------------------------------------- bots/commander.py | 63 ++++++++++++++ bots/data.py | 16 +++- bots/helpers.py | 4 + bots/ui/__init__.py | 2 + bots/ui/pool_stats.py | 91 ++++++++++++++++++++ bots/ui/pools.py | 35 ++++++++ poetry.lock | 118 +++++++++++++++++++++++++- pyproject.toml | 1 + 9 files changed, 342 insertions(+), 177 deletions(-) create mode 100644 bots/commander.py create mode 100644 bots/ui/__init__.py create mode 100644 bots/ui/pool_stats.py create mode 100644 bots/ui/pools.py diff --git a/bots/__main__.py b/bots/__main__.py index 7f389eb..792323c 100644 --- a/bots/__main__.py +++ b/bots/__main__.py @@ -10,174 +10,16 @@ STABLE_TOKEN_ADDRESS, PROTOCOL_NAME, ) -from .data import Token, LiquidityPool -from .helpers import LOGGING_HANDLER, LOGGING_LEVEL, format_currency, format_percentage +from .data import Token +from .helpers import ( + LOGGING_HANDLER, + LOGGING_LEVEL, +) from .price import PriceBot from .tvl import TVLBot from .fees import FeesBot from .rewards import RewardsBot - - -import discord -from discord.ext import commands - -intents = discord.Intents.default() -intents.message_content = True - - -# Defines a custom Select containing colour options -# that the user can choose. The callback function -# of this class is called when the user changes their choice -class Dropdown(discord.ui.Select): - def __init__(self): - # Set the options that will be presented inside the dropdown - options = [ - discord.SelectOption( - label="Red", description="Your favourite colour is red", emoji="🟥" - ), - discord.SelectOption( - label="Green", description="Your favourite colour is green", emoji="🟩" - ), - discord.SelectOption( - label="Blue", description="Your favourite colour is blue", emoji="🟦" - ), - ] - - # The placeholder is what will be shown when no option is chosen - # The min and max values indicate we can only pick one of the three options - # The options parameter defines the dropdown options. We defined this above - super().__init__( - placeholder="Choose your favourite colour...", - min_values=1, - max_values=1, - options=options, - ) - - async def callback(self, interaction: discord.Interaction): - # Use the interaction object to send a response message containing - # the user's favourite colour or choice. The self object refers to the - # Select object, and the values attribute gets a list of the user's - # selected options. We only want the first one. - await interaction.response.send_message( - f"Your favourite colour is {self.values[0]}" - ) - - -class DropdownView(discord.ui.View): - def __init__(self): - super().__init__() - - # Adds the dropdown to our view object. - self.add_item(Dropdown()) - - -bot = commands.Bot(command_prefix=commands.when_mentioned_or("/"), intents=intents) - - -@bot.command() -async def pool(ctx, *args): - print(">>>>>>>>>>>>>>>>>> test command") - - # Create the view containing our dropdown - view = DropdownView() - - # Sending a message containing our view - await ctx.send("Choose your pool:", view=view) - - if len(args) == 0: - await ctx.send("Missing pool address: should be /pool POOL_ADDRESS_HERE") - else: - pool = await LiquidityPool.by_address(args[0]) - if pool is None: - await ctx.send("Nothing found") - else: - print(pool) - tvl = await LiquidityPool.tvl([pool]) - embedVar = 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, - ) - - embedVar.add_field(name="", value="", inline=False) - - # Volume - - embedVar.add_field(name="Volume", value="", inline=False) - embedVar.add_field( - name=" ", - value=format_currency(pool.volume), - inline=True, - ) - embedVar.add_field( - name=" ", - value=format_currency( - pool.token0_volume, symbol=pool.token0.symbol, prefix=False - ), - inline=True, - ) - embedVar.add_field( - name=" ", - value=format_currency( - pool.token1_volume, symbol=pool.token1.symbol, prefix=False - ), - inline=True, - ) - embedVar.add_field(name="", value="", inline=False) - - # Fees - - embedVar.add_field(name="Fees", value="", inline=False) - embedVar.add_field( - name=" ", - value=format_currency( - pool.token0_fees.amount_in_stable - + pool.token1_fees.amount_in_stable - ), - inline=True, - ) - embedVar.add_field( - name=" ", - value=format_currency( - pool.token0_fees.amount, symbol=pool.token0.symbol, prefix=False - ), - inline=True, - ) - embedVar.add_field( - name=" ", - value=format_currency( - pool.token1_fees.amount, symbol=pool.token1.symbol, prefix=False - ), - inline=True, - ) - embedVar.add_field(name="", value="", inline=False) - - # Pool balance - - embedVar.add_field(name="Pool Balance", value="", inline=False) - embedVar.add_field( - name=" ", - value=format_currency( - pool.reserve0.amount, symbol=pool.token0.symbol, prefix=False - ), - inline=True, - ) - embedVar.add_field( - name=" ", - value=format_currency( - pool.reserve1.amount, symbol=pool.token1.symbol, prefix=False - ), - inline=True, - ) - - await ctx.send(embed=embedVar) +from .commander import CommanderBot async def main(): @@ -191,19 +33,18 @@ async def main(): token = await Token.get_by_token_address(TOKEN_ADDRESS) stable = await Token.get_by_token_address(STABLE_TOKEN_ADDRESS) - # price_bot = PriceBot(source_token=token, target_token=stable) + price_bot = PriceBot(source_token=token, target_token=stable) tvl_bot = TVLBot(protocol_name=PROTOCOL_NAME) - # fees_bot = FeesBot(protocol_name=PROTOCOL_NAME) - # rewards_bot = RewardsBot(protocol_name=PROTOCOL_NAME) - - # await bot.start(DISCORD_TOKEN_REWARDS) + fees_bot = FeesBot(protocol_name=PROTOCOL_NAME) + rewards_bot = RewardsBot(protocol_name=PROTOCOL_NAME) + commander_bot = CommanderBot() await asyncio.gather( - bot.start(DISCORD_TOKEN_TVL), - # price_bot.start(DISCORD_TOKEN_PRICING), - # fees_bot.start(DISCORD_TOKEN_FEES), - # tvl_bot.start(DISCORD_TOKEN_TVL), - # rewards_bot.start(DISCORD_TOKEN_REWARDS), + price_bot.start(DISCORD_TOKEN_PRICING), + fees_bot.start(DISCORD_TOKEN_FEES), + tvl_bot.start(DISCORD_TOKEN_TVL), + rewards_bot.start(DISCORD_TOKEN_REWARDS), + commander_bot.start(DISCORD_TOKEN_TVL), ) diff --git a/bots/commander.py b/bots/commander.py new file mode 100644 index 0000000..a8829e8 --- /dev/null +++ b/bots/commander.py @@ -0,0 +1,63 @@ +from .data import LiquidityPool +from .helpers import is_address +from .ui import PoolsDropdown, PoolStats + +import discord +from discord.ext import commands + + +class _CommanderBot(commands.Bot): + def __init__(self): + intents = discord.Intents.default() + intents.message_content = True + super().__init__(command_prefix="/", intents=intents) + + async def on_ready(self): + print(f"Logged in as {self.user} (ID: {self.user.id})") + print("------") + await bot.tree.sync() + + +bot = _CommanderBot() + + +def CommanderBot() -> commands.Bot: + return bot + + +async def on_select_pool( + response: discord.InteractionResponse, + address_or_pool: str | LiquidityPool, +): + pool = ( + await LiquidityPool.by_address(address_or_pool) + if isinstance(address_or_pool, str) + else address_or_pool + ) + tvl = await LiquidityPool.tvl([pool]) + embed = await PoolStats().render(pool, tvl) + + await response.send_message(embed=embed) + + +@bot.tree.command(name="pool", description="Get data for specific pool") +@discord.app_commands.describe( + address_or_query="Pool address or search query", +) +async def pool(interaction: discord.Interaction, address_or_query: str): + if is_address(address_or_query): + pool = await LiquidityPool.by_address(address_or_query) + + if pool is not None: + await on_select_pool(interaction.response, pool) + else: + await interaction.response.send_message( + f"No pool found with this address: {address_or_query}" + ) + return + + pools = await LiquidityPool.search(address_or_query) + + await interaction.response.send_message( + "Choose a pool:", view=PoolsDropdown(pools=pools, callback=on_select_pool) + ) diff --git a/bots/data.py b/bots/data.py index df50f69..c00b47f 100644 --- a/bots/data.py +++ b/bots/data.py @@ -1,5 +1,6 @@ import functools import asyncio +from thefuzz import fuzz from web3 import AsyncWeb3, AsyncHTTPProvider from web3.constants import ADDRESS_ZERO from dataclasses import dataclass @@ -74,7 +75,7 @@ async def get_by_token_address(cls, token_address: str) -> "Token": normalized_address = normalize_address(token_address) tokens = await cls.get_all_listed_tokens() return next(t for t in tokens if t.token_address == normalized_address) - except: + except Exception: return None @@ -261,9 +262,20 @@ async def by_address(cls, address: str) -> "LiquidityPool": try: a = normalize_address(address) return next(pool for pool in pools if pool.lp == a) - except: + except Exception: return None + @classmethod + async def search(cls, query: str, limit: int = 10) -> List["LiquidityPool"]: + def match_score(query: str, symbol: str): + return fuzz.token_sort_ratio(query, symbol) + + pools = await cls.get_pools() + pools_with_ratio = list(map(lambda p: (p, match_score(query, p.symbol)), pools)) + pools_with_ratio.sort(key=lambda p: p[1], reverse=True) + + return list(map(lambda pwr: pwr[0], pools_with_ratio))[:limit] + @classmethod async def tvl(cls, pools) -> float: result = 0 diff --git a/bots/helpers.py b/bots/helpers.py index 294a6c1..85b4ef5 100644 --- a/bots/helpers.py +++ b/bots/helpers.py @@ -7,6 +7,10 @@ from async_lru import alru_cache +def is_address(value: str) -> bool: + return Web3.is_address(value) + + def cache_in_seconds(seconds: int): return alru_cache(ttl=seconds) diff --git a/bots/ui/__init__.py b/bots/ui/__init__.py new file mode 100644 index 0000000..5099191 --- /dev/null +++ b/bots/ui/__init__.py @@ -0,0 +1,2 @@ +from .pools import PoolsDropdown # noqa +from .pool_stats import PoolStats # noqa diff --git a/bots/ui/pool_stats.py b/bots/ui/pool_stats.py new file mode 100644 index 0000000..ec94a89 --- /dev/null +++ b/bots/ui/pool_stats.py @@ -0,0 +1,91 @@ +import discord +from ..data import LiquidityPool +from ..helpers import format_percentage, format_currency + + +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, + ) + embed.add_field( + name=" ", + value=format_currency( + pool.reserve1.amount, symbol=pool.token1.symbol, prefix=False + ), + inline=True, + ) + + return embed diff --git a/bots/ui/pools.py b/bots/ui/pools.py new file mode 100644 index 0000000..f64db6a --- /dev/null +++ b/bots/ui/pools.py @@ -0,0 +1,35 @@ +import discord +from ..data import LiquidityPool +from typing import List, Callable, Awaitable + +intents = discord.Intents.default() +intents.message_content = True + + +def build_select_option(pool: LiquidityPool) -> discord.SelectOption: + return discord.SelectOption(label=pool.symbol, value=pool.lp, emoji="🏊‍♀️") + + +class _PoolsDropdown(discord.ui.Select): + def __init__( + self, + pools: List[LiquidityPool], + callback: Callable[[discord.InteractionResponse, str], Awaitable[None]], + ): + options = list(map(build_select_option, pools)) + super().__init__( + placeholder="Which pool are you intersted in...", + min_values=1, + max_values=1, + options=options, + ) + self._callback = callback + + async def callback(self, interaction: discord.Interaction): + await self._callback(interaction.response, self.values[0]) + + +class PoolsDropdown(discord.ui.View): + def __init__(self, pools: List[LiquidityPool], callback): + super().__init__() + self.add_item(_PoolsDropdown(pools=pools, callback=callback)) diff --git a/poetry.lock b/poetry.lock index 9072d68..f4e1b93 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1344,6 +1344,108 @@ files = [ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] +[[package]] +name = "rapidfuzz" +version = "3.5.2" +description = "rapid fuzzy string matching" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rapidfuzz-3.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1a047d6e58833919d742bbc0dfa66d1de4f79e8562ee195007d3eae96635df39"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22877c027c492b7dc7e3387a576a33ed5aad891104aa90da2e0844c83c5493ef"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0f448b0eacbcc416feb634e1232a48d1cbde5e60f269c84e4fb0912f7bbb001"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05146497672f869baf41147d5ec1222788c70e5b8b0cfcd6e95597c75b5b96b"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f2df3968738a38d2a0058b5e721753f5d3d602346a1027b0dde31b0476418f3"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5afc1fcf1830f9bb87d3b490ba03691081b9948a794ea851befd2643069a30c1"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84be69ea65f64fa01e5c4976be9826a5aa949f037508887add42da07420d65d6"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8658c1045766e87e0038323aa38b4a9f49b7f366563271f973c8890a98aa24b5"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:852b3f93c15fce58b8dc668bd54123713bfdbbb0796ba905ea5df99cfd083132"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:12424a06ad9bd0cbf5f7cea1015e78d924a0034a0e75a5a7b39c0703dcd94095"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b4e9ded8e80530bd7205a7a2b01802f934a4695ca9e9fbe1ce9644f5e0697864"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:affb8fe36157c2dc8a7bc45b6a1875eb03e2c49167a1d52789144bdcb7ab3b8c"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1d33a622572d384f4c90b5f7a139328246ab5600141e90032b521c2127bd605"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-win32.whl", hash = "sha256:2cf9f2ed4a97b388cffd48d534452a564c2491f68f4fd5bc140306f774ceb63a"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:6541ffb70097885f7302cd73e2efd77be99841103023c2f9408551f27f45f7a5"}, + {file = "rapidfuzz-3.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:1dd2542e5103fb8ca46500a979ae14d1609dcba11d2f9fe01e99eec03420e193"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bff7d3127ebc5cd908f3a72f6517f31f5247b84666137556a8fcc5177c560939"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdfdb3685b631d8efbb6d6d3d86eb631be2b408d9adafcadc11e63e3f9c96dec"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97b043fe8185ec53bb3ff0e59deb89425c0fc6ece6e118939963aab473505801"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a4a7832737f87583f3863dc62e6f56dd4a9fefc5f04a7bdcb4c433a0f36bb1b"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d876dba9a11fcf60dcf1562c5a84ef559db14c2ceb41e1ad2d93cd1dc085889"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa4c0612893716bbb6595066ca9ecb517c982355abe39ba9d1f4ab834ace91ad"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:120316824333e376b88b284724cfd394c6ccfcb9818519eab5d58a502e5533f0"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cdbe8e80cc186d55f748a34393533a052d855357d5398a1ccb71a5021b58e8d"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1062425c8358a547ae5ebad148f2e0f02417716a571b803b0c68e4d552e99d32"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66be181965aff13301dd5f9b94b646ce39d99c7fe2fd5de1656f4ca7fafcb38c"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:53df7aea3cf301633cfa2b4b2c2d2441a87dfc878ef810e5b4eddcd3e68723ad"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:76639dca5eb0afc6424ac5f42d43d3bd342ac710e06f38a8c877d5b96de09589"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:27689361c747b5f7b8a26056bc60979875323f1c3dcaaa9e2fec88f03b20a365"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-win32.whl", hash = "sha256:99c9fc5265566fb94731dc6826f43c5109e797078264e6389a36d47814473692"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:666928ee735562a909d81bd2f63207b3214afd4ca41f790ab3025d066975c814"}, + {file = "rapidfuzz-3.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:d55de67c48f06b7772541e8d4c062a2679205799ce904236e2836cb04c106442"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:04e1e02b182283c43c866e215317735e91d22f5d34e65400121c04d5ed7ed859"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:365e544aba3ac13acf1a62cb2e5909ad2ba078d0bfc7d69b1f801dfd673b9782"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b61f77d834f94b0099fa9ed35c189b7829759d4e9c2743697a130dd7ba62259f"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43fb368998b9703fa8c63db292a8ab9e988bf6da0c8a635754be8e69da1e7c1d"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25510b5d142c47786dbd27cfd9da7cae5bdea28d458379377a3644d8460a3404"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf3093443751e5a419834162af358d1e31dec75f84747a91dbbc47b2c04fc085"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fbaf546f15a924613f89d609ff66b85b4f4c2307ac14d93b80fe1025b713138"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32d580df0e130ed85400ff77e1c32d965e9bc7be29ac4072ab637f57e26d29fb"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:358a0fbc49343de20fee8ebdb33c7fa8f55a9ff93ff42d1ffe097d2caa248f1b"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fb379ac0ddfc86c5542a225d194f76ed468b071b6f79ff57c4b72e635605ad7d"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7fb21e182dc6d83617e88dea002963d5cf99cf5eabbdbf04094f503d8fe8d723"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c04f9f1310ce414ab00bdcbf26d0906755094bfc59402cb66a7722c6f06d70b2"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6da61cc38c1a95efc5edcedf258759e6dbab73191651a28c5719587f32a56ad"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-win32.whl", hash = "sha256:f823fd1977071486739f484e27092765d693da6beedaceece54edce1dfeec9b2"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:a8162d81486de85ab1606e48e076431b66d44cf431b2b678e9cae458832e7147"}, + {file = "rapidfuzz-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:dfc63fabb7d8da8483ca836bae7e55766fe39c63253571e103c034ba8ea80950"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:df8fae2515a1e4936affccac3e7d506dd904de5ff82bc0b1433b4574a51b9bfb"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dd6384780c2a16097d47588844cd677316a90e0f41ef96ff485b62d58de79dcf"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:467a4d730ae3bade87dba6bd769e837ab97e176968ce20591fe8f7bf819115b1"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54576669c1502b751b534bd76a4aeaaf838ed88b30af5d5c1b7d0a3ca5d4f7b5"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abafeb82f85a651a9d6d642a33dc021606bc459c33e250925b25d6b9e7105a2e"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73e14617a520c0f1bc15eb78c215383477e5ca70922ecaff1d29c63c060e04ca"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cdf92116e9dfe40da17f921cdbfa0039dde9eb158914fa5f01b1e67a20b19cb"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1962d5ccf8602589dbf8e85246a0ee2b4050d82fade1568fb76f8a4419257704"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:db45028eae2fda7a24759c69ebeb2a7fbcc1a326606556448ed43ee480237a3c"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b685abb8b6d97989f6c69556d7934e0e533aa8822f50b9517ff2da06a1d29f23"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:40139552961018216b8cd88f6df4ecbbe984f907a62a5c823ccd907132c29a14"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0fef4705459842ef8f79746d6f6a0b5d2b6a61a145d7d8bbe10b2e756ea337c8"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6b2ad5516f7068c7d9cbcda8ac5906c589e99bc427df2e1050282ee2d8bc2d58"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-win32.whl", hash = "sha256:2da3a24c2f7dfca7f26ba04966b848e3bbeb93e54d899908ff88dfe3e1def9dc"}, + {file = "rapidfuzz-3.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:e3f2be79d4114d01f383096dbee51b57df141cb8b209c19d0cf65f23a24e75ba"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:089a7e96e5032821af5964d8457fcb38877cc321cdd06ad7c5d6e3d852264cb9"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75d8a52bf8d1aa2ac968ae4b21b83b94fc7e5ea3dfbab34811fc60f32df505b2"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2bacce6bbc0362f0789253424269cc742b1f45e982430387db3abe1d0496e371"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5fd627e604ddc02db2ddb9ddc4a91dd92b7a6d6378fcf30bb37b49229072b89"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2e8b369f23f00678f6e673572209a5d3b0832f4991888e3df97af7b8b9decf3"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c29958265e4c2b937269e804b8a160c027ee1c2627d6152655008a8b8083630e"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00be97f9219355945c46f37ac9fa447046e6f7930f7c901e5d881120d1695458"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada0d8d57e0f556ef38c24fee71bfe8d0db29c678bff2acd1819fc1b74f331c2"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de89585268ed8ee44e80126814cae63ff6b00d08416481f31b784570ef07ec59"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:908ff2de9c442b379143d1da3c886c63119d4eba22986806e2533cee603fe64b"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:54f0061028723c026020f5bb20649c22bc8a0d9f5363c283bdc5901d4d3bff01"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b581107ec0c610cdea48b25f52030770be390db4a9a73ca58b8d70fa8a5ec32e"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1d5a686ea258931aaa38019204bdc670bbe14b389a230b1363d84d6cf4b9dc38"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-win32.whl", hash = "sha256:97f811ca7709c6ee8c0b55830f63b3d87086f4abbcbb189b4067e1cd7014db7b"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:58ee34350f8c292dd24a050186c0e18301d80da904ef572cf5fda7be6a954929"}, + {file = "rapidfuzz-3.5.2-cp39-cp39-win_arm64.whl", hash = "sha256:c5075ce7b9286624cafcf36720ef1cfb2946d75430b87cb4d1f006e82cd71244"}, + {file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af5221e4f7800db3e84c46b79dba4112e3b3cc2678f808bdff4fcd2487073846"}, + {file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8501d7875b176930e6ed9dbc1bc35adb37ef312f6106bd6bb5c204adb90160ac"}, + {file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e414e1ca40386deda4291aa2d45062fea0fbaa14f95015738f8bb75c4d27f862"}, + {file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2059cd73b7ea779a9307d7a78ed743f0e3d33b88ccdcd84569abd2953cd859f"}, + {file = "rapidfuzz-3.5.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:58e3e21f6f13a7cca265cce492bc797425bd4cb2025fdd161a9e86a824ad65ce"}, + {file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b847a49377e64e92e11ef3d0a793de75451526c83af015bdafdd5d04de8a058a"}, + {file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a42c7a8c62b29c4810e39da22b42524295fcb793f41c395c2cb07c126b729e83"}, + {file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51b5166be86e09e011e92d9862b1fe64c4c7b9385f443fb535024e646d890460"}, + {file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f808dcb0088a7a496cc9895e66a7b8de55ffea0eb9b547c75dfb216dd5f76ed"}, + {file = "rapidfuzz-3.5.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d4b05a8f4ab7e7344459394094587b033fe259eea3a8720035e8ba30e79ab39b"}, + {file = "rapidfuzz-3.5.2.tar.gz", hash = "sha256:9e9b395743e12c36a3167a3a9fd1b4e11d92fb0aa21ec98017ee6df639ed385e"}, +] + +[package.extras] +full = ["numpy"] + [[package]] name = "referencing" version = "0.30.2" @@ -1606,6 +1708,20 @@ files = [ {file = "rpds_py-0.10.6.tar.gz", hash = "sha256:4ce5a708d65a8dbf3748d2474b580d606b1b9f91b5c6ab2a316e0b0cf7a4ba50"}, ] +[[package]] +name = "thefuzz" +version = "0.20.0" +description = "Fuzzy string matching in python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "thefuzz-0.20.0-py3-none-any.whl", hash = "sha256:bd2b657a12bd8518917d2d71c53125368706233b822fac688fca956730154388"}, + {file = "thefuzz-0.20.0.tar.gz", hash = "sha256:a25e49786b1c4603c7fc6e2d69e6bc660982a2919698b536ff8354e0631cc40d"}, +] + +[package.dependencies] +rapidfuzz = ">=3.0.0,<4.0.0" + [[package]] name = "tomli" version = "2.0.1" @@ -1862,4 +1978,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "a498c2243598ca7d9a2950ef64fa3655cc1841db8bacd1cc3f73f79fe0b476ee" +content-hash = "5515a802834ef5aff0cdb470274917b1241d112d6d1ba9b162b791e809ead996" diff --git a/pyproject.toml b/pyproject.toml index 0446d0d..b97488a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ protobuf = "4.21.6" async-lru = "2.0.4" discord-py = "2.3.2" python-dotenv = "1.0.0" +thefuzz = "^0.20.0" [tool.poetry.group.dev.dependencies] pytest = "^7.4.3" From 593b8826bd2b7b52d459c240345066a9b9c3a413 Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Thu, 16 Nov 2023 18:31:13 +0000 Subject: [PATCH 3/6] 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 From 884788a65e83bbac166cdf07b0f66607f894b167 Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Mon, 20 Nov 2023 09:52:57 +0000 Subject: [PATCH 4/6] remove ":" from "Rewards:" --- bots/rewards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bots/rewards.py b/bots/rewards.py index c49009c..c5d0138 100644 --- a/bots/rewards.py +++ b/bots/rewards.py @@ -24,7 +24,7 @@ async def ticker(self): bribes = sum(map(lambda lpe: lpe.total_bribes, lpes)) await self.update_nick_for_all_servers( - f"Rewards: ~${amount_to_k_string(fees + bribes)}" + f"Rewards ~${amount_to_k_string(fees + bribes)}" ) await self.update_presence( f"~${amount_to_k_string(fees)} + ~${amount_to_k_string(bribes)} in fees & incentives for voters" # noqa From 9ac1c0684a587863fc6d2caa397b99ff58096691 Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Mon, 20 Nov 2023 13:58:16 +0000 Subject: [PATCH 5/6] update env, fix tests --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index a14835d..d9302c5 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,8 @@ 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 +LP_SUGAR_ADDRESS=0x6A4a8e26D9bA37515F331de7fbA2c6852f55128E +PRICE_ORACLE_ADDRESS=0xcA97e5653d775cA689BED5D0B4164b7656677011 PRICE_BATCH_SIZE=40 # token we are converting from TOKEN_ADDRESS=0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db From f9d38dc20972d45733c528840c5cfe7a26be8384 Mon Sep 17 00:00:00 2001 From: Philip Nuzhnyi Date: Mon, 20 Nov 2023 14:19:14 +0000 Subject: [PATCH 6/6] fix formatting --- .flake8 | 2 +- bots/ui/pool_stats.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index 38aa33f..ab64631 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] select = B,B9,BLK,C,E,F,S,W -ignore = E203,E501,W503 +ignore = E203,E501,W503,W291 per-file-ignores = tests/*:S101 max-line-length = 88 diff --git a/bots/ui/pool_stats.py b/bots/ui/pool_stats.py index cbddfc6..4405247 100644 --- a/bots/ui/pool_stats.py +++ b/bots/ui/pool_stats.py @@ -46,7 +46,7 @@ async def render(self, pool: LiquidityPool, tvl: float) -> str: > - {token1_volume} > - ~{volume} volume this epoch > - ~{fees} fees this epoch -> +> > [Deposit 🐖]({deposit_url}) ● [Incentivize 🙋]({incentivize_url}) """.format( **template_args