Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Commander bot UI fixes #8

Merged
merged 5 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ SUGAR_TOKENS_CACHE_MINUTES=10
# caching for Sugar LPs
SUGAR_LPS_CACHE_MINUTES=10
# caching for oracle pricing
ORACLE_PRICES_CACHE_MINUTES=10
ORACLE_PRICES_CACHE_MINUTES=10
UI_POOL_STATS_THUMBNAIL=https://i.imgur.com/lGbVYac.png
41 changes: 35 additions & 6 deletions bots/commander.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .data import LiquidityPool
from .data import LiquidityPool, LiquidityPoolEpoch
from .helpers import is_address
from .ui import PoolsDropdown, PoolStats

Expand All @@ -7,6 +7,8 @@


class _CommanderBot(commands.Bot):
"""Commander bot instance to handle / commands"""

def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
Expand All @@ -22,21 +24,29 @@ async def on_ready(self):


def CommanderBot() -> commands.Bot:
# keep our commander as a singleton
return bot


async def on_select_pool(
response: discord.InteractionResponse,
interaction: discord.Interaction,
address_or_pool: str | LiquidityPool,
):
"""Handle pool selection and reply with a pool stats embed

Args:
interaction (discord.Interaction): chat interaction
address_or_pool (str | LiquidityPool): pool address or instance
"""
pool = (
await LiquidityPool.by_address(address_or_pool)
if isinstance(address_or_pool, str)
else address_or_pool
)
tvl = await LiquidityPool.tvl([pool])
await response.send_message(
await PoolStats().render(pool, tvl), suppress_embeds=True
pool_epoch = await LiquidityPoolEpoch.fetch_for_pool(pool.lp)
await interaction.response.send_message(
embed=await PoolStats(interaction.client.emojis).render(pool, tvl, pool_epoch)
)


Expand All @@ -45,11 +55,21 @@ async def on_select_pool(
address_or_query="Pool address or search query",
)
async def pool(interaction: discord.Interaction, address_or_query: str):
"""Pool command handler: show specific pool or pool selector

Args:
interaction (discord.Interaction): chat interaction
address_or_query (str): command input
"""
if is_address(address_or_query):
# if /pool receives specific pool address,
# show the pool immediately or show in error
# message if it does not exist

pool = await LiquidityPool.by_address(address_or_query)

if pool is not None:
await on_select_pool(interaction.response, pool)
await on_select_pool(interaction, pool)
else:
await interaction.response.send_message(
f"No pool found with this address: {address_or_query}"
Expand All @@ -58,6 +78,15 @@ async def pool(interaction: discord.Interaction, address_or_query: str):

pools = await LiquidityPool.search(address_or_query)

if len(pools) == 1:
# got exact match, show the pool
await on_select_pool(interaction, pools[0])
return

# search returned several pools, show them in a dropdown
await interaction.response.send_message(
"Choose a pool:", view=PoolsDropdown(pools=pools, callback=on_select_pool)
"Choose a pool:",
view=PoolsDropdown(
interaction=interaction, pools=pools, callback=on_select_pool
),
)
17 changes: 17 additions & 0 deletions bots/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,18 @@ 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)

query_lowercase = query.lower()
pools = await cls.get_pools()
pools = list(
filter(lambda p: p.token0 is not None and p.token1 is not None, pools)
)

# look for exact match first, i.e. we get proper pool symbol in query (case insensitive)
exact_match = list(filter(lambda p: p.symbol.lower() == query_lowercase, pools))

if len(exact_match) == 1:
return exact_match

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)

Expand Down Expand Up @@ -401,6 +409,15 @@ async def fetch_latest(cls):

return result

@classmethod
async def fetch_for_pool(cls, pool_address: str) -> "LiquidityPoolEpoch":
pool_epochs = await cls.fetch_latest()
try:
a = normalize_address(pool_address)
return next(pe for pe in pool_epochs if pe.pool_address == a)
except Exception:
return None

@property
def total_fees(self) -> float:
return sum(map(lambda fee: fee.amount_in_stable, self.fees))
Expand Down
2 changes: 2 additions & 0 deletions bots/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@
ORACLE_PRICES_CACHE_MINUTES = int(os.environ["ORACLE_PRICES_CACHE_MINUTES"])

GOOD_ENOUGH_PAGINATION_LIMIT = 2000

UI_POOL_STATS_THUMBNAIL = os.environ["UI_POOL_STATS_THUMBNAIL"]
1 change: 1 addition & 0 deletions bots/ui/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .pools import PoolsDropdown # noqa
from .pool_stats import PoolStats # noqa
from .emojis import Emojis # noqa
30 changes: 30 additions & 0 deletions bots/ui/emojis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Sequence
from discord import Emoji


class Emojis:
"""Loads all custom emojis associated with current server and
makes them available via get method"""

def __init__(self, emojis: Sequence[Emoji]):
"""Load all custom emojis

Args:
emojis (Sequence[Emoji]): custom emojis attached to a server
"""
self.emojis = {}
for emoji in emojis:
# based on: https://github.com/Rapptz/discord.py/issues/390
self.emojis[emoji.name] = str(emoji)

def get(self, name: str, fallback: str = "*") -> str:
"""Get custom emoji by name or return fallback string

Args:
name (str): name of the custom emoji to get
fallback (str, optional): fallback value to return. Defaults to "*".

Returns:
str: _description_
"""
return self.emojis[name] if name in self.emojis else fallback
Loading