diff --git a/.env.example b/.env.example index 1184228..8c40ee9 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ DISCORD_TOKEN_PRICING= DISCORD_TOKEN_TVL= DISCORD_TOKEN_FEES= +DISCORD_TOKEN_INCENTIVES= WEB3_PROVIDER_URI=https://mainnet.optimism.io PROTOCOL_NAME=Velodrome LP_SUGAR_ADDRESS=0xa1F09427fa89b92e9B4e4c7003508C8614F19791 diff --git a/bots/__main__.py b/bots/__main__.py index 0c1d1fd..1a529cd 100644 --- a/bots/__main__.py +++ b/bots/__main__.py @@ -5,6 +5,7 @@ DISCORD_TOKEN_PRICING, DISCORD_TOKEN_TVL, DISCORD_TOKEN_FEES, + DISCORD_TOKEN_INCENTIVES, TOKEN_ADDRESS, STABLE_TOKEN_ADDRESS, PROTOCOL_NAME, @@ -14,6 +15,7 @@ from .price import PriceBot from .tvl import TVLBot from .fees import FeesBot +from .incentives import IncentivesBot async def main(): @@ -30,11 +32,13 @@ async def main(): price_bot = PriceBot(source_token=token, target_token=stable) tvl_bot = TVLBot(protocol_name=PROTOCOL_NAME) fees_bot = FeesBot(protocol_name=PROTOCOL_NAME) + incentives_bot = IncentivesBot(protocol_name=PROTOCOL_NAME) await asyncio.gather( price_bot.start(DISCORD_TOKEN_PRICING), fees_bot.start(DISCORD_TOKEN_FEES), tvl_bot.start(DISCORD_TOKEN_TVL), + incentives_bot.start(DISCORD_TOKEN_INCENTIVES), ) diff --git a/bots/data.py b/bots/data.py index 25786f0..8c6b752 100644 --- a/bots/data.py +++ b/bots/data.py @@ -270,3 +270,60 @@ def total_fees(self) -> float: result += self.token1_fees.amount_in_stable return result + + +@dataclass(frozen=True) +class LiquidityPoolEpoch: + """Data class for Liquidity Pool + + based on: https://github.com/velodrome-finance/sugar/blob/v2/contracts/LpSugar.vy#L69 + """ + + pool_address: str + bribes: List[Amount] + fees: List[Amount] + + @classmethod + async def fetch_latest(cls): + tokens = await Token.get_all_listed_tokens() + prices = await Price.get_prices(tokens) + + prices = {price.token.token_address: price for price in prices} + tokens = {t.token_address: t for t in tokens} + + sugar = w3.eth.contract(address=LP_SUGAR_ADDRESS, abi=LP_SUGAR_ABI) + pool_epochs = await sugar.functions.epochsLatest( + GOOD_ENOUGH_PAGINATION_LIMIT, 0 + ).call() + + result = [] + + for pe in pool_epochs: + pool_address, bribes, fees = pe[1], pe[4], pe[5] + + bribes = list( + filter( + lambda b: b is not None, + map(lambda b: Amount.build(b[0], b[1], tokens, prices), bribes), + ) + ) + fees = list( + filter( + lambda f: f is not None, + map(lambda f: Amount.build(f[0], f[1], tokens, prices), fees), + ) + ) + + result.append( + LiquidityPoolEpoch(pool_address=pool_address, bribes=bribes, fees=fees) + ) + + return result + + @property + def total_fees(self) -> float: + return sum(map(lambda fee: fee.amount_in_stable, self.fees)) + + @property + def total_bribes(self) -> float: + return sum(map(lambda bribe: bribe.amount_in_stable, self.bribes)) diff --git a/bots/helpers.py b/bots/helpers.py index 171ea38..57b9037 100644 --- a/bots/helpers.py +++ b/bots/helpers.py @@ -30,6 +30,11 @@ def chunk(list_to_chunk: List, n: int): yield list_to_chunk[i : i + n] +def amount_to_k_string(amount: float) -> str: + """Turns 2000 to "2K" """ + return f"{round(amount/1000, 2)}K" + + # logging LOGGING_LEVEL = os.getenv("LOGGING_LEVEL", "DEBUG") LOGGING_HANDLER = logging.StreamHandler(sys.stdout) diff --git a/bots/incentives.py b/bots/incentives.py new file mode 100644 index 0000000..e7d4f8a --- /dev/null +++ b/bots/incentives.py @@ -0,0 +1,33 @@ +from discord.ext import tasks + +from .settings import BOT_TICKER_INTERVAL_MINUTES +from .data import LiquidityPoolEpoch +from .helpers import LOGGER, amount_to_k_string +from .ticker import TickerBot + + +class IncentivesBot(TickerBot): + def __init__(self, *args, protocol_name: str, **kwargs): + super().__init__(*args, **kwargs) + self.protocol_name = protocol_name + + async def on_ready(self): + LOGGER.debug(f"Logged in as {self.user} (ID: {self.user.id})") + LOGGER.debug("------") + await self.update_presence(f"incentives for {self.protocol_name}") + + @tasks.loop(seconds=BOT_TICKER_INTERVAL_MINUTES * 60) + async def ticker(self): + try: + lpes = await LiquidityPoolEpoch.fetch_latest() + fees = sum(map(lambda lpe: lpe.total_fees, lpes)) + bribes = sum(map(lambda lpe: lpe.total_bribes, lpes)) + + await self.update_nick_for_all_servers( + f"Rewards: {amount_to_k_string(fees + bribes)}" + ) + await self.update_presence( + f"{amount_to_k_string(fees)} fees and {amount_to_k_string(bribes)} incentives" + ) + except Exception as ex: + LOGGER.error(f"Ticker failed with {ex}") diff --git a/bots/settings.py b/bots/settings.py index 62c1021..a73fc6e 100644 --- a/bots/settings.py +++ b/bots/settings.py @@ -11,6 +11,8 @@ DISCORD_TOKEN_PRICING = os.environ["DISCORD_TOKEN_PRICING"] DISCORD_TOKEN_TVL = os.environ["DISCORD_TOKEN_TVL"] DISCORD_TOKEN_FEES = os.environ["DISCORD_TOKEN_FEES"] +DISCORD_TOKEN_INCENTIVES = os.environ["DISCORD_TOKEN_INCENTIVES"] + WEB3_PROVIDER_URI = os.environ["WEB3_PROVIDER_URI"] LP_SUGAR_ADDRESS = os.environ["LP_SUGAR_ADDRESS"] PRICE_ORACLE_ADDRESS = os.environ["PRICE_ORACLE_ADDRESS"] diff --git a/bots/ticker.py b/bots/ticker.py index d82d7f8..e8ea7a0 100644 --- a/bots/ticker.py +++ b/bots/ticker.py @@ -28,9 +28,8 @@ async def update_nick_for_all_servers(self, nick: str): async def update_presence(self, presence_text: str): # https://discordpy.readthedocs.io/en/latest/api.html#discord.ActivityType await self.change_presence( - activity=discord.Activity( - name=presence_text, type=discord.ActivityType.watching - ) + # XX: emoji does not work for some reason + activity=discord.CustomActivity(name=presence_text, emoji="😀") ) @tasks.loop(seconds=BOT_TICKER_INTERVAL_MINUTES * 60) diff --git a/tests/test_data.py b/tests/test_data.py index 0ed6a30..e746c55 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -5,7 +5,7 @@ load_dotenv(".env.example") from bots.settings import TOKEN_ADDRESS # noqa -from bots.data import Token, Price, LiquidityPool # noqa +from bots.data import Token, Price, LiquidityPool, LiquidityPoolEpoch # noqa @pytest.mark.asyncio @@ -35,3 +35,13 @@ async def test_fees(): pools = await LiquidityPool.get_pools() fees = sum(map(lambda p: p.total_fees, pools)) assert fees != 0 + + +@pytest.mark.asyncio +async def test_incentives(): + lpes = await LiquidityPoolEpoch.fetch_latest() + fees = sum(map(lambda lpe: lpe.total_fees, lpes)) + bribes = sum(map(lambda lpe: lpe.total_bribes, lpes)) + + assert fees != 0 + assert bribes != 0