Skip to content

Commit

Permalink
add tvl bot
Browse files Browse the repository at this point in the history
  • Loading branch information
callmephilip committed Nov 1, 2023
1 parent 5c5a732 commit fd0ecbb
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 46 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
DISCORD_TOKEN_PRICING=
DISCORD_TOKEN_TVL=
WEB3_PROVIDER_URI=https://mainnet.optimism.io
TARGET_NETWORK=Velodrome
LP_SUGAR_ADDRESS=0xa1F09427fa89b92e9B4e4c7003508C8614F19791
PRICE_ORACLE_ADDRESS=0x07F544813E9Fb63D57a92f28FbD3FF0f7136F5cE
PRICE_BATCH_SIZE=40
# token we are converting from
TOKEN_ADDRESS=0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db
CONNECTOR_TOKENS_ADDRESSES=0x9560e827aF36c94D2Ac33a39bCE1Fe78631088Db,0x4200000000000000000000000000000000000042,0x4200000000000000000000000000000000000006,0x8c6f28f2f1a3c87f0f938b96d27520d9751ec8d9,0x1f32b1c2345538c0c6f582fcb022739c4a194ebb,0xbfd291da8a403daaf7e5e9dc1ec0aceacd4848b9,0xc3864f98f2a61a7caeb95b039d031b4e2f55e0e9,0x9485aca5bbbe1667ad97c7fe7c4531a624c8b1ed,0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1
Expand Down
13 changes: 8 additions & 5 deletions bots/__main__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import asyncio
import logging

import discord

from .settings import (
DISCORD_TOKEN_PRICING,
DISCORD_TOKEN_TVL,
TOKEN_ADDRESS,
STABLE_TOKEN_ADDRESS,
TARGET_NETWORK,
)
from .data import Token
from .helpers import LOGGING_HANDLER, LOGGING_LEVEL
from .price import PriceBot
from .tvl import TVLBot


async def main():
Expand All @@ -24,10 +25,12 @@ async def main():
token = await Token.get_by_token_address(TOKEN_ADDRESS)
stable = await Token.get_by_token_address(STABLE_TOKEN_ADDRESS)

bot = PriceBot(
source_token=token, target_token=stable, intents=discord.Intents.default()
price_bot = PriceBot(source_token=token, target_token=stable)
tvl_bot = TVLBot(target_network=TARGET_NETWORK)

await asyncio.gather(
price_bot.start(DISCORD_TOKEN_PRICING), tvl_bot.start(DISCORD_TOKEN_TVL)
)
await bot.start(DISCORD_TOKEN_PRICING)


if __name__ == "__main__":
Expand Down
92 changes: 86 additions & 6 deletions bots/data.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import functools
import asyncio
from web3 import AsyncWeb3, AsyncHTTPProvider
from web3.constants import ADDRESS_ZERO
from dataclasses import dataclass
from typing import Tuple, List
from typing import Tuple, List, Dict

from .settings import (
WEB3_PROVIDER_URI,
Expand All @@ -13,8 +15,9 @@
STABLE_TOKEN_ADDRESS,
SUGAR_TOKENS_CACHE_MINUTES,
ORACLE_PRICES_CACHE_MINUTES,
PRICE_BATCH_SIZE,
)
from .helpers import cache_in_seconds, normalize_address
from .helpers import cache_in_seconds, normalize_address, chunk

w3 = AsyncWeb3(AsyncHTTPProvider(WEB3_PROVIDER_URI))

Expand All @@ -32,6 +35,9 @@ class Token:
decimals: int
listed: bool

def value_from_bigint(self, value: float) -> float:
return value / 10**self.decimals

@classmethod
def from_tuple(cls, t: Tuple):
(token_address, symbol, decimals, _, listed) = t
Expand Down Expand Up @@ -125,9 +131,83 @@ async def get_prices(
Defaults to CONNECTOR_TOKENS_ADDRESSES.
Returns:
_type_: _description_
List: list of Price objects
"""
# XX: lists are not cacheable, convert them to tuples so cache is happy
return await cls._get_prices(
tuple(tokens), stable_token, tuple(connector_tokens)
batches = await asyncio.gather(
# XX: lists are not cacheable, convert them to tuples so lru cache is happy
*map(
lambda ts: cls._get_prices(
tuple(ts), stable_token, tuple(connector_tokens)
),
list(chunk(tokens, PRICE_BATCH_SIZE)),
)
)
return functools.reduce(lambda l1, l2: l1 + l2, batches, [])


@dataclass(frozen=True)
class LiquidityPool:
"""Data class for Liquidity Pool
based on:
https://github.com/velodrome-finance/sugar/blob/v2/contracts/LpSugar.vy#L31
"""

lp: str
symbol: str
token0: Token
reserve0: float
token1: Token
reserve1: float

@classmethod
def from_tuple(cls, t: Tuple, tokens: Dict):
token0 = normalize_address(t[5])
token1 = normalize_address(t[8])

return LiquidityPool(
lp=normalize_address(t[0]),
symbol=t[1],
token0=tokens.get(token0),
reserve0=t[6],
token1=tokens.get(token1),
reserve1=t[9],
)

@classmethod
async def get_pools(cls):
tokens = await Token.get_all_listed_tokens()
tokens = {t.token_address: t for t in tokens}

sugar = w3.eth.contract(address=LP_SUGAR_ADDRESS, abi=LP_SUGAR_ABI)
pools = await sugar.functions.all(1000, 0, ADDRESS_ZERO).call()
return list(
filter(
lambda p: p is not None,
map(lambda p: LiquidityPool.from_tuple(p, tokens), pools),
)
)

@classmethod
async def tvl(cls, pools):
result = 0

tokens = await Token.get_all_listed_tokens()
prices = await Price.get_prices(tokens)
prices = {price.token.token_address: price for price in prices}

for pool in pools:
t0 = pool.token0
t1 = pool.token1

if t0:
result += (
t0.value_from_bigint(pool.reserve0) * prices[t0.token_address].price
)

if t1:
result += (
t1.value_from_bigint(pool.reserve1) * prices[t1.token_address].price
)

return result
7 changes: 6 additions & 1 deletion bots/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import sys


from typing import List
from web3 import Web3
from async_lru import alru_cache

Expand All @@ -25,6 +25,11 @@ def load_local_json_as_string(relative_path: str) -> str:
return result


def chunk(list_to_chunk: List, n: int):
for i in range(0, len(list_to_chunk), n):
yield list_to_chunk[i : i + n]


# logging
LOGGING_LEVEL = os.getenv("LOGGING_LEVEL", "DEBUG")
LOGGING_HANDLER = logging.StreamHandler(sys.stdout)
Expand Down
34 changes: 2 additions & 32 deletions bots/price.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import asyncio

import discord
from discord.ext import tasks

from .settings import BOT_TICKER_INTERVAL_MINUTES
from .data import Token, Price
from .helpers import LOGGER
from .ticker import TickerBot


class PriceBot(discord.Client):
class PriceBot(TickerBot):
def __init__(self, *args, source_token: Token, target_token: Token, **kwargs):
"""Create price bot for specific source token to target token
Expand All @@ -20,34 +18,11 @@ def __init__(self, *args, source_token: Token, target_token: Token, **kwargs):
self.source_token = source_token
self.target_token = target_token

async def setup_hook(self) -> None:
# start the task to run in the background
self.ticker.start()

async def on_ready(self):
LOGGER.debug(f"Logged in as {self.user} (ID: {self.user.id})")
LOGGER.debug("------")
await self.update_presence(self.source_token.symbol)

async def update_bot_member_nick(self, guild, nick: str):
bot_member = await guild.fetch_member(self.user.id)
if bot_member is None:
return
await bot_member.edit(nick=nick)

async def update_nick_for_all_servers(self, nick: str):
await asyncio.gather(
*map(lambda guild: self.update_bot_member_nick(guild, nick), self.guilds)
)

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
)
)

@tasks.loop(seconds=BOT_TICKER_INTERVAL_MINUTES * 60)
async def ticker(self):
try:
Expand All @@ -57,8 +32,3 @@ async def ticker(self):
)
except Exception as ex:
LOGGER.error(f"Ticker failed with {ex}")

@ticker.before_loop
async def before_my_task(self):
# wait until the bot logs in
await self.wait_until_ready()
4 changes: 4 additions & 0 deletions bots/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
PRICE_ORACLE_ABI = load_local_json_as_string("abi/price_oracle.json")

DISCORD_TOKEN_PRICING = os.environ["DISCORD_TOKEN_PRICING"]
DISCORD_TOKEN_TVL = os.environ["DISCORD_TOKEN_TVL"]
WEB3_PROVIDER_URI = os.environ["WEB3_PROVIDER_URI"]
LP_SUGAR_ADDRESS = os.environ["LP_SUGAR_ADDRESS"]
PRICE_ORACLE_ADDRESS = os.environ["PRICE_ORACLE_ADDRESS"]
PRICE_BATCH_SIZE = int(os.environ["PRICE_BATCH_SIZE"])

TARGET_NETWORK = os.environ["TARGET_NETWORK"]

# token we are converting from
TOKEN_ADDRESS = normalize_address(os.environ["TOKEN_ADDRESS"])
Expand Down
43 changes: 43 additions & 0 deletions bots/ticker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import asyncio

import discord
from discord.ext import tasks

from .settings import BOT_TICKER_INTERVAL_MINUTES


class TickerBot(discord.Client):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs, intents=discord.Intents.default())

async def setup_hook(self) -> None:
# start the task to run in the background
self.ticker.start()

async def update_bot_member_nick(self, guild, nick: str):
bot_member = await guild.fetch_member(self.user.id)
if bot_member is None:
return
await bot_member.edit(nick=nick)

async def update_nick_for_all_servers(self, nick: str):
await asyncio.gather(
*map(lambda guild: self.update_bot_member_nick(guild, nick), self.guilds)
)

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
)
)

@tasks.loop(seconds=BOT_TICKER_INTERVAL_MINUTES * 60)
async def ticker(self):
raise NotImplementedError

@ticker.before_loop
async def before_my_task(self):
# wait until the bot logs in
await self.wait_until_ready()
26 changes: 26 additions & 0 deletions bots/tvl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from discord.ext import tasks

from .settings import BOT_TICKER_INTERVAL_MINUTES
from .data import LiquidityPool
from .helpers import LOGGER
from .ticker import TickerBot


class TVLBot(TickerBot):
def __init__(self, *args, target_network: str, **kwargs):
super().__init__(*args, **kwargs)
self.target_network = target_network

async def on_ready(self):
LOGGER.debug(f"Logged in as {self.user} (ID: {self.user.id})")
LOGGER.debug("------")
await self.update_presence(f"TVL {self.target_network}")

@tasks.loop(seconds=BOT_TICKER_INTERVAL_MINUTES * 60)
async def ticker(self):
try:
pools = await LiquidityPool.get_pools()
tvl = await LiquidityPool.tvl(pools)
await self.update_nick_for_all_servers(f"{round(tvl/1000000, 2)}M")
except Exception as ex:
LOGGER.error(f"Ticker failed with {ex}")
11 changes: 9 additions & 2 deletions tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

load_dotenv(".env.example")

from bots.settings import TOKEN_ADDRESS # noqa
from bots.data import Token, Price # noqa
from bots.settings import TOKEN_ADDRESS # noqa
from bots.data import Token, Price, LiquidityPool # noqa


@pytest.mark.asyncio
Expand All @@ -21,3 +21,10 @@ async def test_get_prices():
assert len(prices) == 1
[price] = prices
assert price.pretty_price != 0


@pytest.mark.asyncio
async def test_tvl():
pools = await LiquidityPool.get_pools()
tvl = await LiquidityPool.tvl(pools)
assert tvl != 0

0 comments on commit fd0ecbb

Please sign in to comment.