Skip to content

Commit

Permalink
Merge pull request #726 from tellor-io/conditional-report
Browse files Browse the repository at this point in the history
Telliot Conditional Reporting (tellor is stale / max price change)
  • Loading branch information
0xSpuddy authored Dec 19, 2023
2 parents 0f82b33 + f2aed06 commit 84dec04
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 3 deletions.
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%)",
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}")
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
"""
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

0 comments on commit 84dec04

Please sign in to comment.