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

Telliot Conditional Reporting (tellor is stale / max price change) #726

Merged
merged 24 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
152 changes: 152 additions & 0 deletions src/telliot_feeds/cli/commands/conditional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from typing import Optional
from typing import TypeVar

import click
from click.core import Context
from telliot_core.cli.utils import async_run

from telliot_feeds.cli.utils import common_options
from telliot_feeds.cli.utils import common_reporter_options
from telliot_feeds.cli.utils import get_accounts_from_name
from telliot_feeds.cli.utils import reporter_cli_core
from telliot_feeds.feeds import CATALOG_FEEDS
from telliot_feeds.reporters.customized.conditional_reporter import ConditionalReporter
from telliot_feeds.utils.cfg import check_endpoint
from telliot_feeds.utils.cfg import setup_config
from telliot_feeds.utils.log import get_logger

logger = get_logger(__name__)
T = TypeVar("T")


@click.group()
def conditional_reporter() -> None:
"""Report data on-chain."""
pass


@conditional_reporter.command()
@common_options
@common_reporter_options
@click.option(
"-pc",
"--percent-change",
help="Price change percentage for triggering a report. Default=0.01 (1%)",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default 50% comment

type=float,
default=0.01,
)
@click.option(
"-st",
"--stale-timeout",
help="Triggers a report when the oracle value is stale. Default=85500 (23.75 hours)",
type=int,
default=85500,
)
@click.pass_context
@async_run
async def conditional(
ctx: Context,
tx_type: int,
gas_limit: int,
max_fee_per_gas: Optional[float],
priority_fee_per_gas: Optional[float],
base_fee_per_gas: Optional[float],
legacy_gas_price: Optional[int],
expected_profit: str,
submit_once: bool,
wait_period: int,
password: str,
min_native_token_balance: float,
stake: float,
account_str: str,
check_rewards: bool,
gas_multiplier: int,
max_priority_fee_range: int,
percent_change: float,
stale_timeout: int,
query_tag: str,
unsafe: bool,
) -> None:
"""Report values to Tellor oracle if certain conditions are met."""
click.echo("Starting Conditional Reporter...")
ctx.obj["ACCOUNT_NAME"] = account_str
ctx.obj["SIGNATURE_ACCOUNT_NAME"] = None
if query_tag is None:
raise click.UsageError("--query-tag (-qt) is required. Use --help for a list of feeds with API support.")
datafeed = CATALOG_FEEDS.get(query_tag)
if datafeed is None:
raise click.UsageError(f"Invalid query tag: {query_tag}, enter a valid query tag with API support, use --help")

accounts = get_accounts_from_name(account_str)
if not accounts:
return
chain_id = accounts[0].chains[0]
ctx.obj["CHAIN_ID"] = chain_id # used in reporter_cli_core
# Initialize telliot core app using CLI context
async with reporter_cli_core(ctx) as core:

core._config, account = setup_config(core.config, account_name=account_str, unsafe=unsafe)

endpoint = check_endpoint(core._config)

if not endpoint or not account:
click.echo("Accounts and/or endpoint unset.")
click.echo(f"Account: {account}")
click.echo(f"Endpoint: {core._config.get_endpoint()}")
return

# Make sure current account is unlocked
if not account.is_unlocked:
account.unlock(password)

click.echo("Reporter settings:")
click.echo(f"Max tolerated price change: {percent_change * 100}%")
click.echo(f"Value considered stale after: {stale_timeout} seconds")
click.echo(f"Transaction type: {tx_type}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate

click.echo(f"Transaction type: {tx_type}")
click.echo(f"Gas Limit: {gas_limit}")
click.echo(f"Legacy gas price (gwei): {legacy_gas_price}")
click.echo(f"Max fee (gwei): {max_fee_per_gas}")
click.echo(f"Priority fee (gwei): {priority_fee_per_gas}")
click.echo(f"Desired stake amount: {stake}")
click.echo(f"Minimum native token balance (e.g. ETH if on Ethereum mainnet): {min_native_token_balance}")
click.echo("\n")

_ = input("Press [ENTER] to confirm settings.")

contracts = core.get_tellor360_contracts()

common_reporter_kwargs = {
"endpoint": core.endpoint,
"account": account,
"datafeed": datafeed,
"gas_limit": gas_limit,
"max_fee_per_gas": max_fee_per_gas,
"priority_fee_per_gas": priority_fee_per_gas,
"base_fee_per_gas": base_fee_per_gas,
"legacy_gas_price": legacy_gas_price,
"chain_id": core.config.main.chain_id,
"wait_period": wait_period,
"oracle": contracts.oracle,
"autopay": contracts.autopay,
"token": contracts.token,
"expected_profit": expected_profit,
"stake": stake,
"transaction_type": tx_type,
"min_native_token_balance": int(min_native_token_balance * 10**18),
"check_rewards": check_rewards,
"gas_multiplier": gas_multiplier,
"max_priority_fee_range": max_priority_fee_range,
}

reporter = ConditionalReporter(
stale_timeout=stale_timeout,
max_price_change=percent_change,
**common_reporter_kwargs,
)

if submit_once:
reporter.wait_period = 0
await reporter.report(report_count=1)
else:
await reporter.report()
2 changes: 2 additions & 0 deletions src/telliot_feeds/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from telliot_feeds.cli.commands.account import account
from telliot_feeds.cli.commands.catalog import catalog
from telliot_feeds.cli.commands.conditional import conditional
from telliot_feeds.cli.commands.config import config
from telliot_feeds.cli.commands.integrations import integrations
from telliot_feeds.cli.commands.liquity import liquity
Expand Down Expand Up @@ -51,6 +52,7 @@ def main(
main.add_command(liquity)
main.add_command(request_withdraw)
main.add_command(withdraw)
main.add_command(conditional)

if __name__ == "__main__":
main()
145 changes: 145 additions & 0 deletions src/telliot_feeds/reporters/customized/conditional_reporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import asyncio
from dataclasses import dataclass
from typing import Any
from typing import Optional
from typing import TypeVar

from web3 import Web3

from telliot_feeds.feeds import DataFeed
from telliot_feeds.reporters.tellor_360 import Tellor360Reporter
from telliot_feeds.utils.log import get_logger
from telliot_feeds.utils.reporter_utils import current_time

logger = get_logger(__name__)
T = TypeVar("T")


@dataclass
class GetDataBefore:
retrieved: bool
value: bytes
timestampRetrieved: int


@dataclass
class ConditionalReporter(Tellor360Reporter):
"""Backup Reporter that inherits from Tellor360Reporter and
implements conditions when intended as backup to chainlink"""

def __init__(
self,
stale_timeout: int,
max_price_change: float,
datafeed: Optional[DataFeed[Any]] = None,
*args: Any,
**kwargs: Any,
) -> None:
super().__init__(*args, **kwargs)
self.stale_timeout = stale_timeout
self.max_price_change = max_price_change
self.datafeed = datafeed

async def get_tellor_latest_data(self) -> Optional[GetDataBefore]:
"""Get latest data from tellor oracle (getDataBefore with current time)

Returns:
- Optional[GetDataBefore]: latest data from tellor oracle
"""
if self.datafeed is None:
logger.debug(f"no datafeed set: {self.datafeed}")
return None
data, status = await self.oracle.read("getDataBefore", self.datafeed.query.query_id, current_time())
if not status.ok:
logger.warning(f"error getting tellor data: {status.e}")
return None
return GetDataBefore(*data)

async def get_telliot_feed_data(self, datafeed: DataFeed[Any]) -> Optional[float]:
"""Fetch spot price data from API sources and calculate a value
Returns:
- Optional[GetDataBefore]: latest data from tellor oracle
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should current data, right?

Copy link
Contributor Author

@0xSpuddy 0xSpuddy Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was my first try, butliquity backup used getDataBefore, then I used the same so I could use the same typing. Any advantage to using getCurrentData?

"""
v, _ = await datafeed.source.fetch_new_datapoint()
logger.info(f"telliot feeds value: {v}")
return v

def tellor_price_change_above_max(
self, tellor_latest_data: GetDataBefore, telliot_feed_data: Optional[float]
) -> bool:
"""Check if spot price change since last report is above max price deviation
params:
- tellor_latest_data: latest data from tellor oracle
- telliot_feed_data: latest data from API sources

Returns:
- bool: True if price change is above max price deviation, False otherwise
"""
oracle_price = (Web3.toInt(tellor_latest_data.value)) / 10**18
feed_price = telliot_feed_data if telliot_feed_data else None

if feed_price is None:
logger.warning("No feed data available")
return False

min_price = min(oracle_price, feed_price)
max_price = max(oracle_price, feed_price)
logger.info(f"Latest Tellor price = {oracle_price}")
percent_change = (max_price - min_price) / max_price
logger.info(f"feed price change = {percent_change}")
if percent_change > self.max_price_change:
logger.info("Feed price change above max")
return True
else:
return False

async def conditions_met(self) -> bool:
"""Trigger methods to check conditions if reporting spot is necessary

Returns:
- bool: True if conditions are met, False otherwise
"""
logger.info("checking conditions and reporting if necessary")
if self.datafeed is None:
logger.info(f"no datafeed was setß: {self.datafeed}. Please provide a spot-price query type (see --help)")
return False
tellor_latest_data = await self.get_tellor_latest_data()
telliot_feed_data = await self.get_telliot_feed_data(datafeed=self.datafeed)
time = current_time()
time_passed_since_tellor_report = time - tellor_latest_data.timestampRetrieved if tellor_latest_data else time
if tellor_latest_data is None:
logger.debug("tellor data returned None")
return True
elif not tellor_latest_data.retrieved:
logger.debug(f"No oracle submissions in tellor for query: {self.datafeed.query.descriptor}")
return True
elif time_passed_since_tellor_report > self.stale_timeout:
logger.debug(f"tellor data is stale, time elapsed since last report: {time_passed_since_tellor_report}")
return True
elif self.tellor_price_change_above_max(tellor_latest_data, telliot_feed_data):
logger.debug("tellor price change above max")
return True
else:
logger.debug(f"tellor {self.datafeed.query.descriptor} data is recent enough")
return False

async def report(self, report_count: Optional[int] = None) -> None:
"""Submit values to Tellor oracles on an interval."""

while report_count is None or report_count > 0:
online = await self.is_online()
if online:
if self.has_native_token():
if await self.conditions_met():
_, _ = await self.report_once()
else:
logger.info("feeds are recent enough, no need to report")

else:
logger.warning("Unable to connect to the internet!")

logger.info(f"Sleeping for {self.wait_period} seconds")
await asyncio.sleep(self.wait_period)

if report_count is not None:
report_count -= 1
2 changes: 1 addition & 1 deletion src/telliot_feeds/sources/price/spot/coingecko.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"uni": "uniswap",
"usdt": "tether",
"yfi": "yearn-finance",
"steth": "staked-ether",
"steth": "lido-staked-ether",
"reth": "rocket-pool-eth",
"op": "optimism",
"grt": "the-graph",
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ def mumbai_test_cfg():


@pytest.fixture(scope="function", autouse=True)
def goerli_test_cfg():
return local_node_cfg(chain_id=5)
def sepolia_test_cfg():
return local_node_cfg(chain_id=11155111)


@pytest.fixture(scope="function", autouse=True)
Expand Down
Loading
Loading