diff --git a/src/tellor_disputables/data.py b/src/tellor_disputables/data.py index 5c71a6a..34821d1 100644 --- a/src/tellor_disputables/data.py +++ b/src/tellor_disputables/data.py @@ -34,6 +34,7 @@ from tellor_disputables import ALWAYS_ALERT_QUERY_TYPES from tellor_disputables import NEW_REPORT_ABI +from tellor_disputables.discord import send_discord_msg from tellor_disputables.utils import are_all_attributes_none from tellor_disputables.utils import disputable_str from tellor_disputables.utils import get_logger @@ -431,6 +432,10 @@ async def parse_new_report_event( except eth_abi.exceptions.DecodingError: new_report.value = event_data.args._value + if new_report.query_type == "SpotPrice": + if len(event_data.args._value) != 32: + send_discord_msg("Spot price value length is not 32 bytes") + # if query of event matches a query type of the monitored feeds, fill the query parameters monitored_feed = None @@ -456,7 +461,7 @@ async def parse_new_report_event( if feed_qid == new_report.query_id: if new_report.query_type == "SpotPrice": catalog_entry = query_catalog.find(query_id=new_report.query_id) - mf.feed = CATALOG_FEEDS.get(catalog_entry[0].tag) + mf.feed = get_feed_from_catalog(catalog_entry[0].tag) else: @@ -486,7 +491,7 @@ async def parse_new_report_event( catalog = query_catalog.find(query_id=new_report.query_id) if catalog: tag = catalog[0].tag - feed = CATALOG_FEEDS.get(tag) + feed = get_feed_from_catalog(tag) if feed is None: logger.error(f"Unable to find feed for tag {tag}") return None @@ -571,3 +576,7 @@ def get_block_number_at_timestamp(cfg: TelliotConfig, timestamp: int) -> Any: estimated_block_number = block_a.number + estimated_block_delta return int(estimated_block_number) + + +def get_feed_from_catalog(tag: str) -> Optional[DataFeed]: + return CATALOG_FEEDS.get(tag) diff --git a/src/tellor_disputables/discord.py b/src/tellor_disputables/discord.py index 7ff32cc..65c2910 100644 --- a/src/tellor_disputables/discord.py +++ b/src/tellor_disputables/discord.py @@ -1,11 +1,11 @@ """Send text messages using Twilio.""" import os +from typing import Any import click from discordwebhook import Discord from tellor_disputables import ALWAYS_ALERT_QUERY_TYPES -from tellor_disputables.data import NewReport def generic_alert(msg: str) -> None: @@ -38,7 +38,7 @@ def dispute_alert(msg: str) -> None: return -def alert(all_values: bool, new_report: NewReport) -> None: +def alert(all_values: bool, new_report: Any) -> None: if new_report.query_type in ALWAYS_ALERT_QUERY_TYPES: msg = generate_alert_msg(False, new_report.link) diff --git a/tests/test_auto_dispute_multiple_ids.py b/tests/test_auto_dispute_multiple_ids.py index cf9a037..cd058a4 100644 --- a/tests/test_auto_dispute_multiple_ids.py +++ b/tests/test_auto_dispute_multiple_ids.py @@ -13,8 +13,11 @@ from chained_accounts import ChainedAccount from telliot_core.apps.core import TelliotConfig from telliot_core.apps.core import TelliotCore +from telliot_feeds.datasource import DataSource from telliot_feeds.dtypes.datapoint import datetime_now_utc +from telliot_feeds.dtypes.datapoint import OptionalDataPoint from telliot_feeds.feeds import DATAFEED_BUILDER_MAPPING +from telliot_feeds.feeds import eth_usd_median_feed from telliot_feeds.feeds import evm_call_feed_example from telliot_feeds.queries.price.spot_price import SpotPrice from web3 import Web3 @@ -163,7 +166,9 @@ async def setup_and_start(is_disputing, config, config_patches=None): # using exit stack makes nested patching easier to read with ExitStack() as stack: stack.enter_context(patch("getpass.getpass", return_value="")) - stack.enter_context(patch("tellor_disputables.discord.send_discord_msg", side_effect=print("alert sent"))) + stack.enter_context( + patch("tellor_disputables.discord.send_discord_msg", side_effect=lambda _: print("alert sent")) + ) stack.enter_context(patch("tellor_disputables.cli.TelliotConfig", new=lambda: config)) stack.enter_context(patch("telliot_feeds.feeds.evm_call_feed.source.cfg", config)) stack.enter_context( @@ -384,3 +389,83 @@ async def test_evmcall_right_value_wrong_timestamp(submit_multiple_bad_values: A result = matching_rows[0] if matching_rows else None assert result is not None, "Expected row not found." assert expected in ",".join(result), "Expected row not in the response." + + +value = 3400 + + +class FakeDataSource(DataSource[float]): + async def fetch_new_datapoint(self) -> OptionalDataPoint[float]: + return value, datetime_now_utc() + + +@pytest.mark.asyncio +async def test_spot_short_value(stake_deposited: Awaitable[TelliotCore], capsys): + + core = await stake_deposited + core.config.endpoints.endpoints = [core.config.endpoints.find(chain_id=1337)[0]] + contracts = core.get_tellor360_contracts() + w3 = core.endpoint._web3 + oracle = contracts.oracle + submit_value = oracle.contract.get_function_by_name("submitValue") + # submit fake eth value + submit_value_txn = submit_value( + eth_query_id, int.to_bytes(int(value * 1e18), 20, "big"), 0, eth_query_data + ).buildTransaction(txn_kwargs(w3)) + submit_value_hash = w3.eth.send_transaction(submit_value_txn) + receipt = w3.eth.wait_for_transaction_receipt(submit_value_hash) + assert receipt["status"] == 1 + + eth_usd_median_feed.source = FakeDataSource() + eth_config = { + "feeds": [ + {"query_id": eth_query_id, "threshold": {"type": "Percentage", "amount": 0.75}}, + ] + } + config_patches = [ + patch("builtins.open", side_effect=custom_open_side_effect), + patch("yaml.safe_load", return_value=eth_config), + patch("tellor_disputables.data.get_feed_from_catalog", return_value=eth_usd_median_feed), + patch( + "tellor_disputables.data.send_discord_msg", + side_effect=lambda _: print("Spot price value length is not 32 bytes"), + ), + ] + await setup_and_start(False, core.config, config_patches) + assert "Spot price value length is not 32 bytes" in capsys.readouterr().out + + +@pytest.mark.asyncio +async def test_spot_long_value(stake_deposited: Awaitable[TelliotCore], capsys): + + core = await stake_deposited + core.config.endpoints.endpoints = [core.config.endpoints.find(chain_id=1337)[0]] + contracts = core.get_tellor360_contracts() + w3 = core.endpoint._web3 + oracle = contracts.oracle + submit_value = oracle.contract.get_function_by_name("submitValue") + # submit fake eth value + submit_value_txn = submit_value( + eth_query_id, int.to_bytes(int(value * 1e18), 33, "big"), 0, eth_query_data + ).buildTransaction(txn_kwargs(w3)) + submit_value_hash = w3.eth.send_transaction(submit_value_txn) + receipt = w3.eth.wait_for_transaction_receipt(submit_value_hash) + assert receipt["status"] == 1 + + eth_usd_median_feed.source = FakeDataSource() + eth_config = { + "feeds": [ + {"query_id": eth_query_id, "threshold": {"type": "Percentage", "amount": 0.75}}, + ] + } + config_patches = [ + patch("builtins.open", side_effect=custom_open_side_effect), + patch("yaml.safe_load", return_value=eth_config), + patch("tellor_disputables.data.get_feed_from_catalog", return_value=eth_usd_median_feed), + patch( + "tellor_disputables.data.send_discord_msg", + side_effect=lambda _: print("Spot price value length is not 32 bytes"), + ), + ] + await setup_and_start(False, core.config, config_patches) + assert "Spot price value length is not 32 bytes" in capsys.readouterr().out diff --git a/tests/test_disputer.py b/tests/test_disputer.py index 772b509..8d58795 100644 --- a/tests/test_disputer.py +++ b/tests/test_disputer.py @@ -213,7 +213,7 @@ async def test_get_dispute_fee(): chain_id=80001, provider="polygon", network="mumbai", - url="https://polygon-mumbai-bor-rpc.publicnode.com", + url="https://polygon-mumbai.api.onfinality.io/public", explorer="https://mumbai.polygonscan.com/", ) # todo: use mock instead