diff --git a/contracts/SampleReporterContract/SampleFlexReporter.sol b/contracts/SampleReporterContract/SampleFlexReporter.sol index ece3e4b1..82da0994 100644 --- a/contracts/SampleReporterContract/SampleFlexReporter.sol +++ b/contracts/SampleReporterContract/SampleFlexReporter.sol @@ -3,17 +3,62 @@ pragma solidity ^0.8.15; interface IFlex { function balanceOf(address account) external view returns (uint256); + + function allowance( + address _user, + address _spender + ) external view returns (uint256); + function approve(address _spender, uint256 _amount) external returns (bool); - function transfer(address recipient, uint256 amount) external returns (bool); - function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + function transfer( + address recipient, + uint256 amount + ) external returns (bool); + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); + function depositStake(uint256 _amount) external; + function requestStakingWithdraw(uint256 _amount) external; + function getCurrentTip(bytes32 _queryId) external view returns (uint256); - function submitValue(bytes32 _queryId, bytes calldata _value, uint256 _nonce, bytes memory _queryData) external; + + function submitValue( + bytes32 _queryId, + bytes calldata _value, + uint256 _nonce, + bytes memory _queryData + ) external; + function withdrawStake() external; + function stakeAmount() external view returns (uint256); - function getStakerInfo(address _staker) external view returns (uint256, uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256); + function getStakerInfo( + address _staker + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256 + ); + + function getNewValueCountbyQueryId( + bytes32 _queryId + ) external view returns (uint256); } contract SampleFlexReporter { @@ -21,9 +66,14 @@ contract SampleFlexReporter { IFlex public autopay; IFlex public token; address public owner; - uint256 public profitThreshold;//inTRB - - constructor(address _oracle, address _autopay, address _token, uint256 _profitThreshold){ + uint256 public profitThreshold; //inTRB + + constructor( + address _oracle, + address _autopay, + address _token, + uint256 _profitThreshold + ) { oracle = IFlex(_oracle); autopay = IFlex(_autopay); token = IFlex(_token); @@ -31,7 +81,7 @@ contract SampleFlexReporter { profitThreshold = _profitThreshold; } - modifier onlyOwner { + modifier onlyOwner() { require(msg.sender == owner, "Only owner can call this function."); _; } @@ -40,7 +90,7 @@ contract SampleFlexReporter { owner = _newOwner; } - function depositStake(uint256 _amount) onlyOwner external{ + function depositStake(uint256 _amount) external onlyOwner { oracle.depositStake(_amount); } @@ -48,35 +98,79 @@ contract SampleFlexReporter { oracle.requestStakingWithdraw(_amount); } - function submitValue(bytes32 _queryId, bytes memory _value, uint256 _nonce, bytes memory _queryData) onlyOwner external{ + function submitValue( + bytes32 _queryId, + bytes memory _value, + uint256 _nonce, + bytes memory _queryData + ) external onlyOwner { uint256 _reward; _reward = autopay.getCurrentTip(_queryId); require(_reward > profitThreshold, "profit threshold not met"); - oracle.submitValue(_queryId,_value,_nonce,_queryData); + oracle.submitValue(_queryId, _value, _nonce, _queryData); + } + + function submitValueBypass( + bytes32 _queryId, + bytes memory _value, + uint256 _nonce, + bytes memory _queryData + ) external onlyOwner { + oracle.submitValue(_queryId, _value, _nonce, _queryData); } - function submitValueBypass(bytes32 _queryId, bytes memory _value, uint256 _nonce, bytes memory _queryData) onlyOwner external{ - oracle.submitValue(_queryId,_value,_nonce,_queryData); + function transfer(address _to, uint256 _amount) external onlyOwner { + token.transfer(_to, _amount); } - function transfer(address _to, uint256 _amount) external onlyOwner{ - token.transfer(_to,_amount); + function balanceOf(address account) external view returns (uint256) { + return token.balanceOf(address(this)); } - function approve(uint256 _amount) external onlyOwner{ - token.approve(address(oracle), _amount); + function allowance( + address owner, + address spender + ) external view returns (uint256) { + return token.allowance(address(this), address(oracle)); } - function withdrawStake() onlyOwner external{ + function approve(address spender, uint256 amount) external onlyOwner { + token.approve(address(oracle), amount); + } + + function withdrawStake() external onlyOwner { oracle.withdrawStake(); } - function getStakeAmount() external view returns (uint256){ + function getStakeAmount() external view returns (uint256) { return oracle.stakeAmount(); } - function getStakerInfo(address _stakerAddress) external view returns (uint256, uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256){ - return oracle.getStakerInfo(_stakerAddress); + function getStakerInfo( + address _stakerAddress + ) + external + view + returns ( + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256, + uint256 + ) + { + return oracle.getStakerInfo(address(this)); + } + + function getNewValueCountbyQueryId( + bytes32 _queryId + ) external view returns (uint256) { + return oracle.getNewValueCountbyQueryId(_queryId); } + receive() external payable {} } diff --git a/contracts/SampleReporterContract/SampleXReporter.sol b/contracts/SampleReporterContract/SampleXReporter.sol deleted file mode 100644 index 0541420b..00000000 --- a/contracts/SampleReporterContract/SampleXReporter.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - - -interface ITellor{ - function submitValue(bytes32 _queryId, bytes calldata _value, uint256 _nonce, bytes memory _queryData) external; - function getAddressVars(bytes32 _data) external view returns (address); - function depositStake() external; - function requestStakingWithdraw() external; - function getCurrentReward(bytes32 _queryId) external view returns(uint256, uint256); - function transfer(address _to, uint256 _amount)external returns (bool success); - function withdrawStake() external; -} - -contract Reporter { - ITellor public tellor; - ITellor public oracle; - address public owner; - uint256 public profitThreshold;//inTRB - - constructor(address _tellorAddress, address _oracleAddress, uint256 _profitThreshold){ - tellor = ITellor(_tellorAddress); - oracle = ITellor(_oracleAddress);//keccak256(_ORACLE_CONTRACT) - owner = msg.sender; - profitThreshold = _profitThreshold; - } - - modifier onlyOwner { - require(msg.sender == owner, "Only owner can call this function."); - _; - } - - function changeOwner(address _newOwner) external onlyOwner { - owner = _newOwner; - } - - function depositStake() onlyOwner external{ - tellor.depositStake(); - } - - function requestStakingWithdraw() external onlyOwner { - tellor.requestStakingWithdraw(); - } - - function submitValue(bytes32 _queryId, bytes memory _value, uint256 _nonce, bytes memory _queryData) onlyOwner external{ - uint256 _reward; - (,_reward) = oracle.getCurrentReward(_queryId); - require(_reward > profitThreshold, "profit threshold not met"); - oracle.submitValue(_queryId,_value,_nonce,_queryData); - } - - function submitValueBypass(bytes32 _queryId, bytes memory _value, uint256 _nonce, bytes memory _queryData) onlyOwner external{ - oracle.submitValue(_queryId,_value,_nonce,_queryData); - } - - function transfer(address _to, uint256 _amount) external onlyOwner{ - tellor.transfer(_to,_amount); - } - - function withdrawStake() onlyOwner external{ - tellor.withdrawStake(); - } -} diff --git a/contracts/TellorX/TellorXMasterMock.sol b/contracts/TellorX/TellorXMasterMock.sol deleted file mode 100644 index d58dff12..00000000 --- a/contracts/TellorX/TellorXMasterMock.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-Licence-Identifier: MIT -pragma solidity ^0.8.10; - -contract TellorXMasterMock { - - function getStakerInfo(address _staker) - external view - returns (uint256, uint256) - { - return (uint256(1), uint256(123456789)); - } - -} diff --git a/contracts/TellorX/TellorXOracleMock.sol b/contracts/TellorX/TellorXOracleMock.sol deleted file mode 100644 index 57771c64..00000000 --- a/contracts/TellorX/TellorXOracleMock.sol +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-Licence-Identifier: MIT -pragma solidity ^0.8.10; - -contract TellorXOracleMock { - function getReportTimestampByIndex(bytes32 _queryId, uint256 _index) - public - pure - returns (uint256) - { - return 1234; - } - - function getReportingLock() public pure returns (uint256) { - return 12; - } - - function getTimeBasedReward() public pure returns (uint256) { - return 1e18; - } - - function getCurrentReward(bytes32 _queryId) - public - pure - returns (uint256[2] memory) - { - return [uint256(1e18), uint256(2e18)]; - } - - function getTimestampCountById(bytes32 _queryId) - public - pure - returns (uint256) - { - return 30; - } - - function getTimeOfLastNewValue() public pure returns (uint256) { - return 123456789; - } - - function getTipsById(bytes32 _queryId) public pure returns (uint256) { - return 3e18; - } - - function getReporterLastTimestamp(address _reporter) - external - view - returns (uint256) - { - return 123456789; - } - - function submitValue( - bytes32 _queryId, - bytes calldata _value, - uint256 _nonce, - bytes memory _queryData - ) external { - } -} diff --git a/src/telliot_feeds/cli/commands/liquity.py b/src/telliot_feeds/cli/commands/liquity.py index a32c663a..6fa87c75 100644 --- a/src/telliot_feeds/cli/commands/liquity.py +++ b/src/telliot_feeds/cli/commands/liquity.py @@ -6,6 +6,7 @@ from web3 import Web3 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 @@ -26,6 +27,7 @@ def liquity_reporter() -> None: @liquity_reporter.command() @common_options +@common_reporter_options @click.option( "-clf", "--chainlink-feed", @@ -39,8 +41,9 @@ async def liquity( ctx: Context, tx_type: int, gas_limit: int, - max_fee: Optional[float], - priority_fee: Optional[float], + 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, @@ -56,6 +59,7 @@ async def liquity( frozen_timeout: int, query_tag: str, chainlink_feed: str, + unsafe: bool, ) -> None: """Report values to Tellor oracle if certain conditions are met.""" click.echo("Starting Liquity Backup Reporter...") @@ -82,13 +86,10 @@ async def liquity( raise click.UsageError("Invalid chain link feed address") ctx.obj["CHAIN_ID"] = chain_id # used in reporter_cli_core - # if max_fee flag is set, then priority_fee must also be set - if (max_fee is not None and priority_fee is None) or (max_fee is None and priority_fee is not None): - raise click.UsageError("Must specify both max fee and priority fee") # 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) + core._config, account = setup_config(core.config, account_name=account_str, unsafe=unsafe) endpoint = check_endpoint(core._config) @@ -108,8 +109,8 @@ async def liquity( 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}") - click.echo(f"Priority fee (gwei): {priority_fee}") + 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") @@ -123,8 +124,9 @@ async def liquity( "account": account, "datafeed": datafeed, "gas_limit": gas_limit, - "max_fee": max_fee, - "priority_fee": priority_fee, + "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, diff --git a/src/telliot_feeds/cli/commands/report.py b/src/telliot_feeds/cli/commands/report.py index 0c7ca691..e9503863 100644 --- a/src/telliot_feeds/cli/commands/report.py +++ b/src/telliot_feeds/cli/commands/report.py @@ -1,5 +1,6 @@ from typing import Any from typing import Optional +from typing import Union import click from chained_accounts import find_accounts @@ -10,6 +11,7 @@ from telliot_feeds.cli.utils import build_feed_from_input 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 print_reporter_settings from telliot_feeds.cli.utils import reporter_cli_core @@ -24,7 +26,6 @@ from telliot_feeds.reporters.flashbot import FlashbotsReporter from telliot_feeds.reporters.rng_interval import RNGReporter from telliot_feeds.reporters.tellor_360 import Tellor360Reporter -from telliot_feeds.reporters.tellor_flex import TellorFlexReporter from telliot_feeds.utils.cfg import check_endpoint from telliot_feeds.utils.cfg import setup_config from telliot_feeds.utils.log import get_logger @@ -52,6 +53,7 @@ def reporter() -> None: ) @reporter.command() @common_options +@common_reporter_options @click.option( "--build-feed", "-b", @@ -129,13 +131,6 @@ def reporter() -> None: callback=validate_address, prompt=False, ) -@click.option( - "--tellor-360/--tellor-flex", - "-360/-flex", - "tellor_360", - default=True, - help="Choose between Tellor 360 or Flex contracts", -) @click.option( "--random-feeds/--no-random-feeds", "-rf/-nrf", @@ -158,8 +153,9 @@ async def report( build_feed: bool, tx_type: int, gas_limit: int, - max_fee: Optional[float], - priority_fee: Optional[float], + base_fee_per_gas: Optional[float], + priority_fee_per_gas: Optional[float], + max_fee_per_gas: Optional[float], legacy_gas_price: Optional[int], expected_profit: str, submit_once: bool, @@ -175,7 +171,6 @@ async def report( custom_token_contract: Optional[ChecksumAddress], custom_oracle_contract: Optional[ChecksumAddress], custom_autopay_contract: Optional[ChecksumAddress], - tellor_360: bool, stake: float, account_str: str, signature_account: str, @@ -195,9 +190,6 @@ async def report( return ctx.obj["CHAIN_ID"] = accounts[0].chains[0] # used in reporter_cli_core - # if max_fee flag is set, then priority_fee must also be set - if (max_fee is not None and priority_fee is None) or (max_fee is None and priority_fee is not None): - raise click.UsageError("Must specify both max fee and priority fee") # Initialize telliot core app using CLI context async with reporter_cli_core(ctx) as core: @@ -253,8 +245,9 @@ async def report( query_tag=query_tag, transaction_type=tx_type, gas_limit=gas_limit, - max_fee=max_fee, - priority_fee=priority_fee, + max_fee=base_fee_per_gas, + priority_fee=priority_fee_per_gas, + base_fee=base_fee_per_gas, legacy_gas_price=legacy_gas_price, expected_profit=expected_profit, chain_id=core.config.main.chain_id, @@ -266,7 +259,7 @@ async def report( if not unsafe: _ = input("Press [ENTER] to confirm settings.") - contracts = core.get_tellor360_contracts() if tellor_360 else core.get_tellorflex_contracts() + contracts = core.get_tellor360_contracts() if custom_oracle_contract: contracts.oracle.connect() # set telliot_core.contract.Contract.contract attribute @@ -309,8 +302,9 @@ async def report( "account": account, "datafeed": chosen_feed, "gas_limit": gas_limit, - "max_fee": max_fee, - "priority_fee": priority_fee, + "base_fee_per_gas": base_fee_per_gas, + "priority_fee_per_gas": priority_fee_per_gas, + "max_fee_per_gas": max_fee_per_gas, "legacy_gas_price": legacy_gas_price, "chain_id": core.config.main.chain_id, "wait_period": wait_period, @@ -327,7 +321,7 @@ async def report( "max_priority_fee_range": max_priority_fee_range, "ignore_tbr": ignore_tbr, } - + reporter: Union[FlashbotsReporter, RNGReporter, Tellor360Reporter] if sig_acct_addr: reporter = FlashbotsReporter( signature_account=sig_account, @@ -335,7 +329,7 @@ async def report( ) elif rng_auto: common_reporter_kwargs["wait_period"] = 120 if wait_period < 120 else wait_period - reporter = RNGReporter( # type: ignore + reporter = RNGReporter( **common_reporter_kwargs, ) elif reporting_diva_protocol: @@ -348,14 +342,10 @@ async def report( **common_reporter_kwargs, **diva_reporter_kwargs, # type: ignore ) - elif tellor_360: - reporter = Tellor360Reporter( - **common_reporter_kwargs, - ) # type: ignore else: - reporter = TellorFlexReporter( + reporter = Tellor360Reporter( **common_reporter_kwargs, - ) # type: ignore + ) if submit_once: _, _ = await reporter.report_once() diff --git a/src/telliot_feeds/cli/commands/request_withdraw_stake.py b/src/telliot_feeds/cli/commands/request_withdraw_stake.py new file mode 100644 index 00000000..3add5ca8 --- /dev/null +++ b/src/telliot_feeds/cli/commands/request_withdraw_stake.py @@ -0,0 +1,61 @@ +from typing import Optional + +import click +from click.core import Context +from telliot_core.cli.utils import async_run + +from telliot_feeds.cli.utils import call_oracle +from telliot_feeds.cli.utils import common_options +from telliot_feeds.cli.utils import get_accounts_from_name + + +@click.group() +def request_withdraw_stake() -> None: + """Request to withdraw tokens from the Tellor oracle which locks them for 7 days.""" + pass + + +@request_withdraw_stake.command() +@common_options +@click.option( + "--amount", "-amt", "amount", help="Amount of tokens to request withdraw", nargs=1, type=float, required=True +) +@click.pass_context +@async_run +async def request_withdraw( + ctx: Context, + account_str: str, + amount: float, + tx_type: int, + gas_limit: int, + base_fee_per_gas: Optional[float], + priority_fee_per_gas: Optional[float], + max_fee_per_gas: Optional[float], + legacy_gas_price: Optional[int], + password: str, + min_native_token_balance: float, + gas_multiplier: int, + max_priority_fee_range: int, +) -> None: + """Request withdraw of tokens from oracle""" + ctx.obj["ACCOUNT_NAME"] = account_str + + accounts = get_accounts_from_name(account_str) + if not accounts: + return + + ctx.obj["CHAIN_ID"] = accounts[0].chains[0] # used in reporter_cli_core + + user_inputs = { + "password": password, + "min_native_token_balance": min_native_token_balance, + "gas_limit": gas_limit, + "base_fee_per_gas": base_fee_per_gas, + "priority_fee_per_gas": priority_fee_per_gas, + "max_fee_per_gas": max_fee_per_gas, + "legacy_gas_price": legacy_gas_price, + "transaction_type": tx_type, + "gas_multiplier": gas_multiplier, + "max_priority_fee_range": max_priority_fee_range, + } + await call_oracle(ctx=ctx, func="requestStakingWithdraw", user_inputs=user_inputs, _amount=int(amount * 1e18)) diff --git a/src/telliot_feeds/cli/commands/stake.py b/src/telliot_feeds/cli/commands/stake.py new file mode 100644 index 00000000..c3f8a245 --- /dev/null +++ b/src/telliot_feeds/cli/commands/stake.py @@ -0,0 +1,94 @@ +from typing import Optional + +import click +from click.core import Context +from eth_utils import to_checksum_address +from telliot_core.cli.utils import async_run + +from telliot_feeds.cli.utils import common_options +from telliot_feeds.cli.utils import get_accounts_from_name +from telliot_feeds.cli.utils import reporter_cli_core +from telliot_feeds.reporters.stake import Stake +from telliot_feeds.utils.cfg import check_endpoint +from telliot_feeds.utils.cfg import setup_config +from telliot_feeds.utils.reporter_utils import has_native_token_funds + + +@click.group() +def deposit_stake() -> None: + """Deposit tokens to the Tellor oracle.""" + pass + + +@deposit_stake.command() +@common_options +@click.option("--amount", "-amt", "amount", help="Amount of tokens to stake", nargs=1, type=float, required=True) +@click.pass_context +@async_run +async def stake( + ctx: Context, + account_str: str, + amount: float, + tx_type: int, + gas_limit: int, + base_fee_per_gas: Optional[float], + priority_fee_per_gas: Optional[float], + max_fee_per_gas: Optional[float], + legacy_gas_price: Optional[int], + password: str, + min_native_token_balance: float, + gas_multiplier: int, + max_priority_fee_range: int, +) -> None: + """Deposit tokens to oracle""" + ctx.obj["ACCOUNT_NAME"] = account_str + + accounts = get_accounts_from_name(account_str) + if not accounts: + return + + ctx.obj["CHAIN_ID"] = accounts[0].chains[0] # 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) + + 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) + + contracts = core.get_tellor360_contracts() + # set private key for token approval txn via token contract + contracts.token._private_key = account.local_account.key + # set private key for oracle stake deposit txn + contracts.oracle._private_key = account.local_account.key + + if has_native_token_funds( + to_checksum_address(account.address), + core.endpoint.web3, + min_balance=int(min_native_token_balance * 10**18), + ): + s = Stake( + endpoint=core.endpoint, + account=account, + transaction_type=tx_type, + gas_limit=gas_limit, + legacy_gas_price=legacy_gas_price, + gas_multiplier=gas_multiplier, + max_priority_fee_range=max_priority_fee_range, + priority_fee_per_gas=priority_fee_per_gas, + base_fee_per_gas=base_fee_per_gas, + max_fee_per_gas=max_fee_per_gas, + oracle=contracts.oracle, + token=contracts.token, + min_native_token_balance=min_native_token_balance, + ) + _ = await s.deposit_stake(int(amount * 1e18)) diff --git a/src/telliot_feeds/cli/commands/withdraw.py b/src/telliot_feeds/cli/commands/withdraw.py new file mode 100644 index 00000000..d81f32ed --- /dev/null +++ b/src/telliot_feeds/cli/commands/withdraw.py @@ -0,0 +1,57 @@ +from typing import Optional + +import click +from click.core import Context +from telliot_core.cli.utils import async_run + +from telliot_feeds.cli.utils import call_oracle +from telliot_feeds.cli.utils import common_options +from telliot_feeds.cli.utils import get_accounts_from_name + + +@click.group() +def withdraw_stake() -> None: + """Withdraw staked tokens from the oracle after 7 days.""" + pass + + +@withdraw_stake.command() +@common_options +@click.pass_context +@async_run +async def withdraw( + ctx: Context, + account_str: str, + tx_type: int, + gas_limit: int, + base_fee_per_gas: Optional[float], + priority_fee_per_gas: Optional[float], + max_fee_per_gas: Optional[float], + legacy_gas_price: Optional[int], + password: str, + min_native_token_balance: float, + gas_multiplier: int, + max_priority_fee_range: int, +) -> None: + """Withdraw of tokens from oracle""" + ctx.obj["ACCOUNT_NAME"] = account_str + + accounts = get_accounts_from_name(account_str) + if not accounts: + return + + ctx.obj["CHAIN_ID"] = accounts[0].chains[0] # used in reporter_cli_core + + user_inputs = { + "password": password, + "min_native_token_balance": min_native_token_balance, + "gas_limit": gas_limit, + "base_fee_per_gas": base_fee_per_gas, + "priority_fee_per_gas": priority_fee_per_gas, + "max_fee_per_gas": max_fee_per_gas, + "legacy_gas_price": legacy_gas_price, + "transaction_type": tx_type, + "gas_multiplier": gas_multiplier, + "max_priority_fee_range": max_priority_fee_range, + } + await call_oracle(ctx=ctx, func="withdrawStake", user_inputs=user_inputs) diff --git a/src/telliot_feeds/cli/main.py b/src/telliot_feeds/cli/main.py index 5ec1c218..2d6be747 100644 --- a/src/telliot_feeds/cli/main.py +++ b/src/telliot_feeds/cli/main.py @@ -14,7 +14,10 @@ from telliot_feeds.cli.commands.liquity import liquity from telliot_feeds.cli.commands.query import query from telliot_feeds.cli.commands.report import report +from telliot_feeds.cli.commands.request_withdraw_stake import request_withdraw from telliot_feeds.cli.commands.settle import settle +from telliot_feeds.cli.commands.stake import stake +from telliot_feeds.cli.commands.withdraw import withdraw from telliot_feeds.utils.log import get_logger @@ -44,7 +47,10 @@ def main( main.add_command(integrations) main.add_command(config) main.add_command(account) +main.add_command(stake) main.add_command(liquity) +main.add_command(request_withdraw) +main.add_command(withdraw) if __name__ == "__main__": main() diff --git a/src/telliot_feeds/cli/utils.py b/src/telliot_feeds/cli/utils.py index 8df78d5e..01b90c50 100644 --- a/src/telliot_feeds/cli/utils.py +++ b/src/telliot_feeds/cli/utils.py @@ -3,6 +3,7 @@ from typing import Any from typing import Callable from typing import cast +from typing import Dict from typing import get_args from typing import get_type_hints from typing import Optional @@ -27,6 +28,10 @@ from telliot_feeds.feeds import DATAFEED_BUILDER_MAPPING from telliot_feeds.queries.abi_query import AbiQuery from telliot_feeds.queries.query_catalog import query_catalog +from telliot_feeds.reporters.gas import GasFees +from telliot_feeds.utils.cfg import check_endpoint +from telliot_feeds.utils.cfg import setup_config +from telliot_feeds.utils.reporter_utils import has_native_token_funds load_dotenv() @@ -35,10 +40,11 @@ def print_reporter_settings( signature_address: str, query_tag: str, gas_limit: int, + base_fee: Optional[float], priority_fee: Optional[float], + max_fee: Optional[float], expected_profit: str, chain_id: int, - max_fee: Optional[float], transaction_type: int, legacy_gas_price: Optional[int], reporting_diva_protocol: bool, @@ -97,7 +103,7 @@ def reporter_cli_core(ctx: click.Context) -> TelliotCore: # Ensure chain id compatible with flashbots relay if ctx.obj.get("SIGNATURE_ACCOUNT_NAME", None) is not None: # Only supports mainnet - assert core.config.main.chain_id in (1, 5) + assert core.config.main.chain_id in (1, 5, 11155111) if ctx.obj["TEST_CONFIG"]: try: @@ -293,6 +299,49 @@ def get_accounts_from_name(name: Optional[str]) -> list[ChainedAccount]: return accounts +def common_reporter_options(f: Callable[..., Any]) -> Callable[..., Any]: + """Decorator for common options between reporter commands""" + + @click.option( + "--query-tag", + "-qt", + "query_tag", + help="select datafeed using query tag", + required=False, + nargs=1, + type=click.Choice([q.tag for q in query_catalog.find()]), + ) + @click.option( + "-wp", "--wait-period", help="wait period between feed suggestion calls", nargs=1, type=int, default=7 + ) + @click.option("--submit-once/--submit-continuous", default=False) + @click.option("--stake", "-s", "stake", help=STAKE_MESSAGE, nargs=1, type=float, default=10.0) + @click.option( + "--check-rewards/--no-check-rewards", + "-cr/-ncr", + "check_rewards", + default=True, + help=REWARDS_CHECK_MESSAGE, + ) + @click.option( + "--profit", + "-p", + "expected_profit", + help="lower threshold (inclusive) for expected percent profit", + nargs=1, + # User can omit profitability checks by specifying "YOLO" + type=click.UNPROCESSED, + required=False, + callback=parse_profit_input, + default="100.0", + ) + @functools.wraps(f) + def wrapper(*args: Any, **kwargs: Any) -> Any: + return f(*args, **kwargs) + + return wrapper + + def common_options(f: Callable[..., Any]) -> Callable[..., Any]: """Decorator for common options between commands""" @@ -313,28 +362,34 @@ def common_options(f: Callable[..., Any]) -> Callable[..., Any]: required=False, default=True, ) - @click.option( - "--query-tag", - "-qt", - "query_tag", - help="select datafeed using query tag", - required=False, - nargs=1, - type=click.Choice([q.tag for q in query_catalog.find()]), - ) @click.option("--gas-limit", "-gl", "gas_limit", help="use custom gas limit", nargs=1, type=int) @click.option( - "--max-fee", "-mf", "max_fee", help="use custom maxFeePerGas (gwei)", nargs=1, type=float, required=False + "--max-fee", + "-mf", + "max_fee_per_gas", + help="use custom maxFeePerGas (gwei)", + nargs=1, + type=float, + required=False, ) @click.option( "--priority-fee", "-pf", - "priority_fee", + "priority_fee_per_gas", help="use custom maxPriorityFeePerGas (gwei)", nargs=1, type=float, required=False, ) + @click.option( + "--base-fee", + "-bf", + "base_fee_per_gas", + help="use custom baseFeePerGas (gwei)", + nargs=1, + type=float, + required=False, + ) @click.option( "--gas-price", "-gp", @@ -344,22 +399,6 @@ def common_options(f: Callable[..., Any]) -> Callable[..., Any]: type=int, required=False, ) - @click.option( - "-wp", "--wait-period", help="wait period between feed suggestion calls", nargs=1, type=int, default=7 - ) - @click.option( - "--profit", - "-p", - "expected_profit", - help="lower threshold (inclusive) for expected percent profit", - nargs=1, - # User can omit profitability checks by specifying "YOLO" - type=click.UNPROCESSED, - required=False, - callback=parse_profit_input, - default="100.0", - ) - @click.option("--submit-once/--submit-continuous", default=False) @click.option("-pwd", "--password", type=str) @click.option( "--tx-type", @@ -371,7 +410,6 @@ def common_options(f: Callable[..., Any]) -> Callable[..., Any]: callback=valid_transaction_type, default=2, ) - @click.option("--stake", "-s", "stake", help=STAKE_MESSAGE, nargs=1, type=float, default=10.0) @click.option( "--min-native-token-balance", "-mnb", @@ -381,18 +419,11 @@ def common_options(f: Callable[..., Any]) -> Callable[..., Any]: type=float, default=0.25, ) - @click.option( - "--check-rewards/--no-check-rewards", - "-cr/-ncr", - "check_rewards", - default=True, - help=REWARDS_CHECK_MESSAGE, - ) @click.option( "--gas-multiplier", "-gm", "gas_multiplier", - help="increase gas price by this percentage (default 1%) ie 5 = 5%", + help="increase gas price for legacy transaction by this percentage (default 1%) ie 5 = 5%", nargs=1, type=int, default=1, # 1% above the gas price by web3 @@ -401,10 +432,10 @@ def common_options(f: Callable[..., Any]) -> Callable[..., Any]: "--max-priority-fee-range", "-mpfr", "max_priority_fee_range", - help="the maximum range of priority fees to use in gwei (default 80 gwei)", + help="the maximum range of priority fees to use in gwei (default 3 gwei)", nargs=1, type=int, - default=80, # 80 gwei + default=3, # 3 gwei ) @functools.wraps(f) def wrapper(*args: Any, **kwargs: Any) -> Any: @@ -413,6 +444,50 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: return wrapper +async def call_oracle( + *, + ctx: click.Context, + func: str, + user_inputs: Dict[str, Any], + **params: Any, +) -> None: + # Initialize telliot core app using CLI context + async with reporter_cli_core(ctx) as core: + + core._config, account = setup_config(core.config, account_name=ctx.obj.get("ACCOUNT_NAME")) + + 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(user_inputs.pop("password")) + + contracts = core.get_tellor360_contracts() + # set private key for oracle interaction calls + contracts.oracle._private_key = account.local_account.key + min_native_token_balance = user_inputs.pop("min_native_token_balance") + if has_native_token_funds( + to_checksum_address(account.address), + core.endpoint.web3, + min_balance=int(min_native_token_balance * 10**18), + ): + gas = GasFees(endpoint=core.endpoint, account=account, **user_inputs) + gas.update_gas_fees() + gas_info = gas.get_gas_info_core() + + try: + _ = await contracts.oracle.write(func, **params, **gas_info) + except ValueError as e: + if "no gas strategy selected" in str(e): + click.echo("Can't set gas fees automatically. Please specify gas fees manually.") + + class CustomHexBytes(HexBytes): """Wrapper around HexBytes that doesn't accept int or bool""" diff --git a/src/telliot_feeds/flashbots/provider.py b/src/telliot_feeds/flashbots/provider.py index a9ff4e18..95f61ca3 100644 --- a/src/telliot_feeds/flashbots/provider.py +++ b/src/telliot_feeds/flashbots/provider.py @@ -25,6 +25,7 @@ def get_default_endpoint(chain_id: int = 1) -> URI: uri = { 1: URI(os.environ.get("FLASHBOTS_HTTP_PROVIDER_URI", "https://relay.flashbots.net")), 5: URI(os.environ.get("FLASHBOTS_HTTP_PROVIDER_URI_GOERLI", "https://relay-goerli.flashbots.net")), + 11155111: URI(os.environ.get("FLASHBOTS_HTTP_PROVIDER_URI_SEPOLIA", "https://relay-sepolia.flashbots.net")), } return uri[chain_id] diff --git a/src/telliot_feeds/integrations/diva_protocol/report.py b/src/telliot_feeds/integrations/diva_protocol/report.py index 6270ee3b..d8b33ebe 100644 --- a/src/telliot_feeds/integrations/diva_protocol/report.py +++ b/src/telliot_feeds/integrations/diva_protocol/report.py @@ -4,6 +4,7 @@ import asyncio import time from typing import Any +from typing import Dict from typing import Optional from typing import Tuple @@ -11,8 +12,7 @@ from telliot_core.utils.key_helpers import lazy_unlock_account from telliot_core.utils.response import error_status from telliot_core.utils.response import ResponseStatus -from web3 import Web3 -from web3.datastructures import AttributeDict +from web3.types import TxReceipt from telliot_feeds.datafeed import DataFeed from telliot_feeds.integrations.diva_protocol import DIVA_DIAMOND_ADDRESS @@ -136,23 +136,20 @@ async def fetch_datafeed(self) -> Optional[DataFeed[Any]]: error_status(note=msg, log=logger.warning) return None self.datafeed = datafeed + logger.info(f"Current query: {datafeed.query}") return datafeed - async def set_final_ref_value(self, pool_id: str, gas_price: int) -> ResponseStatus: - return await self.middleware_contract.set_final_reference_value(pool_id=pool_id, legacy_gas_price=gas_price) + async def set_final_ref_value(self, pool_id: str, gas_fees: Dict[str, Any]) -> ResponseStatus: + return await self.middleware_contract.set_final_reference_value(pool_id=pool_id, **gas_fees) async def settle_pool(self, pool_id: str) -> ResponseStatus: """Settle pool""" - if not self.legacy_gas_price: - gas_price = await self.fetch_gas_price() - if not gas_price: - msg = "Unable to fetch gas price for tx type 0" - return error_status(note=msg, log=logger.warning) - else: - gas_price = self.legacy_gas_price + status = self.update_gas_fees() + if not status.ok: + return error_status("unable to generate gas fees", log=logger.error) + gas_fees = self.get_gas_info_core() - gas_price = int(gas_price) if gas_price >= 1 else 1 - status = await self.set_final_ref_value(pool_id=pool_id, gas_price=gas_price) + status = await self.set_final_ref_value(pool_id=pool_id, gas_fees=gas_fees) if status is not None and status.ok: logger.info(f"Pool {pool_id} settled.") return status @@ -212,139 +209,45 @@ async def settle_pools(self) -> ResponseStatus: update_reported_pools(pools=reported_pools) return ResponseStatus() - async def report_once( - self, - ) -> Tuple[Optional[AttributeDict[Any, Any]], ResponseStatus]: - """Report query response to a TellorFlex oracle.""" - staked, status = await self.ensure_staked() - if not staked or not status.ok: - logger.warning(status.error) - return None, status - - status = await self.check_reporter_lock() - if not status.ok: - return None, status - - datafeed = await self.fetch_datafeed() - if not datafeed: - msg = "Unable to fetch DIVA Protocol datafeed." - return None, error_status(note=msg, log=logger.info) - - logger.info(f"Current query: {datafeed.query}") - - status = ResponseStatus() + def sign_n_send_transaction(self, built_tx: Any) -> Tuple[Optional[TxReceipt], ResponseStatus]: + """Send a signed transaction to the blockchain and wait for confirmation - # Update datafeed value - latest_data = await datafeed.source.fetch_new_datapoint() - if latest_data[0] is None: - msg = "Unable to retrieve updated datafeed value." - return None, error_status(msg, log=logger.info) - - # Get query info & encode value to bytes - query = datafeed.query - query_id = query.query_id - query_data = query.query_data - try: - value = query.value_type.encode(latest_data[0]) - except Exception as e: - msg = f"Error encoding response value {latest_data[0]}" - return None, error_status(msg, e=e, log=logger.error) - - # Get nonce - report_count, read_status = await self.get_num_reports_by_id(query_id) - - if not read_status.ok: - status.error = "Unable to retrieve report count: " + read_status.error # error won't be none # noqa: E501 - logger.error(status.error) - status.e = read_status.e - return None, status - - # Start transaction build - submit_val_func = self.oracle.contract.get_function_by_name("submitValue") - submit_val_tx = submit_val_func( - _queryId=query_id, - _value=value, - _nonce=report_count, - _queryData=query_data, - ) - # Estimate gas usage amount - gas_limit, status = self.submit_val_tx_gas_limit(submit_val_tx=submit_val_tx) - if not status.ok or gas_limit is None: - return None, status - - acc_nonce, nonce_status = self.get_acct_nonce() - if not nonce_status.ok: - return None, nonce_status - - # Add transaction type 2 (EIP-1559) data - if self.transaction_type == 2: - priority_fee, max_fee = self.get_fee_info() - if priority_fee is None or max_fee is None: - return None, error_status("Unable to suggest type 2 txn fees", log=logger.error) - - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "maxFeePerGas": Web3.toWei(max_fee, "gwei"), - "maxPriorityFeePerGas": Web3.toWei(priority_fee, "gwei"), - "chainId": self.chain_id, - } - ) - # Add transaction type 0 (legacy) data - else: - # Fetch legacy gas price if not provided by user - if not self.legacy_gas_price: - gas_price = await self.fetch_gas_price() - if gas_price is None: - note = "Unable to fetch gas price for tx type 0" - return None, error_status(note, log=logger.warning) - else: - gas_price = self.legacy_gas_price - - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "gasPrice": Web3.toWei(gas_price, "gwei"), - "chainId": self.chain_id, - } - ) + Params: + tx_signed: The signed transaction object + Returns a tuple of the transaction receipt and a ResponseStatus object + """ lazy_unlock_account(self.account) local_account = self.account.local_account - tx_signed = local_account.sign_transaction(built_submit_val_tx) - + tx_signed = local_account.sign_transaction(built_tx) try: logger.debug("Sending submitValue transaction") - tx_hash = self.endpoint._web3.eth.send_raw_transaction(tx_signed.rawTransaction) + tx_hash = self.web3.eth.send_raw_transaction(tx_signed.rawTransaction) except Exception as e: note = "Send transaction failed" return None, error_status(note, log=logger.error, e=e) - # Confirm submitValue transaction try: - tx_receipt = self.endpoint._web3.eth.wait_for_transaction_receipt(tx_hash, timeout=360) + # Confirm transaction + tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash, timeout=360) + tx_url = f"{self.endpoint.explorer}/tx/{tx_hash.hex()}" if tx_receipt["status"] == 0: - msg = f"Transaction reverted: {tx_url}" + msg = f"Transaction reverted. ({tx_url})" return tx_receipt, error_status(msg, log=logger.error) - except Exception as e: - note = "Failed to confirm transaction" - return None, error_status(note, log=logger.error, e=e) - if status.ok and not status.error: - self.last_submission_timestamp = 0 + logger.info(f"View reported data: \n{tx_url}") # Update reported pools pools = get_reported_pools() cur_time = int(time.time()) - update_reported_pools(pools=pools, add=[[datafeed.query.poolId.hex(), [cur_time, "not settled"]]]) - logger.info(f"View reported data at timestamp {cur_time}: \n{tx_url}") - else: - logger.error(status) - - return tx_receipt, status + if self.datafeed is not None: + update_reported_pools(pools=pools, add=[[self.datafeed.query.poolId.hex(), [cur_time, "not settled"]]]) + logger.info(f"View reported data at timestamp {cur_time}: \n{tx_url}") + return tx_receipt, ResponseStatus() + except Exception as e: + note = "Failed to confirm transaction" + return None, error_status(note, log=logger.error, e=e) async def report(self, report_count: Optional[int] = None) -> None: """Report values for pool reference assets & settle pools.""" diff --git a/src/telliot_feeds/reporters/custom_reporter.py b/src/telliot_feeds/reporters/custom_reporter.py deleted file mode 100644 index 76fc7d39..00000000 --- a/src/telliot_feeds/reporters/custom_reporter.py +++ /dev/null @@ -1,275 +0,0 @@ -from typing import Any -from typing import Optional -from typing import Tuple -from typing import Union - -from chained_accounts import ChainedAccount -from eth_utils import to_checksum_address -from telliot_core.contract.contract import Contract -from telliot_core.model.endpoints import RPCEndpoint -from telliot_core.utils.key_helpers import lazy_unlock_account -from telliot_core.utils.response import error_status -from telliot_core.utils.response import ResponseStatus -from web3.datastructures import AttributeDict - -from telliot_feeds.datafeed import DataFeed -from telliot_feeds.feeds.eth_usd_feed import eth_usd_median_feed -from telliot_feeds.feeds.trb_usd_feed import trb_usd_median_feed -from telliot_feeds.reporters.interval import IntervalReporter -from telliot_feeds.utils.log import get_logger - - -logger = get_logger(__name__) - - -class CustomXReporter(IntervalReporter): - """Custom reporter contract - Use by entering an abi and contract address through the command line - requires depositstake and submitValue signature functions to work - """ - - def __init__( - self, - custom_contract: Contract, - endpoint: RPCEndpoint, - account: ChainedAccount, - chain_id: int, - master: Contract, - oracle: Contract, - datafeed: Optional[DataFeed[Any]] = None, - expected_profit: Union[str, float] = 100.0, - transaction_type: int = 0, - gas_limit: Optional[int] = None, - max_fee: Optional[float] = None, - priority_fee: Optional[float] = None, - legacy_gas_price: Optional[int] = None, - ) -> None: - - self.endpoint = endpoint - self.account = account - self.master = master - self.oracle = oracle - self.datafeed = datafeed - self.chain_id = chain_id - self.acct_addr = to_checksum_address(custom_contract.address) - self.last_submission_timestamp = 0 - self.expected_profit = expected_profit - self.transaction_type = transaction_type - self.gas_limit = gas_limit - self.max_fee = max_fee - self.priority_fee = priority_fee - self.legacy_gas_price = legacy_gas_price - self.trb_usd_median_feed = trb_usd_median_feed - self.eth_usd_median_feed = eth_usd_median_feed - self.custom_contract = custom_contract - self.gas_info: dict[str, Union[float, int]] = {} - - async def ensure_staked(self) -> Tuple[bool, ResponseStatus]: - """Make sure the current user is staked - Returns a bool signifying whether the current address is - staked. If the address is not initially, it attempts to stake with - the address's funds.""" - status = ResponseStatus() - - gas_price_gwei = await self.fetch_gas_price() - if not gas_price_gwei: - note = "Unable to fetch gas price during during ensure_staked()" - return False, error_status(note=note, log=logger.warning) - - staker_info, read_status = await self.master.read(func_name="getStakerInfo", _staker=self.acct_addr) - - if (not read_status.ok) or (staker_info is None): - msg = "Unable to read reporters staker status: " + read_status.error # error won't be none # noqa: E501 - status = error_status(msg, log=logger.info) - status.e = read_status.e - return False, status - - logger.info(f"Stake status: {staker_info[0]}") - - # Status 1: staked - if staker_info[0] == 1: - return True, status - - # Status 0: not yet staked - elif staker_info[0] == 0: - logger.info("Address not yet staked. Depositing stake.") - - _, write_status = await self.custom_contract.write( - func_name="depositStake", - gas_limit=350000, - legacy_gas_price=gas_price_gwei, - ) - - if write_status.ok: - return True, status - else: - msg = ( - "Unable to stake deposit: " - + write_status.error - + f"Make sure {self.acct_addr} has enough ETH & TRB (100)" - ) # error won't be none # noqa: E501 - return False, error_status(msg, log=logger.info) - - # Status 3: disputed - if staker_info[0] == 3: - msg = "Current address disputed. Switch address to continue reporting." # noqa: E501 - return False, error_status(msg, log=logger.info) - - # Statuses 2, 4, and 5: stake transition - else: - msg = "Current address is locked in dispute or for withdrawal." # noqa: E501 - return False, error_status(msg, log=logger.info) - - async def report_once( - self, - ) -> Tuple[Optional[AttributeDict[Any, Any]], ResponseStatus]: - """Report query value once - This method checks to see if a user is able to submit - values to the TellorX oracle, given their staker status - and last submission time. Also, this method does not - submit values if doing so won't make a profit.""" - # Check staker status - staked, status = await self.ensure_staked() - if not staked or not status.ok: - logger.warning(status.error) - return None, status - - status = await self.check_reporter_lock() - if not status.ok: - return None, status - - # Get suggested datafeed if none provided - datafeed = await self.fetch_datafeed() - if not datafeed: - msg = "Unable to suggest datafeed" - return None, error_status(note=msg, log=logger.info) - - logger.info(f"Current query: {datafeed.query.descriptor}") - # Update datafeed value - await datafeed.source.fetch_new_datapoint() - latest_data = datafeed.source.latest - if latest_data[0] is None: - msg = "Unable to retrieve updated datafeed value." - return None, error_status(msg, log=logger.info) - # Get query info & encode value to bytes - query = datafeed.query - query_id = query.query_id - query_data = query.query_data - try: - value = query.value_type.encode(latest_data[0]) - except Exception as e: - msg = f"Error encoding response value {latest_data[0]}" - return None, error_status(msg, e=e, log=logger.error) - # Get nonce - report_count, read_status = await self.get_num_reports_by_id(query_id) - if not read_status.ok: - status.error = "Unable to retrieve report count: " + read_status.error # error won't be none # noqa: E501 - logger.error(status.error) - status.e = read_status.e - return None, status - # Start transaction build - submit_val_func = self.custom_contract.contract.get_function_by_name("submitValue") - submit_val_tx = submit_val_func( - _queryId=query_id, - _value=value, - _nonce=report_count, - _queryData=query_data, - ) - # Estimate gas usage amount - gas_limit, status = self.submit_val_tx_gas_limit(submit_val_tx=submit_val_tx) - if not status.ok or gas_limit is None: - return None, status - # Set gas limit to dict - self.gas_info["gas_limit"] = gas_limit - - acc_nonce, nonce_status = self.get_acct_nonce() - if not nonce_status.ok: - return None, nonce_status - - # Add transaction type 2 (EIP-1559) data - if self.transaction_type == 2: - priority_fee, max_fee = self.get_fee_info() - if priority_fee is None or max_fee is None: - return None, error_status("Unable to suggest type 2 txn fees", log=logger.error) - - # Set gas price to max fee used for profitability check - self.gas_info["type"] = 2 - self.gas_info["max_fee"] = max_fee - self.gas_info["priority_fee"] = priority_fee - self.gas_info["base_fee"] = max_fee - priority_fee - - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "maxFeePerGas": self.web3.toWei(max_fee, "gwei"), - # TODO: Investigate more why etherscan txs using Flashbots have - # the same maxFeePerGas and maxPriorityFeePerGas. Example: - # https://etherscan.io/tx/0x0bd2c8b986be4f183c0a2667ef48ab1d8863c59510f3226ef056e46658541288 # noqa: E501 - "maxPriorityFeePerGas": self.web3.toWei(priority_fee, "gwei"), # noqa: E501 - "chainId": self.chain_id, - } - ) - # Add transaction type 0 (legacy) data - else: - # Fetch legacy gas price if not provided by user - if not self.legacy_gas_price: - gas_price = await self.fetch_gas_price() - if not gas_price: - note = "Unable to fetch gas price for tx type 0" - return None, error_status(note, log=logger.warning) - else: - gas_price = self.legacy_gas_price - - self.gas_info["type"] = 0 - self.gas_info["gas_price"] = gas_price - - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "gasPrice": self.web3.toWei(gas_price, "gwei"), - "chainId": self.chain_id, - } - ) - - status = await self.ensure_profitable(datafeed) - if not status.ok: - return None, status - - status = ResponseStatus() - - lazy_unlock_account(self.account) - local_account = self.account.local_account - tx_signed = local_account.sign_transaction(built_submit_val_tx) - - # Ensure reporter lock is checked again after attempting to submit val - self.last_submission_timestamp = 0 - - try: - logger.debug("Sending submitValue transaction") - tx_hash = self.web3.eth.send_raw_transaction(tx_signed.rawTransaction) - except Exception as e: - note = "Send transaction failed" - return None, error_status(note, log=logger.error, e=e) - - try: - # Confirm transaction - tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash, timeout=360) - - tx_url = f"{self.endpoint.explorer}/tx/{tx_hash.hex()}" - - if tx_receipt["status"] == 0: - msg = f"Transaction reverted. ({tx_url})" - return tx_receipt, error_status(msg, log=logger.error) - - except Exception as e: - note = "Failed to confirm transaction" - return None, error_status(note, log=logger.error, e=e) - - if status.ok and not status.error: - logger.info(f"View reported data: \n{tx_url}") - else: - logger.error(status) - - return tx_receipt, status diff --git a/src/telliot_feeds/reporters/flashbot.py b/src/telliot_feeds/reporters/flashbot.py index 3858ddb3..435c4ade 100644 --- a/src/telliot_feeds/reporters/flashbot.py +++ b/src/telliot_feeds/reporters/flashbot.py @@ -13,9 +13,8 @@ from requests.exceptions import HTTPError from telliot_core.utils.response import error_status from telliot_core.utils.response import ResponseStatus -from web3 import Web3 -from web3.datastructures import AttributeDict from web3.exceptions import TransactionNotFound +from web3.types import TxReceipt from telliot_feeds.flashbots import flashbot # type: ignore from telliot_feeds.flashbots.provider import get_default_endpoint # type: ignore @@ -32,7 +31,7 @@ class FlashbotsReporter(Tellor360Reporter): def __init__(self, signature_account: ChainedAccount, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - self.account: LocalAccount = Account.from_key(self.account.key) + self.account._local_account = Account.from_key(self.account.key) self.signature_account: LocalAccount = Account.from_key(signature_account.key) self.sig_acct_addr = to_checksum_address(signature_account.address) @@ -43,131 +42,12 @@ def __init__(self, signature_account: ChainedAccount, *args: Any, **kwargs: Any) logger.info(f"Flashbots provider endpoint: {flashbots_uri}") flashbot(self.endpoint._web3, self.signature_account, flashbots_uri) - async def report_once( - self, - ) -> Tuple[Optional[AttributeDict[Any, Any]], ResponseStatus]: - """Report query value once - - This method checks to see if a user is able to submit - values to the TellorX oracle, given their staker status - and last submission time. Also, this method does not - submit values if doing so won't make a profit.""" - staked, status = await self.ensure_staked() - if not staked and status.ok: - return None, status - - status = await self.check_reporter_lock() - if not status.ok: - return None, status - - datafeed = await self.fetch_datafeed() - if datafeed is None: - return None, error_status(note="Unable to fetch datafeed", log=logger.warning) - - logger.info(f"Current query: {datafeed.query.descriptor}") - - # Update datafeed value - await datafeed.source.fetch_new_datapoint() - latest_data = datafeed.source.latest - if latest_data[0] is None: - msg = "Unable to retrieve updated datafeed value." - return None, error_status(msg, log=logger.info) - - # Get query info & encode value to bytes - query = datafeed.query - query_id = query.query_id - query_data = query.query_data - try: - value = query.value_type.encode(latest_data[0]) - except Exception as e: - msg = f"Error encoding response value {latest_data[0]}" - return None, error_status(msg, e=e, log=logger.error) - - # Get nonce - timestamp_count, read_status = await self.oracle.read(func_name="getNewValueCountbyQueryId", _queryId=query_id) - if not read_status.ok: - status.error = "Unable to retrieve newValueCount: " + read_status.error # error won't be none # noqa: E501 - logger.error(status.error) - status.e = read_status.e - return None, status - - # Start transaction build - submit_val_func = self.oracle.contract.get_function_by_name("submitValue") - submit_val_tx = submit_val_func( - _queryId=query_id, - _value=value, - _nonce=timestamp_count, - _queryData=query_data, - ) - # Estimate gas usage amount - gas_limit, status = self.submit_val_tx_gas_limit(submit_val_tx=submit_val_tx) - if not status.ok or gas_limit is None: - return None, status - - self.gas_info["gas_limit"] = gas_limit - # Get account nonce - acc_nonce, nonce_status = self.get_acct_nonce() - if not nonce_status.ok: - return None, nonce_status - - # Add transaction type 2 (EIP-1559) data - if self.transaction_type == 2: - priority_fee, max_fee = self.get_fee_info() - if priority_fee is None or max_fee is None: - return None, error_status("Unable to suggest type 2 txn fees", log=logger.error) - - logger.info(f"maxFeePerGas: {max_fee}") - logger.info(f"maxPriorityFeePerGas: {priority_fee}") - - # Set gas price to max fee used for profitability check - self.gas_info["type"] = 2 - self.gas_info["max_fee"] = max_fee - self.gas_info["priority_fee"] = priority_fee - self.gas_info["base_fee"] = max_fee - priority_fee - - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "maxFeePerGas": Web3.toWei(max_fee, "gwei"), - # TODO: Investigate more why etherscan txs using Flashbots have - # the same maxFeePerGas and maxPriorityFeePerGas. Example: - # https://etherscan.io/tx/0x0bd2c8b986be4f183c0a2667ef48ab1d8863c59510f3226ef056e46658541288 # noqa: E501 - "maxPriorityFeePerGas": Web3.toWei(priority_fee, "gwei"), # noqa: E501 - "chainId": self.chain_id, - } - ) - # Add transaction type 0 (legacy) data - else: - if not self.legacy_gas_price: - gas_price = await self.fetch_gas_price() - if gas_price is None: - note = "Unable to fetch gas price for tx type 0" - return None, error_status(note, log=logger.warning) - else: - gas_price = self.legacy_gas_price - - self.gas_info["type"] = 0 - self.gas_info["gas_price"] = gas_price - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "gasPrice": Web3.toWei(gas_price, "gwei"), - "chainId": self.chain_id, - } - ) - - status = await self.ensure_profitable(datafeed) - if not status.ok: - return None, status + def sign_n_send_transaction(self, built_tx: Any) -> Tuple[Optional[TxReceipt], ResponseStatus]: status = ResponseStatus() - - submit_val_tx_signed = self.account.sign_transaction(built_submit_val_tx) # type: ignore - # Create bundle of one pre-signed, EIP-1559 (type 2) transaction + tx_signed = self.account.local_account.sign_transaction(built_tx) bundle = [ - {"signed_transaction": submit_val_tx_signed.rawTransaction}, + {"signed_transaction": tx_signed.rawTransaction}, ] # Send bundle to be executed in the next block @@ -185,6 +65,7 @@ async def report_once( except HTTPError as e: msg = "Unable to send bundle to miners due to HTTP error" return None, error_status(note=msg, e=e, log=logger.error) + logger.info(f"Bundle sent to miners in block {block}") # Wait for transaction confirmation @@ -198,14 +79,8 @@ async def report_once( status.e = e return None, status - status = ResponseStatus() - if status.ok and not status.error: - # Reset previous submission timestamp - self.last_submission_timestamp = 0 - tx_hash = tx_receipt["transactionHash"].hex() - # Point to relevant explorer - logger.info(f"View reported data: \n{self.endpoint.explorer}/tx/{tx_hash}") - else: - logger.error(status) + tx_hash = tx_receipt["transactionHash"].hex() + # Point to relevant explorer + logger.info(f"View reported data: \n{self.endpoint.explorer}/tx/{tx_hash}") return tx_receipt, status diff --git a/src/telliot_feeds/reporters/gas.py b/src/telliot_feeds/reporters/gas.py new file mode 100644 index 00000000..5232d7a4 --- /dev/null +++ b/src/telliot_feeds/reporters/gas.py @@ -0,0 +1,345 @@ +from decimal import Decimal +from typing import Any +from typing import Dict +from typing import List +from typing import Literal +from typing import Optional +from typing import Tuple +from typing import Union + +from chained_accounts import ChainedAccount +from eth_utils import to_checksum_address +from telliot_core.apps.core import RPCEndpoint +from telliot_core.utils.response import error_status +from telliot_core.utils.response import ResponseStatus +from web3 import Web3 +from web3.contract import ContractFunction +from web3.types import FeeHistory +from web3.types import Wei + +from telliot_feeds.reporters.types import GasParams +from telliot_feeds.utils.log import get_logger +from telliot_feeds.utils.reporter_utils import fee_history_priority_fee_estimate + + +logger = get_logger(__name__) + +FEE = Literal["maxPriorityFeePerGas", "maxFeePerGas", "gasPrice", "gas"] +FEES = Dict[FEE, Optional[Union[Wei, int]]] + + +class GasFees: + """Set gas fees for a transaction. + + call update_gas_prices() to update/set gas prices + then call get_gas_prices() to get the gas prices for a transaction assembled manually + or call get_gas_params_core() to get the gas prices for a transaction assembled by telliot_core + returns gas_info for the transaction type + """ + + gas_info: GasParams = { + "maxPriorityFeePerGas": None, + "maxFeePerGas": None, + "gasPrice": None, + "gas": None, + } + + @staticmethod + def to_gwei(value: Union[Wei, int]) -> Union[int, float]: + """Converts wei to gwei.""" + converted_value = Web3.fromWei(value, "gwei") + # Returns float if Gwei value is Decimal, otherwise returns int + if isinstance(converted_value, Decimal): + return float(converted_value) + return converted_value + + @staticmethod + def from_gwei(value: Union[int, float, Decimal]) -> Wei: + """Converts gwei to wei.""" + return Web3.toWei(value, "gwei") + + @staticmethod + def optional_to_gwei(value: Union[Wei, int, None]) -> Union[int, float, None]: + """Converts wei to gwei and if value is None, returns None.""" + if value is None: + return None + return GasFees.to_gwei(value) + + @staticmethod + def optional_from_gwei(value: Union[int, float, Decimal, None]) -> Optional[Wei]: + """Converts gwei to wei and if value is None, returns None.""" + if value is None: + return None + return GasFees.from_gwei(value) + + @staticmethod + def to_ether(value: Union[Wei, int]) -> Union[int, float]: + """Converts wei to ether. ie 1e18 wei = 1 ether""" + converted_value = Web3.fromWei(value, "ether") + if isinstance(converted_value, Decimal): + return float(converted_value) + return converted_value + + @staticmethod + def from_ether(value: Union[int, float, Decimal]) -> Wei: + """Converts ether to wei. ie 1 ether = 1e18 wei""" + return Web3.toWei(value, "ether") + + def __init__( + self, + endpoint: RPCEndpoint, + account: ChainedAccount, + transaction_type: int, + gas_limit: Optional[int] = None, # Amount of a transaction will need to be executed + legacy_gas_price: Optional[int] = None, # Type 0 transaction pre London fork in Gwei + gas_multiplier: int = 1, # 1 percent + # Max priority fee range that shouldn't be exceeded when auto calculating gas price in Gwei + max_priority_fee_range: int = 5, + priority_fee_per_gas: Optional[int] = None, + base_fee_per_gas: Optional[int] = None, + max_fee_per_gas: Optional[int] = None, + reward_percentile: Optional[List[float]] = None, + block_count: int = 10, # Number of blocks to use for gas price calculation + min_native_token_balance: int = 0, # Minimum native token balance to be considered for gas price calculation + ): + self.endpoint = endpoint + self.account = account + self.transaction_type = transaction_type + self.gas_limit = gas_limit + self.gas_multiplier = gas_multiplier + self.legacy_gas_price = self.optional_from_gwei(legacy_gas_price) + self.max_fee_per_gas = self.optional_from_gwei(max_fee_per_gas) + self.priority_fee_per_gas = self.optional_from_gwei(priority_fee_per_gas) + self.base_fee_per_gas = self.optional_from_gwei(base_fee_per_gas) + self.max_priority_fee_range = self.from_gwei(max_priority_fee_range) + self.reward_percentile = reward_percentile or [25.0, 50.0, 75.0] + self.block_count = block_count + self.min_native_token_balance = min_native_token_balance + + self.acct_address = to_checksum_address(account.address) + self.web3: Web3 = endpoint._web3 + assert self.web3 is not None, f"Web3 is not initialized, check endpoint {endpoint}" + + def set_gas_info(self, fees: FEES) -> None: + """Set class variable gas_info keys to values in fees""" + for fee in fees: + logger.debug(f"Setting gas info {fee} to {fees[fee]}") + self.gas_info[fee] = fees[fee] + + def _reset_gas_info(self) -> None: + """Resets class variable gas_info keys to None values + This is used to reset gas_info before updating gas_info + """ + self.gas_info = { + "maxPriorityFeePerGas": None, + "maxFeePerGas": None, + "gasPrice": None, + "gas": None, + } + + def get_gas_info(self) -> Dict[str, Any]: + """Get gas info and remove None values""" + return {k: v for k, v in self.gas_info.items() if v is not None} + + def get_gas_info_core(self) -> Dict[str, Union[int, float, Wei, None]]: + """Convert gas info to gwei and update keys to follow telliot core param convention""" + gas = self.gas_info + return { + "max_fee_per_gas": self.optional_to_gwei(gas["maxFeePerGas"]), + "max_priority_fee_per_gas": self.optional_to_gwei(gas["maxPriorityFeePerGas"]), + "legacy_gas_price": self.optional_to_gwei(gas["gasPrice"]), + "gas_limit": gas["gas"], + } + + def estimate_gas_amount(self, pre_built_transaction: ContractFunction) -> Tuple[Optional[int], ResponseStatus]: + """Estimate the gas amount for a given transaction + ie how many gas units will a transaction need to be executed + + Returns: + - gas amount in wei int + """ + if self.gas_limit is not None: + self.set_gas_info({"gas": self.gas_limit}) + return self.gas_limit, ResponseStatus() + try: + gas = pre_built_transaction.estimateGas({"from": self.acct_address}) + self.set_gas_info({"gas": gas}) + return gas, ResponseStatus() + except Exception as e: + return None, error_status("Error estimating gas amount:", e, logger.error) + + def get_legacy_gas_price(self) -> Tuple[Optional[FEES], ResponseStatus]: + """Fetch the legacy gas price for a type 0 (legacy) transaction from the node + + Returns: + - gas_price in gwei int + """ + if self.legacy_gas_price is not None: + return {"gasPrice": self.legacy_gas_price}, ResponseStatus() + + try: + gas_price = self.web3.eth.gas_price + if gas_price is None: + return None, error_status("Error fetching legacy gas price, rpc returned None", log=logger.error) + multiplier = 1.0 + (self.gas_multiplier / 100.0) # 1 percent default extra + legacy_gas_price = int(gas_price * multiplier) + return {"gasPrice": Wei(legacy_gas_price)}, ResponseStatus() + except Exception as e: + return None, error_status("Error fetching legacy gas price", e=e, log=logger.error) + + def fee_history(self) -> Tuple[Optional[FeeHistory], ResponseStatus]: + """Fetch the fee history for a type 2 (EIP1559) transaction from the node + This function uses the `fee_history` method of `web3.eth` to get the history of + transaction fees from the EVM network. The number of blocks to retrieve and the + reward percentiles to compute are hardcoded to 5 blocks and [25, 50, 75] percentiles + meaning for each block get the 25th, 50th and 75th percentile of priority fee per gas + + Returns: + - fee_history: FeeHistory + """ + try: + fee_history = self.web3.eth.fee_history( + block_count=self.block_count, newest_block="latest", reward_percentiles=self.reward_percentile + ) + if fee_history is None: + return None, error_status("unable to fetch fee history from node") + return fee_history, ResponseStatus() + except Exception as e: + return None, error_status("Error fetching fee history", e=e, log=logger.error) + + def get_max_fee(self, base_fee: Wei) -> Wei: + """Calculate the max fee for a type 2 (EIP1559) transaction""" + if self.max_fee_per_gas is not None: + return self.max_fee_per_gas + # if a block is 100% full, the base fee per gas is set to increase by 12.5% for the next block + # adding 12.5% to base_fee arg to ensure inclusion to at least the next block if not included in current block + return Wei(int(base_fee * 1.125)) + + def get_max_priority_fee(self, fee_history: Optional[FeeHistory] = None) -> Tuple[Optional[Wei], ResponseStatus]: + """Return the max priority fee for a type 2 (EIP1559) transaction + if priority fee is provided then return the provided priority fee + else try to fetch a priority fee suggestion from the node using Eth._max_priority_fee method + with a fallback that returns the max priority fee based on the fee history + + Args: + - fee_history: Optional[FeeHistory] + + Returns: + - max_priority_fee: Wei + - ResponseStatus + """ + priority_fee = self.priority_fee_per_gas + max_range = self.max_priority_fee_range + if priority_fee is not None: + return priority_fee, ResponseStatus() + else: + try: + max_priority_fee = self.web3.eth._max_priority_fee() + return max_priority_fee if max_priority_fee < max_range else max_range, ResponseStatus() + except ValueError: + logger.warning("unable to fetch max priority fee from node using eth._max_priority_fee_per_gas method.") + if fee_history is not None: + return fee_history_priority_fee_estimate(fee_history, max_range), ResponseStatus() + else: + fee_history, status = self.fee_history() + if fee_history is None: + msg = "unable to fetch history to calculate max priority fee" + return None, error_status(msg, e=status.error, log=logger.error) + else: + return fee_history_priority_fee_estimate(fee_history, max_range), ResponseStatus() + + def get_base_fee(self) -> Tuple[Optional[Union[Wei, FeeHistory]], ResponseStatus]: + """Return the base fee for a type 2 (EIP1559) transaction. + if base fee is provided then return the provided base fee + else return the base fee based on the Eth.feed_history method response + """ + base_fee = self.base_fee_per_gas + if base_fee is not None: + return base_fee, ResponseStatus() + else: + fee_history, status = self.fee_history() + if fee_history is None: + msg = "unable to fetch history to set base fee" + return None, error_status(msg, e=status.error, log=logger.error) + else: + return fee_history, ResponseStatus() + + def get_eip1559_gas_price(self) -> Tuple[Optional[FEES], ResponseStatus]: + """Get the gas price for a type 2 (EIP1559) transaction + if at least two user args for gas aren't provided then fetch fee history and assemble the gas params + for args that aren't provided, else use the provided args to assemble the gas params + + Returns: + - Dict[str, Wei]; {priority_fee_per_gas: Wei, max_fee_per_gas: Wei} + - ResponseStatus + """ + fee_args = [self.base_fee_per_gas, self.priority_fee_per_gas, self.max_fee_per_gas] + provided_fee_args = [arg for arg in fee_args if arg is not None] + if len(provided_fee_args) < 2: + # Get base fee + _base_fee, status = self.get_base_fee() # returns FeeHistory if base fee is not provided + if _base_fee is None: + msg = "no base fee set" + return None, error_status(msg, e=status.error, log=logger.error) + else: + if not isinstance(_base_fee, int): + base_fee = _base_fee["baseFeePerGas"][-1] + fee_history = _base_fee + else: + fee_history = None + base_fee = _base_fee + # Get priority fee + priority_fee, status = self.get_max_priority_fee(fee_history) + if priority_fee is None: + msg = "no priority fee set" + return None, error_status(msg, e=status.error, log=logger.error) + # Get max fee + max_fee = self.get_max_fee(base_fee) + logger.debug(f"base fee: {base_fee}, priority fee: {priority_fee}, max fee: {max_fee}") + else: + # if two args are given then we can calculate the third + if self.base_fee_per_gas is not None and self.priority_fee_per_gas is not None: + # calculate max fee + max_fee = self.get_max_fee(self.base_fee_per_gas) + priority_fee = self.priority_fee_per_gas + + elif self.base_fee_per_gas is not None and self.max_fee_per_gas is not None: + # calculate priority fee + priority_fee = Wei(self.max_fee_per_gas - self.base_fee_per_gas) + max_fee = self.max_fee_per_gas + elif self.priority_fee_per_gas is not None and self.max_fee_per_gas is not None: + priority_fee = self.priority_fee_per_gas + max_fee = self.max_fee_per_gas + else: + return None, error_status("Error calculating EIP1559 gas price no args provided", logger.error) + + return { + "maxPriorityFeePerGas": priority_fee, + "maxFeePerGas": max_fee if max_fee > priority_fee else priority_fee, + }, ResponseStatus() + + def update_gas_fees(self) -> ResponseStatus: + """Update class gas_info with the latest gas fees whenever called""" + self._reset_gas_info() + self.set_gas_info({"gas": self.gas_limit}) + if self.transaction_type == 0: + legacy_gas_fees, status = self.get_legacy_gas_price() + if legacy_gas_fees is None: + return error_status( + "Failed to update gas fees for legacy type transaction", e=status.error, log=logger.error + ) + self.set_gas_info(legacy_gas_fees) + logger.debug(f"Legacy transaction gas price: {legacy_gas_fees} status: {status}") + return status + elif self.transaction_type == 2: + eip1559_gas_fees, status = self.get_eip1559_gas_price() + if eip1559_gas_fees is None: + return error_status( + "Failed to update gas fees for EIP1559 type transaction", e=status.error, log=logger.debug + ) + self.set_gas_info(eip1559_gas_fees) + logger.debug(f"Gas fees: {eip1559_gas_fees} status: {status}") + return status + else: + msg = f"Failed to update gas fees: invalid transaction type: {self.transaction_type}" + return error_status(msg, log=logger.error) diff --git a/src/telliot_feeds/reporters/interval.py b/src/telliot_feeds/reporters/interval.py deleted file mode 100644 index 3e9b57b4..00000000 --- a/src/telliot_feeds/reporters/interval.py +++ /dev/null @@ -1,519 +0,0 @@ -""" BTCUSD Price Reporter -Example of a subclassed Reporter. -""" -import asyncio -import time -from typing import Any -from typing import Optional -from typing import Tuple -from typing import Union - -from chained_accounts import ChainedAccount -from eth_utils import to_checksum_address -from telliot_core.contract.contract import Contract -from telliot_core.model.endpoints import RPCEndpoint -from telliot_core.utils.key_helpers import lazy_unlock_account -from telliot_core.utils.response import error_status -from telliot_core.utils.response import ResponseStatus -from web3.contract import ContractFunction -from web3.datastructures import AttributeDict - -from telliot_feeds.datafeed import DataFeed -from telliot_feeds.feeds import CATALOG_FEEDS -from telliot_feeds.feeds.eth_usd_feed import eth_usd_median_feed -from telliot_feeds.feeds.trb_usd_feed import trb_usd_median_feed -from telliot_feeds.utils.log import get_logger -from telliot_feeds.utils.reporter_utils import fee_history_priority_fee_estimate -from telliot_feeds.utils.reporter_utils import has_native_token_funds -from telliot_feeds.utils.reporter_utils import is_online -from telliot_feeds.utils.reporter_utils import tellor_suggested_report -from telliot_feeds.utils.reporter_utils import tkn_symbol - - -logger = get_logger(__name__) - - -class IntervalReporter: - """Reports values from given datafeeds to a TellorX Oracle - every 7 seconds.""" - - def __init__( - self, - endpoint: RPCEndpoint, - account: ChainedAccount, - chain_id: int, - master: Contract, - oracle: Contract, - datafeed: Optional[DataFeed[Any]] = None, - expected_profit: Union[str, float] = 100.0, - transaction_type: int = 0, - gas_limit: Optional[int] = None, - max_fee: Optional[float] = None, - priority_fee: Optional[float] = None, - legacy_gas_price: Optional[int] = None, - gas_multiplier: int = 1, - max_priority_fee_range: int = 80, # 80 gwei - wait_period: int = 10, - min_native_token_balance: int = 10**18, - ) -> None: - - self.endpoint = endpoint - self.account = account - self.master = master - self.oracle = oracle - self.datafeed = datafeed - self.chain_id = chain_id - self.acct_addr = to_checksum_address(account.address) - self.last_submission_timestamp = 0 - self.expected_profit = expected_profit - self.transaction_type = transaction_type - self.gas_limit = gas_limit - self.max_fee = max_fee - self.priority_fee = priority_fee - self.legacy_gas_price = legacy_gas_price - self.gas_multiplier = gas_multiplier - self.max_priority_fee_range = max_priority_fee_range - self.trb_usd_median_feed = trb_usd_median_feed - self.eth_usd_median_feed = eth_usd_median_feed - self.wait_period = wait_period - self.min_native_token_balance = min_native_token_balance - self.web3 = self.endpoint._web3 - - self.gas_info: dict[str, Union[float, int]] = {} - - logger.info(f"Reporting with account: {self.acct_addr}") - - async def check_reporter_lock(self) -> ResponseStatus: - """Ensure enough time has passed since last report - Returns a bool signifying whether a given address is in a - reporter lock or not (TellorX oracle users cannot submit - multiple times within 12 hours).""" - status = ResponseStatus() - - # Save last submission timestamp to reduce web3 calls - if self.last_submission_timestamp == 0: - last_timestamp, read_status = await self.oracle.read("getReporterLastTimestamp", _reporter=self.acct_addr) - - # Log web3 errors - if (not read_status.ok) or (last_timestamp is None): - status.ok = False - status.error = "Unable to retrieve reporter's last report timestamp:" + read_status.error - logger.error(status.error) - status.e = read_status.e - return status - - self.last_submission_timestamp = last_timestamp - logger.info(f"Last submission timestamp: {self.last_submission_timestamp}") - - if time.time() < self.last_submission_timestamp + 43200: # 12 hours in seconds - status.ok = False - status.error = "Current address is in reporter lock." - logger.info(status.error) - return status - - return status - - async def fetch_gas_price(self) -> Optional[float]: - """Fetches the current gas price from an EVM network and returns - an adjusted gas price. - - Returns: - An optional integer representing the adjusted gas price in wei, or - None if the gas price could not be retrieved. - """ - try: - price = self.web3.eth.gas_price - priceGwei = self.web3.fromWei(price, "gwei") - except Exception as e: - logger.error(f"Error fetching gas price: {e}") - return None - # increase gas price by 1.0 + gas_multiplier - multiplier = 1.0 + (self.gas_multiplier / 100.0) - gas_price = (float(priceGwei) * multiplier) if priceGwei else None - return gas_price - - async def ensure_staked(self) -> Tuple[bool, ResponseStatus]: - """Make sure the current user is staked - Returns a bool signifying whether the current address is - staked. If the address is not initially, it attempts to stake with - the address's funds.""" - status = ResponseStatus() - - gas_price_gwei = await self.fetch_gas_price() - if not gas_price_gwei: - note = "Unable to fetch gas price during during ensure_staked()" - return False, error_status(note=note, log=logger.warning) - - staker_info, read_status = await self.master.read(func_name="getStakerInfo", _staker=self.acct_addr) - - if (not read_status.ok) or (staker_info is None): - msg = "Unable to read reporters staker status: " + read_status.error # error won't be none # noqa: E501 - status = error_status(msg, log=logger.info) - status.e = read_status.e - return False, status - - logger.info(f"Stake status: {staker_info[0]}") - - # Status 1: staked - if staker_info[0] == 1: - return True, status - - # Status 0: not yet staked - elif staker_info[0] == 0: - logger.info("Address not yet staked. Depositing stake.") - - _, write_status = await self.master.write( - func_name="depositStake", - gas_limit=350000, - legacy_gas_price=gas_price_gwei, - ) - - if write_status.ok: - return True, status - else: - msg = ( - "Unable to stake deposit: " - + write_status.error - + f"Make sure {self.acct_addr} has enough ETH & TRB (100)" - ) # error won't be none # noqa: E501 - return False, error_status(msg, log=logger.info) - - # Status 3: disputed - if staker_info[0] == 3: - msg = "Current address disputed. Switch address to continue reporting." # noqa: E501 - return False, error_status(msg, log=logger.info) - - # Statuses 2, 4, and 5: stake transition - else: - msg = "Current address is locked in dispute or for withdrawal." # noqa: E501 - return False, error_status(msg, log=logger.info) - - async def ensure_profitable(self, datafeed: DataFeed[Any]) -> ResponseStatus: - """Estimate profitability - - Returns a bool signifying whether submitting for a given - queryID would generate a net profit.""" - status = ResponseStatus() - - # Get current tips and time-based reward for given queryID - rewards, read_status = await self.oracle.read("getCurrentReward", _queryId=datafeed.query.query_id) - - # Log web3 errors - if (not read_status.ok) or (rewards is None): - status.ok = False - status.error = "Unable to retrieve queryID's current rewards:" + read_status.error - logger.error(status.error) - status.e = read_status.e - return status - - # Fetch token prices - price_feeds = [self.eth_usd_median_feed, self.trb_usd_median_feed] - _ = await asyncio.gather(*[feed.source.fetch_new_datapoint() for feed in price_feeds]) - - price_eth_usd = self.eth_usd_median_feed.source.latest[0] - price_trb_usd = self.trb_usd_median_feed.source.latest[0] - - if price_eth_usd is None: - note = "Unable to fetch ETH/USD price for profit calculation" - return error_status(note=note, log=logger.warning) - if price_trb_usd is None: - note = "Unable to fetch TRB/USD price for profit calculation" - return error_status(note=note, log=logger.warning) - - tips, tb_reward = rewards - - if not self.gas_info: - return error_status("Gas info not set", log=logger.warning) - - gas_info = self.gas_info - txn_fee = gas_info["gas_price"] * gas_info["gas_limit"] - - if gas_info["type"] == 0: - txn_fee = gas_info["gas_price"] * gas_info["gas_limit"] - logger.info( - f""" - - Tips: {tips / 1e18} - Time-based reward: {tb_reward / 1e18} TRB - Transaction fee: {self.web3.fromWei(txn_fee, 'gwei'):.09f} {tkn_symbol(self.chain_id)} - Gas price: {gas_info["gas_price"]} gwei - Gas limit: {gas_info["gas_limit"]} - Txn type: 0 (Legacy) - """ - ) - if gas_info["type"] == 2: - txn_fee = gas_info["max_fee"] * gas_info["gas_limit"] - logger.info( - f""" - - Tips: {tips / 1e18} - Time-based reward: {tb_reward / 1e18} TRB - Max transaction fee: {self.web3.fromWei(txn_fee, 'gwei')} {tkn_symbol(self.chain_id)} - Max fee per gas: {gas_info["max_fee"]} gwei - Max priority fee per gas: {gas_info["priority_fee"]} gwei - Gas limit: {gas_info["gas_limit"]} - Txn type: 2 (EIP-1559) - """ - ) - - # Calculate profit - revenue = tb_reward + tips - rev_usd = revenue / 1e18 * price_trb_usd - costs_usd = txn_fee / 1e9 * price_eth_usd - profit_usd = rev_usd - costs_usd - logger.info(f"Estimated profit: ${round(profit_usd, 2)}") - - percent_profit = ((profit_usd) / costs_usd) * 100 - logger.info(f"Estimated percent profit: {round(percent_profit, 2)}%") - - if (self.expected_profit != "YOLO") and (percent_profit < self.expected_profit): - status.ok = False - status.error = "Estimated profitability below threshold." - logger.info(status.error) - return status - - return status - - def get_fee_info(self) -> Tuple[Optional[float], Optional[float]]: - """Calculate max fee and priority fee if not set - for more info: - https://web3py.readthedocs.io/en/v5/web3.eth.html?highlight=fee%20history#web3.eth.Eth.fee_history - """ - if self.max_fee is None: - try: - fee_history = self.web3.eth.fee_history( - block_count=5, newest_block="latest", reward_percentiles=[25, 50, 75] - ) - # "base fee for the next block after the newest of the returned range" - base_fee = fee_history.baseFeePerGas[-1] / 1e9 - # estimate priority fee from fee history - priority_fee_max = int(self.max_priority_fee_range * 1e9) # convert to wei - priority_fee = fee_history_priority_fee_estimate(fee_history, priority_fee_max=priority_fee_max) / 1e9 - max_fee = base_fee + priority_fee - return priority_fee, max_fee - except Exception as e: - logger.warning(f"Error in calculating gas fees: {e}") - return None, None - return self.priority_fee, self.max_fee - - async def fetch_datafeed(self) -> Optional[DataFeed[Any]]: - if self.datafeed is None: - suggested_qtag = await tellor_suggested_report(self.oracle) - if suggested_qtag is None: - logger.warning("Could not get suggested query") - return None - self.datafeed = CATALOG_FEEDS[suggested_qtag] - - return self.datafeed - - async def get_num_reports_by_id(self, query_id: bytes) -> Tuple[int, ResponseStatus]: - count, read_status = await self.oracle.read(func_name="getNewValueCountbyQueryId", _queryId=query_id) - return count, read_status - - async def is_online(self) -> bool: - return await is_online() - - def has_native_token(self) -> bool: - """Check if account has native token funds for a network for gas fees - of at least min_native_token_balance that is set in the cli""" - return has_native_token_funds(self.acct_addr, self.web3, min_balance=self.min_native_token_balance) - - def get_acct_nonce(self) -> tuple[Optional[int], ResponseStatus]: - """Get transaction count for an address""" - try: - return self.web3.eth.get_transaction_count(self.acct_addr), ResponseStatus() - except ValueError as e: - return None, error_status("Account nonce request timed out", e=e, log=logger.warning) - except Exception as e: - return None, error_status("Unable to retrieve account nonce", e=e, log=logger.error) - - # Estimate gas usage and set the gas limit if not provided - def submit_val_tx_gas_limit(self, submit_val_tx: ContractFunction) -> tuple[Optional[int], ResponseStatus]: - """Estimate gas usage for submitValue transaction - Args: - submit_val_tx: The submitValue transaction object - Returns a tuple of the gas limit and a ResponseStatus object""" - if self.gas_limit is None: - try: - gas_limit: int = submit_val_tx.estimateGas({"from": self.acct_addr}) - if not gas_limit: - return None, error_status("Unable to estimate gas for submitValue transaction") - return gas_limit, ResponseStatus() - except Exception as e: - msg = "Unable to estimate gas for submitValue transaction" - return None, error_status(msg, e=e, log=logger.error) - return self.gas_limit, ResponseStatus() - - async def report_once( - self, - ) -> Tuple[Optional[AttributeDict[Any, Any]], ResponseStatus]: - """Report query value once - This method checks to see if a user is able to submit - values to the TellorX oracle, given their staker status - and last submission time. Also, this method does not - submit values if doing so won't make a profit.""" - # Check staker status - staked, status = await self.ensure_staked() - if not staked or not status.ok: - logger.warning(status.error) - return None, status - - status = await self.check_reporter_lock() - if not status.ok: - return None, status - - # Get suggested datafeed if none provided - datafeed = await self.fetch_datafeed() - if not datafeed: - msg = "Unable to suggest datafeed" - return None, error_status(note=msg, log=logger.info) - - logger.info(f"Current query: {datafeed.query.descriptor}") - - # Update datafeed value - await datafeed.source.fetch_new_datapoint() - latest_data = datafeed.source.latest - if latest_data[0] is None: - msg = "Unable to retrieve updated datafeed value." - return None, error_status(msg, log=logger.info) - - # Get query info & encode value to bytes - query = datafeed.query - query_id = query.query_id - query_data = query.query_data - try: - value = query.value_type.encode(latest_data[0]) - logger.debug(f"IntervalReporter Encoded value: {value.hex()}") - except Exception as e: - msg = f"Error encoding response value {latest_data[0]}" - return None, error_status(msg, e=e, log=logger.error) - - # Get nonce - report_count, read_status = await self.get_num_reports_by_id(query_id) - - if not read_status.ok: - status.error = "Unable to retrieve report count: " + read_status.error # error won't be none # noqa: E501 - logger.error(status.error) - status.e = read_status.e - return None, status - - # Start transaction build - submit_val_func = self.oracle.contract.get_function_by_name("submitValue") - submit_val_tx = submit_val_func( - _queryId=query_id, - _value=value, - _nonce=report_count, - _queryData=query_data, - ) - # Estimate gas usage amount - gas_limit, status = self.submit_val_tx_gas_limit(submit_val_tx=submit_val_tx) - if not status.ok or gas_limit is None: - return None, status - - self.gas_info["gas_limit"] = gas_limit - # Get account nonce - acc_nonce, nonce_status = self.get_acct_nonce() - if not nonce_status.ok: - return None, nonce_status - # Add transaction type 2 (EIP-1559) data - if self.transaction_type == 2: - priority_fee, max_fee = self.get_fee_info() - if priority_fee is None or max_fee is None: - return None, error_status("Unable to suggest type 2 txn fees", log=logger.error) - # Set gas price to max fee used for profitability check - self.gas_info["type"] = 2 - self.gas_info["max_fee"] = max_fee - self.gas_info["priority_fee"] = priority_fee - self.gas_info["base_fee"] = max_fee - priority_fee - - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "maxFeePerGas": self.web3.toWei(max_fee, "gwei"), - # TODO: Investigate more why etherscan txs using Flashbots have - # the same maxFeePerGas and maxPriorityFeePerGas. Example: - # https://etherscan.io/tx/0x0bd2c8b986be4f183c0a2667ef48ab1d8863c59510f3226ef056e46658541288 # noqa: E501 - "maxPriorityFeePerGas": self.web3.toWei(priority_fee, "gwei"), # noqa: E501 - "chainId": self.chain_id, - } - ) - # Add transaction type 0 (legacy) data - else: - # Fetch legacy gas price if not provided by user - if not self.legacy_gas_price: - gas_price = await self.fetch_gas_price() - if not gas_price: - note = "Unable to fetch gas price for tx type 0" - return None, error_status(note, log=logger.warning) - - else: - gas_price = self.legacy_gas_price - # Set gas price to legacy gas price used for profitability check - self.gas_info["type"] = 0 - self.gas_info["gas_price"] = gas_price - built_submit_val_tx = submit_val_tx.buildTransaction( - { - "nonce": acc_nonce, - "gas": gas_limit, - "gasPrice": self.web3.toWei(gas_price, "gwei"), - "chainId": self.chain_id, - } - ) - - # Check if profitable if not YOLO - status = await self.ensure_profitable(datafeed) - if not status.ok: - return None, status - - lazy_unlock_account(self.account) - local_account = self.account.local_account - tx_signed = local_account.sign_transaction(built_submit_val_tx) - - # Ensure reporter lock is checked again after attempting to submit val - self.last_submission_timestamp = 0 - - try: - logger.debug("Sending submitValue transaction") - tx_hash = self.web3.eth.send_raw_transaction(tx_signed.rawTransaction) - except Exception as e: - note = "Send transaction failed" - return None, error_status(note, log=logger.error, e=e) - - try: - # Confirm transaction - tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash, timeout=360) - - tx_url = f"{self.endpoint.explorer}/tx/{tx_hash.hex()}" - - if tx_receipt["status"] == 0: - msg = f"Transaction reverted. ({tx_url})" - return tx_receipt, error_status(msg, log=logger.error) - - except Exception as e: - note = "Failed to confirm transaction" - return None, error_status(note, log=logger.error, e=e) - - if status.ok and not status.error: - logger.info(f"View reported data: \n{tx_url}") - else: - logger.error(status) - - return tx_receipt, status - - 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(): - _, _ = await self.report_once() - 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 diff --git a/src/telliot_feeds/reporters/reporter_autopay_utils.py b/src/telliot_feeds/reporters/reporter_autopay_utils.py deleted file mode 100644 index 2891a5a9..00000000 --- a/src/telliot_feeds/reporters/reporter_autopay_utils.py +++ /dev/null @@ -1,573 +0,0 @@ -""" -Uses Python's interface from https://github.com/banteg/multicall.py.git for MakerDAO's multicall contract. -Multicall contract helps reduce node calls by combining contract function calls -and returning the values all together. This is helpful especially if API nodes like Infura are being used. -""" -import math -from dataclasses import dataclass -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple - -from clamfig.base import Registry -from eth_abi import decode_single -from multicall import Call -from multicall import Multicall -from telliot_core.tellor.tellorflex.autopay import TellorFlexAutopayContract -from telliot_core.utils.response import error_status -from telliot_core.utils.timestamp import TimeStamp -from web3.exceptions import ContractLogicError -from web3.main import Web3 - -from telliot_feeds.feeds import CATALOG_FEEDS -from telliot_feeds.queries.query_catalog import query_catalog -from telliot_feeds.reporters.tips import CATALOG_QUERY_IDS -from telliot_feeds.utils.log import get_logger - -logger = get_logger(__name__) - - -# chains where autopay contract is deployed -AUTOPAY_CHAINS = ( - 137, - 80001, - 69, - 1666600000, - 1666700000, - 421611, - 42161, - 10200, - 100, - 10, - 420, - 421613, - 3141, - 314159, - 314, - 11155111, - 369, - 943, -) - - -@dataclass -class Tag: - query_tag: str - feed_id: str - - -@dataclass -class FeedDetails: - """Data types for feed details contract response""" - - reward: int - balance: int - startTime: int - interval: int - window: int - priceThreshold: int - feedsWithFundingIndex: int - - -class AutopayCalls: - def __init__(self, autopay: TellorFlexAutopayContract, catalog: Dict[bytes, str] = CATALOG_QUERY_IDS): - self.autopay = autopay - self.w3: Web3 = autopay.node._web3 - self.catalog = catalog - - async def get_current_feeds(self, require_success: bool = False) -> Optional[Dict[str, Any]]: - """ - Getter for: - - feed ids list for each query id in catalog - - a report's timestamp index from oracle for current timestamp and three months ago - (used for getting all timestamps for the past three months) - - Reason of why three months: reporters can't claim tips from funded feeds past three months - getting three months of timestamp is useful to determine if there will be a balance after every eligible - timestamp claims a tip thus draining the feeds balance as a result - - Return: - - {'tag': (feed_id_bytes,), ('tag', 'current_time'): index_at_timestamp, - ('tag', 'three_mos_ago'): index_at_timestamp} - """ - calls = [] - current_time = TimeStamp.now().ts - three_mos_ago = current_time - 7889238 # 3 months in seconds - for query_id, tag in self.catalog.items(): - if "legacy" in tag or "spot" in tag: - calls.append( - Call( - self.autopay.address, - ["getCurrentFeeds(bytes32)(bytes32[])", query_id], - [[tag, None]], - ) - ) - calls.append( - Call( - self.autopay.address, - ["getIndexForDataBefore(bytes32,uint256)(bool,uint256)", query_id, current_time], - [["disregard_boolean", None], [(tag, "current_time"), None]], - ) - ) - calls.append( - Call( - self.autopay.address, - ["getIndexForDataBefore(bytes32,uint256)(bool,uint256)", query_id, three_mos_ago], - [["disregard_boolean", None], [(tag, "three_mos_ago"), None]], - ) - ) - data = await safe_multicall(calls, self.w3, require_success) - if not data: - return None - try: - data.pop("disregard_boolean") - except KeyError as e: - msg = f"No feeds returned by multicall, KeyError: {e}" - logger.warning(msg) - return data - - async def get_feed_details(self, require_success: bool = False) -> Any: - """ - Getter for: - - timestamps for three months of reports to oracle using queryId and index - - feed details of all feedIds for every queryId - - current values from oracle for every queryId in catalog (used to determine - can submit now in eligible window) - - Return: - - {('current_feeds', 'tag', 'feed_id'): [feed_details], ('current_values', 'tag'): True, - ('current_values', 'tag', 'current_price'): float(price), - ('current_values', 'tag', 'timestamp'): 1655137179} - """ - - current_feeds = await self.get_current_feeds() - - if not current_feeds: - logger.info("No available feeds") - return None - - # separate items from current feeds - # multicall for multiple different functions returns different types of data at once - # ie the response is {"tag": (feedids, ), ('trb-usd-spot', 'current_time'): 0, - # ('trb-usd-spot', 'three_mos_ago'): 0,eth-jpy-spot: (),'eth-jpy-spot', 'current_time'): 0, - # ('eth-jpy-spot', 'three_mos_ago'): 0} - # here we filter out the tag key if it is string and its value is of length > 0 - - tags_with_feed_ids = { - tag: feed_id - for tag, feed_id in current_feeds.items() - if (not isinstance(tag, tuple) and (current_feeds[tag])) - } - idx_current: List[int] = [] # indices for every query id reports' current timestamps - idx_three_mos_ago: List[int] = [] # indices for every query id reports' three months ago timestamps - tags: List[str] = [] # query tags from catalog - for key in current_feeds: - if isinstance(key, tuple) and key[0] in tags_with_feed_ids: - if key[1] == "current_time": - idx_current.append(current_feeds[key]) - tags.append((key[0], tags_with_feed_ids[key[0]])) - else: - idx_three_mos_ago.append(current_feeds[key]) - - merged_indices = list(zip(idx_current, idx_three_mos_ago)) - merged_query_idx = dict(zip(tags, merged_indices)) - - get_timestampby_query_id_n_idx_call = [] - - tag: str - for (tag, _), (end, start) in merged_query_idx.items(): - if start and end: - for idx in range(start, end): - - get_timestampby_query_id_n_idx_call.append( - Call( - self.autopay.address, - [ - "getTimestampbyQueryIdandIndex(bytes32,uint256)(uint256)", - query_catalog._entries[tag].query.query_id, - idx, - ], - [[(tag, idx), None]], - ) - ) - - def _to_list(_: bool, val: Any) -> List[Any]: - """Helper function, converts feed_details from tuple to list""" - return list(val) - - get_data_feed_call = [] - - feed_ids: List[bytes] - for tag, feed_ids in merged_query_idx: - for feed_id in feed_ids: - c = Call( - self.autopay.address, - ["getDataFeed(bytes32)((uint256,uint256,uint256,uint256,uint256,uint256,uint256))", feed_id], - [[("current_feeds", tag, feed_id.hex()), _to_list]], - ) - - get_data_feed_call.append(c) - - get_current_values_call = [] - - for tag, _ in merged_query_idx: - - c = Call( - self.autopay.address, - ["getCurrentValue(bytes32)(bool,bytes,uint256)", query_catalog._entries[tag].query.query_id], - [ - [("current_values", tag), None], - [("current_values", tag, "current_price"), None], - [("current_values", tag, "timestamp"), None], - ], - ) - - get_current_values_call.append(c) - - calls = get_data_feed_call + get_current_values_call + get_timestampby_query_id_n_idx_call - return await safe_multicall(calls, self.w3, require_success) - - async def reward_claim_status( - self, require_success: bool = False - ) -> Tuple[Optional[Dict[Any, Any]], Optional[Dict[Any, Any]], Optional[Dict[Any, Any]]]: - """ - Getter that checks if a timestamp's tip has been claimed - """ - feed_details_before_check = await self.get_feed_details() - if not feed_details_before_check: - logger.info("No feeds balance to check") - return None, None, None - # create a key to use for the first timestamp since it doesn't have a before value that needs to be checked - feed_details_before_check[(0, 0)] = 0 - timestamp_before_key = (0, 0) - - feeds = {} - current_values = {} - for i, j in feed_details_before_check.items(): - if "current_feeds" in i: - feeds[i] = j - elif "current_values" in i: - current_values[i] = j - - reward_claimed_status_call = [] - for _, tag, feed_id in feeds: - details = FeedDetails(*feeds[(_, tag, feed_id)]) - for keys in list(feed_details_before_check): - if "current_feeds" not in keys and "current_values" not in keys: - if tag in keys: - is_first = _is_timestamp_first_in_window( - feed_details_before_check[timestamp_before_key], - feed_details_before_check[keys], - details.startTime, - details.window, - details.interval, - ) - timestamp_before_key = keys - if is_first: - reward_claimed_status_call.append( - Call( - self.autopay.address, - [ - "getRewardClaimedStatus(bytes32,bytes32,uint256)(bool)", - bytes.fromhex(feed_id), - query_catalog._entries[tag].query.query_id, - feed_details_before_check[keys], - ], - [[(tag, feed_id, feed_details_before_check[keys]), None]], - ) - ) - - data = await safe_multicall(reward_claimed_status_call, self.w3, require_success) - if data is not None: - return feeds, current_values, data - else: - return None, None, None - - async def get_current_tip(self, require_success: bool = False) -> Optional[Dict[str, Any]]: - """ - Return response from autopay getCurrenTip call. - Default value for require_success is False because AutoPay returns an - error if tip amount is zero. - """ - calls = [ - Call(self.autopay.address, ["getCurrentTip(bytes32)(uint256)", query_id], [[self.catalog[query_id], None]]) - for query_id in self.catalog - ] - return await safe_multicall(calls, self.w3, require_success) - - -async def get_feed_tip(query: bytes, autopay: TellorFlexAutopayContract) -> Optional[int]: - """ - Get total tips for a query id with funded feeds - - - query: if the query exists in the telliot queries catalog then input should be the query id, - otherwise input should be the query data for newly generated ids in order to determine if submission - for the query is supported by telliot - """ - if not autopay.connect().ok: - msg = "can't suggest feed, autopay contract not connected" - error_status(note=msg, log=logger.critical) - return None - - if query in CATALOG_QUERY_IDS: - query_id = query - single_query = {query_id: CATALOG_QUERY_IDS[query_id]} - else: - try: - query_data = query - qtype_name, _ = decode_single("(string,bytes)", query_data) - except OverflowError: - logger.warning("Query data not available to decode") - return None - if qtype_name not in Registry.registry: - logger.warning(f"Unsupported query type: {qtype_name}") - return None - else: - query_id = Web3.keccak(query_data) - CATALOG_QUERY_IDS[query_id] = query_id.hex() - single_query = {query_id: CATALOG_QUERY_IDS[query_id]} - - autopay_calls = AutopayCalls(autopay, catalog=single_query) - feed_tips = await get_continuous_tips(autopay, autopay_calls) - if feed_tips is None: - tips = 0 - msg = "No feeds available for query id" - logger.warning(msg) - return tips - try: - tips = feed_tips[CATALOG_QUERY_IDS[query_id]] - except KeyError: - msg = f"Tips for {CATALOG_QUERY_IDS[query_id]} not showing" - logger.warning(CATALOG_QUERY_IDS[query_id]) - return None - return tips - - -async def get_one_time_tips( - autopay: TellorFlexAutopayContract, -) -> Any: - """ - Check query ids in catalog for one-time-tips and return query id with the most tips - """ - one_time_tips = AutopayCalls(autopay=autopay, catalog=CATALOG_QUERY_IDS) - return await one_time_tips.get_current_tip() - - -async def get_continuous_tips(autopay: TellorFlexAutopayContract, tipping_feeds: Any = None) -> Any: - """ - Check query ids in catalog for funded feeds, combine tips, and return query id with most tips - """ - if tipping_feeds is None: - tipping_feeds = AutopayCalls(autopay=autopay, catalog=CATALOG_QUERY_IDS) - response = await tipping_feeds.reward_claim_status() - if response == (None, None, None): - logger.info("No feeds to check") - return None - current_feeds, current_values, claim_status = response - # filter out feeds that don't have a remaining balance after accounting for claimed tips - current_feeds = _remaining_feed_balance(current_feeds, claim_status) - # remove "current_feeds" word from tuple key in current_feeds dict to help when checking - # current_values dict for corresponding current values - current_feeds = {(key[1], key[2]): value for key, value in current_feeds.items()} - values_filtered = {} - for key, value in current_values.items(): - if len(key) > 2: - values_filtered[(key[1], key[2])] = value - else: - values_filtered[key[1]] = value - return await _get_feed_suggestion(current_feeds, values_filtered) - - -async def autopay_suggested_report( - autopay: TellorFlexAutopayContract, -) -> Tuple[Optional[str], Any]: - """ - Gets one-time tips and continuous tips then extracts query id with the most tips for a report suggestion - - Return: query id, tip amount - """ - chain = autopay.node.chain_id - if chain in AUTOPAY_CHAINS: - - # get query_ids with one time tips - singletip_dict = await get_one_time_tips(autopay) - # get query_ids with active feeds - datafeed_dict = await get_continuous_tips(autopay) - - # remove none type from dict - single_tip_suggestion = {} - if singletip_dict is not None: - single_tip_suggestion = {i: j for i, j in singletip_dict.items() if j} - - datafeed_suggestion = {} - if datafeed_dict is not None: - datafeed_suggestion = {i: j for i, j in datafeed_dict.items() if j} - - # combine feed dicts and add tips for duplicate query ids - combined_dict = { - key: _add_values(single_tip_suggestion.get(key), datafeed_suggestion.get(key)) - for key in single_tip_suggestion | datafeed_suggestion - } - # get feed with most tips - tips_sorted = sorted(combined_dict.items(), key=lambda item: item[1], reverse=True) # type: ignore - if tips_sorted: - suggested_feed = tips_sorted[0] - return suggested_feed[0], suggested_feed[1] - else: - return None, None - else: - logger.warning(f"Chain {chain} does not have Autopay contract support") - return None, None - - -async def _get_feed_suggestion(feeds: Any, current_values: Any) -> Any: - """ - Calculates tips and checks if a submission is in an eligible window for a feed submission - for a given query_id and feed_ids - - Return: a dict tag:tip amount - """ - current_time = TimeStamp.now().ts - query_id_with_tips = {} - - for query_tag, feed_id in feeds: # i is (query_id,feed_id) - if feeds[(query_tag, feed_id)] is not None: # feed_detail[i] is (details) - try: - feed_details = FeedDetails(*feeds[(query_tag, feed_id)]) - except TypeError: - msg = "couldn't decode feed details from contract" - continue - except Exception as e: - msg = f"unknown error decoding feed details from contract: {e}" - continue - - if feed_details.balance <= 0: - continue - num_intervals = math.floor((current_time - feed_details.startTime) / feed_details.interval) - # Start time of latest submission window - current_window_start = feed_details.startTime + (feed_details.interval * num_intervals) - - if not current_values[query_tag]: - value_before_now = 0 - timestamp_before_now = 0 - else: - value_before_now = current_values[(query_tag, "current_price")] - timestamp_before_now = current_values[(query_tag, "timestamp")] - - rules = [ - (current_time - current_window_start) < feed_details.window, - timestamp_before_now < current_window_start, - ] - if not all(rules): - msg = f"{query_tag}, isn't eligible for a tip" - error_status(note=msg, log=logger.info) - continue - - if feed_details.priceThreshold == 0: - if query_tag not in query_id_with_tips: - query_id_with_tips[query_tag] = feed_details.reward - else: - query_id_with_tips[query_tag] += feed_details.reward - else: - datafeed = CATALOG_FEEDS[query_tag] - value_now = await datafeed.source.fetch_new_datapoint() - # value is always a number for a price oracle submission - # convert bytes value to int - try: - value_before_now = int(int(current_values[(query_tag, "current_price")].hex(), 16) / 1e18) - except ValueError: - logger.info("Can't check price threshold, oracle price submission not a number") - continue - if value_now[0] is None or value_before_now is None: - note = f"Unable to fetch {datafeed} price for tip calculation" - error_status(note=note, log=logger.warning) - continue - current_value = value_now[0] - - if value_before_now == 0: - price_change = 10000 - - elif current_value >= value_before_now: - price_change = (10000 * (current_value - value_before_now)) / value_before_now - - else: - price_change = (10000 * (value_before_now - current_value)) / value_before_now - - if price_change > feed_details.priceThreshold: - if query_tag not in query_id_with_tips: - query_id_with_tips[query_tag] = feed_details.reward - else: - query_id_with_tips[query_tag] += feed_details.reward - - return query_id_with_tips - - -async def safe_multicall(calls: List[Call], endpoint: Web3, require_success: bool) -> Optional[Dict[str, Any]]: - """ - Multicall...call with error handling - - Args: - calls: list of Call objects, representing calls made by request - endpoint: web3 RPC endpoint connection - require_success: throws error if True and contract logic reverts - - Returns: - data if multicall is successful - """ - mc = Multicall(calls=calls, _w3=endpoint, require_success=require_success) - - try: - data: Dict[str, Any] = await mc.coroutine() - return data - except ContractLogicError as e: - msg = f"Contract reversion in multicall request, ContractLogicError: {e}" - logger.warning(msg) - return None - except ValueError as e: - if "unsupported block number" in str(e): - msg = f"Unsupported block number in multicall request, ValueError: {e}" - logger.warning(msg) - return None - else: - msg = f"Error in multicall request, ValueError: {e}" - return None - - -def _add_values(x: Optional[int], y: Optional[int]) -> Optional[int]: - """Helper function to add values when combining dicts with same key""" - return sum((num for num in (x, y) if num is not None)) - - -def _is_timestamp_first_in_window( - timestamp_before: int, timestamp_to_check: int, feed_start_timestamp: int, feed_window: int, feed_interval: int -) -> bool: - """ - Calculates to check if timestamp(timestamp_to_check) is first in window - - Return: bool - """ - # Number of intervals since start time - num_intervals = math.floor((timestamp_to_check - feed_start_timestamp) / feed_interval) - # Start time of latest submission window - current_window_start = feed_start_timestamp + (feed_interval * num_intervals) - eligible = [(timestamp_to_check - current_window_start) < feed_window, timestamp_before < current_window_start] - return all(eligible) - - -def _remaining_feed_balance(current_feeds: Any, reward_claimed_status: Any) -> Any: - """ - Checks if a feed has a remaining balance - - """ - for _, tag, feed_id in current_feeds: - details = FeedDetails(*current_feeds[_, tag, feed_id]) - balance = details.balance - if balance > 0: - for _tag, _feed_id, timestamp in reward_claimed_status: - if balance > 0 and tag == _tag and feed_id == _feed_id: - if not reward_claimed_status[tag, feed_id, timestamp]: - balance -= details.reward - current_feeds[_, tag, feed_id][1] = max(balance, 0) - return current_feeds diff --git a/src/telliot_feeds/reporters/rng_interval.py b/src/telliot_feeds/reporters/rng_interval.py index 27ab8e6d..f183348c 100644 --- a/src/telliot_feeds/reporters/rng_interval.py +++ b/src/telliot_feeds/reporters/rng_interval.py @@ -12,7 +12,6 @@ from telliot_feeds.datafeed import DataFeed from telliot_feeds.feeds.tellor_rng_feed import assemble_rng_datafeed from telliot_feeds.queries.tellor_rng import TellorRNG -from telliot_feeds.reporters.reporter_autopay_utils import get_feed_tip from telliot_feeds.reporters.tellor_360 import Tellor360Reporter from telliot_feeds.utils.log import get_logger @@ -59,22 +58,8 @@ async def fetch_datafeed(self) -> Optional[DataFeed[Any]]: error_status(note=msg, log=logger.warning) return None self.datafeed = datafeed - tip = 0 - - single_tip, status = await self.autopay.get_current_tip(datafeed.query.query_id) - if not status.ok: - msg = "Unable to fetch single tip" - error_status(msg, log=logger.warning) - return None - tip += single_tip - - feed_tip = await get_feed_tip( - datafeed.query.query_data, self.autopay - ) # input query data instead of query id to use tip listener - if feed_tip is None: - msg = "Unable to fetch feed tip" - error_status(msg, log=logger.warning) - return None - tip += feed_tip - logger.debug(f"Current tip for RNG query: {tip}") + # if check_rewards is True, autopay and TBR(if mainnet) are checked + if self.check_rewards: + tip = await self.rewards() + logger.info(f"Tip found for RNG feed: {tip}") return datafeed diff --git a/src/telliot_feeds/reporters/stake.py b/src/telliot_feeds/reporters/stake.py new file mode 100644 index 00000000..e6619aa6 --- /dev/null +++ b/src/telliot_feeds/reporters/stake.py @@ -0,0 +1,121 @@ +import time +from typing import Any +from typing import Optional +from typing import Tuple + +from telliot_core.contract.contract import Contract +from telliot_core.utils.response import error_status +from telliot_core.utils.response import ResponseStatus + +from telliot_feeds.reporters.gas import GasFees +from telliot_feeds.utils.log import get_logger + +logger = get_logger(__name__) + + +class Stake(GasFees): + """Stake tokens to tellor oracle""" + + def __init__( + self, + oracle: Contract, + token: Contract, + *args: Any, + **kwargs: Any, + ): + super().__init__(*args, **kwargs) + self.oracle = oracle + self.token = token + + async def get_current_token_balance(self) -> Tuple[Optional[int], ResponseStatus]: + """Reads the current balance of the account""" + wallet_balance: int + wallet_balance, status = await self.token.read("balanceOf", account=self.acct_address) + if not status.ok: + msg = f"Unable to read account balance: {status.error}" + return None, error_status(msg, status.e, log=logger.error) + + logger.info(f"Current wallet TRB balance: {self.to_ether(wallet_balance)!r}") + return wallet_balance, status + + async def check_allowance(self, amount: int) -> Tuple[Optional[int], ResponseStatus]: + """ "Read the spender allowance for the accounts TRB""" + allowance, allowance_status = await self.token.read( + "allowance", owner=self.acct_address, spender=self.oracle.address + ) + if not allowance_status.ok: + msg = "Unable to check allowance:" + return None, error_status(msg, e=allowance_status.error, log=logger.error) + + logger.debug(f"Current allowance: {self.to_ether(allowance):.04f}") + return allowance, allowance_status + + async def approve_spending(self, amount: int) -> Tuple[bool, ResponseStatus]: + """Approve contract to spend TRB tokens""" + logger.info(f"Approving {self.oracle.address} token spending: {amount}...") + # calculate and set gas params + status = self.update_gas_fees() + if not status.ok: + return False, error_status("unable to calculate fees for approve txn", e=status.error, log=logger.error) + fees = self.get_gas_info_core() + + approve_receipt, approve_status = await self.token.write( + func_name="approve", + # have to convert to gwei because of telliot_core where numbers are converted to wei + # consider changing this in telliot_core + spender=self.oracle.address, + amount=amount, + **fees, + ) + if not approve_status.ok: + msg = "Unable to approve staking: " + return False, error_status(msg, e=approve_status.error, log=logger.error) + logger.debug(f"Approve transaction status: {approve_receipt.status}, block: {approve_receipt.blockNumber}") + return True, approve_status + + async def deposit_stake(self, amount: int) -> Tuple[bool, ResponseStatus]: + """Deposits stake into the oracle contract""" + # check TRB wallet balance! + wallet_balance, wallet_balance_status = await self.get_current_token_balance() + if wallet_balance is None or not wallet_balance_status.ok: + return False, wallet_balance_status + + if amount > wallet_balance: + msg = ( + f"Amount to stake: {self.to_ether(amount):.04f} " + f"is greater than your balance: {self.to_ether(wallet_balance):.04f} so " + "not enough TRB to cover the stake" + ) + return False, error_status(msg, log=logger.warning) + + # check allowance to avoid unnecessary approval transactions + allowance, allowance_status = await self.check_allowance(amount) + if allowance is None or not allowance_status.ok: + return False, allowance_status + + # if allowance is less than amount_to_stake then approve + if allowance < amount: + approve_receipt, approve_status = await self.approve_spending(amount - allowance) + if not approve_receipt or not approve_status.ok: + return False, approve_status + # Add this to avoid nonce error from txn happening too fast + time.sleep(1) + + # deposit stake + logger.info(f"Now depositing stake: {amount}...") + # calculate and set gas params + status = self.update_gas_fees() + if not status.ok: + return False, error_status("unable to calculate fees for deposit txn", e=status.error, log=logger.error) + + fees = self.get_gas_info_core() + deposit_receipt, deposit_status = await self.oracle.write( + func_name="depositStake", + _amount=amount, + **fees, + ) + if not deposit_status.ok: + msg = "Unable to deposit stake!" + return False, error_status(msg, e=deposit_status.error, log=logger.error) + logger.debug(f"Deposit transaction status: {deposit_receipt.status}, block: {deposit_receipt.blockNumber}") + return True, deposit_status diff --git a/src/telliot_feeds/reporters/tellor_360.py b/src/telliot_feeds/reporters/tellor_360.py index 94055c50..d315d4fd 100644 --- a/src/telliot_feeds/reporters/tellor_360.py +++ b/src/telliot_feeds/reporters/tellor_360.py @@ -1,63 +1,100 @@ +import asyncio import math import time -from dataclasses import dataclass from datetime import timedelta from typing import Any +from typing import Dict from typing import Optional from typing import Tuple +from typing import Union from eth_abi.exceptions import EncodingTypeError from eth_utils import to_checksum_address +from telliot_core.contract.contract import Contract +from telliot_core.utils.key_helpers import lazy_unlock_account from telliot_core.utils.response import error_status from telliot_core.utils.response import ResponseStatus +from web3.contract import ContractFunction +from web3.types import TxParams +from web3.types import TxReceipt from telliot_feeds.constants import CHAINS_WITH_TBR from telliot_feeds.feeds import DataFeed +from telliot_feeds.feeds.trb_usd_feed import trb_usd_median_feed from telliot_feeds.reporters.rewards.time_based_rewards import get_time_based_rewards -from telliot_feeds.reporters.tellor_flex import TellorFlexReporter +from telliot_feeds.reporters.stake import Stake from telliot_feeds.reporters.tips.suggest_datafeed import get_feed_and_tip from telliot_feeds.reporters.tips.tip_amount import fetch_feed_tip +from telliot_feeds.reporters.types import GasParams +from telliot_feeds.reporters.types import StakerInfo from telliot_feeds.utils.log import get_logger +from telliot_feeds.utils.reporter_utils import get_native_token_feed +from telliot_feeds.utils.reporter_utils import has_native_token_funds +from telliot_feeds.utils.reporter_utils import is_online from telliot_feeds.utils.reporter_utils import suggest_random_feed +from telliot_feeds.utils.reporter_utils import tkn_symbol +from telliot_feeds.utils.stake_info import StakeInfo logger = get_logger(__name__) -@dataclass -class StakerInfo: - """Data types for staker info - start_date: TimeStamp - stake_balance: int - locked_balance: int - reward_debt: int - last_report: TimeStamp - reports_count: int - gov_vote_count: int - vote_count: int - in_total_stakers: bool - """ - - start_date: int - stake_balance: int - locked_balance: int - reward_debt: int - last_report: int - reports_count: int - gov_vote_count: int - vote_count: int - in_total_stakers: bool - - -class Tellor360Reporter(TellorFlexReporter): - def __init__(self, stake: float = 0, use_random_feeds: bool = False, *args: Any, **kwargs: Any) -> None: - self.stake_amount: Optional[int] = None - self.staker_info: Optional[StakerInfo] = None - self.allowed_stake_amount = 0 - super().__init__(*args, **kwargs) - self.stake: float = stake +class Tellor360Reporter(Stake): + """Reports values from given datafeeds to a TellorFlex.""" + + def __init__( + self, + autopay: Contract, + chain_id: int, + datafeed: Optional[DataFeed[Any]] = None, + expected_profit: Union[str, float] = "YOLO", + wait_period: int = 7, + check_rewards: bool = True, + ignore_tbr: bool = False, # relevant only for eth-mainnet and eth-testnets + stake: float = 0, + use_random_feeds: bool = False, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.autopay = autopay + self.datafeed = datafeed self.use_random_feeds: bool = use_random_feeds + self.qtag_selected = False if self.datafeed is None else True + self.expected_profit = expected_profit + self.stake: float = self.from_ether(stake) + self.stake_info = StakeInfo() + self.check_rewards: bool = check_rewards + self.autopaytip = 0 + self.ignore_tbr = ignore_tbr + self.wait_period = wait_period + self.chain_id = chain_id + self.acct_addr = to_checksum_address(self.account.address) + logger.info(f"Reporting with account: {self.acct_addr}") + + async def get_stake_amount(self) -> Tuple[Optional[int], ResponseStatus]: + """Reads the current stake amount from the oracle contract + + Returns: + - (int, ResponseStatus) the current stake amount in TellorFlex + """ + stake_amount: int + stake_amount, status = await self.oracle.read("getStakeAmount") + if not status.ok: + msg = f"Unable to read current stake amount: {status.error}" + return None, error_status(msg, status.e, log=logger.error) + return stake_amount, status - assert self.acct_addr == to_checksum_address(self.account.address) + async def get_staker_details(self) -> Tuple[Optional[StakerInfo], ResponseStatus]: + """Reads the staker details for the account from the oracle contract + + Returns: + - (StakerInfo, ResponseStatus) the staker details for the account + """ + response, status = await self.oracle.read("getStakerInfo", _stakerAddress=self.acct_addr) + if not status.ok: + msg = f"Unable to read account staker info: {status.error}" + return None, error_status(msg, status.e, log=logger.error) + staker_details = StakerInfo(*response) + return staker_details, status async def ensure_staked(self) -> Tuple[bool, ResponseStatus]: """Compares stakeAmount and stakerInfo every loop to monitor changes to the stakeAmount or stakerInfo @@ -67,152 +104,60 @@ async def ensure_staked(self) -> Tuple[bool, ResponseStatus]: - (bool, ResponseStatus) """ # get oracle required stake amount - stake_amount: int - stake_amount, status = await self.oracle.read("getStakeAmount") - - if (not status.ok) or (stake_amount is None): - msg = "Unable to read current stake amount" - return False, error_status(msg, log=logger.info) - - logger.info(f"Current Oracle stakeAmount: {stake_amount / 1e18!r}") + stake_amount, status = await self.get_stake_amount() + if not status.ok or stake_amount is None: + return False, status + # store stake amount + self.stake_info.store_stake_amount(stake_amount) + # check if stake amount changed (logs when it does) + self.stake_info.stake_amount_change() # get accounts current stake total - staker_info, status = await self.oracle.read( - "getStakerInfo", - _stakerAddress=self.acct_addr, - ) - if (not status.ok) or (staker_info is None): - msg = "Unable to read reporters staker info" - return False, error_status(msg, log=logger.info) - - # first when reporter start set stakerInfo - if self.staker_info is None: - self.staker_info = StakerInfo(*staker_info) - - # on subsequent loops keeps checking if staked balance in oracle contract decreased - # if it decreased account is probably dispute barring withdrawal - if self.staker_info.stake_balance > staker_info[1]: - # update balance in script - self.staker_info.stake_balance = staker_info[1] - logger.info("your staked balance has decreased and account might be in dispute") - - # after the first loop keep track of the last report's timestamp to calculate reporter lock - self.staker_info.last_report = staker_info[4] - self.staker_info.reports_count = staker_info[5] + staker_details, status = await self.get_staker_details() + if not status.ok or staker_details is None: + return False, status + # store staker balance + self.stake_info.store_staker_balance(staker_details.stake_balance) + # update time of last report + self.stake_info.update_last_report_time(staker_details.last_report) + # update reports count + self.stake_info.update_reports_count(staker_details.reports_count) + # check if staker balance changed which means a value they submitted has been disputed + # (logs when it does) + self.stake_info.is_in_dispute() logger.info( f""" STAKER INFO - start date: {staker_info[0]} - stake_balance: {staker_info[1] / 1e18!r} - locked_balance: {staker_info[2]} - last report: {staker_info[4]} - reports count: {staker_info[5]} + start date: {staker_details.start_date} + stake_balance: {self.to_ether(staker_details.stake_balance)!r} + locked_balance: {staker_details.locked_balance} + last report: {staker_details.last_report} + reports count: {staker_details.reports_count} """ ) - account_staked_bal = self.staker_info.stake_balance - - # after the first loop, logs if stakeAmount has increased or decreased - if self.stake_amount is not None: - if self.stake_amount < stake_amount: - logger.info("Stake amount has increased possibly due to TRB price change.") - elif self.stake_amount > stake_amount: - logger.info("Stake amount has decreased possibly due to TRB price change.") - - self.stake_amount = stake_amount - # deposit stake if stakeAmount in oracle is greater than account stake or # a stake in cli is selected thats greater than account stake - if self.stake_amount > account_staked_bal or (self.stake * 1e18) > account_staked_bal: + chosen_stake_amount = self.stake > staker_details.stake_balance + if chosen_stake_amount: + logger.info("Chosen stake is greater than account stake balance") + if self.stake_info.stake_amount_gt_staker_balance or chosen_stake_amount: logger.info("Approving and depositing stake...") - # amount to deposit whichever largest difference either chosen stake or stakeAmount to keep reporting - stake_diff = max(int(self.stake_amount - account_staked_bal), int((self.stake * 1e18) - account_staked_bal)) - - # check TRB wallet balance! - wallet_balance, wallet_balance_status = await self.token.read("balanceOf", account=self.acct_addr) - - if not wallet_balance_status.ok: - msg = "unable to read account TRB balance" - return False, error_status(msg, log=logger.info) - - logger.info(f"Current wallet TRB balance: {wallet_balance / 1e18!r}") - - if stake_diff > wallet_balance: - msg = "Not enough TRB in the account to cover the stake" - return False, error_status(msg, log=logger.warning) - # approve token spending - if self.transaction_type == 2: - priority_fee, max_fee = self.get_fee_info() - if priority_fee is None or max_fee is None: - return False, error_status("Unable to suggest type 2 txn fees", log=logger.error) - # Approve token spending for a transaction type 2 - receipt, approve_status = await self.token.write( - func_name="approve", - gas_limit=self.gas_limit, - max_priority_fee_per_gas=priority_fee, - max_fee_per_gas=max_fee, - spender=self.oracle.address, - amount=stake_diff, - ) - if not approve_status.ok: - msg = "Unable to approve staking" - return False, error_status(msg, log=logger.error) - logger.debug(f"Approve transaction status: {receipt.status}, block: {receipt.blockNumber}") - # deposit stake for a transaction type 2 - _, deposit_status = await self.oracle.write( - func_name="depositStake", - gas_limit=self.gas_limit, - max_priority_fee_per_gas=priority_fee, - max_fee_per_gas=max_fee, - _amount=stake_diff, - ) - - if not deposit_status.ok: - msg = "Unable to deposit stake" - return False, error_status(msg, log=logger.error) - else: - # Fetch legacy gas price if not provided by user - if self.legacy_gas_price is None: - gas_price_in_gwei = await self.fetch_gas_price() - if not gas_price_in_gwei: - note = "Unable to fetch gas price for tx type 0" - return False, error_status(note, log=logger.warning) - else: - gas_price_in_gwei = self.legacy_gas_price - # Approve token spending for a transaction type 0 and deposit stake - receipt, approve_status = await self.token.write( - func_name="approve", - gas_limit=self.gas_limit, - legacy_gas_price=gas_price_in_gwei, - spender=self.oracle.address, - amount=stake_diff, - ) - if not approve_status.ok: - msg = "Unable to approve staking" - return False, error_status(msg, log=logger.error) - # Add this to avoid nonce error from txn happening too fast - time.sleep(1) - logger.debug(f"Approve transaction status: {receipt.status}, block: {receipt.blockNumber}") - # Deposit stake to oracle contract - _, deposit_status = await self.oracle.write( - func_name="depositStake", - gas_limit=self.gas_limit, - legacy_gas_price=gas_price_in_gwei, - _amount=stake_diff, - ) - if not deposit_status.ok: - msg = ( - "Unable to stake deposit: " - + deposit_status.error - + f"Make sure {self.acct_addr} has enough of the current chain's " - + "currency and the oracle's currency (TRB)" - ) - return False, error_status(msg, log=logger.error) + # current oracle stake amount vs current account stake balance + to_stake_amount_1 = self.stake_info.current_stake_amount - staker_details.stake_balance + # chosen stake via cli flag vs current account stake balance + to_stake_amount_2 = self.stake - staker_details.stake_balance + amount_to_stake = max(int(to_stake_amount_1), int(to_stake_amount_2)) + + _, deposit_status = await self.deposit_stake(amount_to_stake) + if not deposit_status.ok: + return False, deposit_status + # add staked balance after successful stake deposit - self.staker_info.stake_balance += stake_diff + self.stake_info.update_staker_balance(amount_to_stake) return True, ResponseStatus() @@ -222,16 +167,18 @@ async def check_reporter_lock(self) -> ResponseStatus: Return: - ResponseStatus: yay or nay """ - if self.staker_info is None or self.stake_amount is None: + staker_balance = self.stake_info.current_staker_balance + current_stake_amount = self.stake_info.current_stake_amount + if staker_balance is None or current_stake_amount is None: msg = "Unable to calculate reporter lock remaining time" return error_status(msg, log=logger.info) # 12hrs in seconds is 43200 try: - reporter_lock = 43200 / math.floor(self.staker_info.stake_balance / self.stake_amount) + reporter_lock = 43200 / math.floor(staker_balance / current_stake_amount) except ZeroDivisionError: # Tellor Playground contract's stakeAmount is 0 reporter_lock = 0 - time_remaining = round(self.staker_info.last_report + reporter_lock - time.time()) + time_remaining = round(self.stake_info.last_report_time + reporter_lock - time.time()) if time_remaining > 0: hr_min_sec = str(timedelta(seconds=time_remaining)) msg = "Currently in reporter lock. Time left: " + hr_min_sec @@ -253,7 +200,7 @@ async def rewards(self) -> int: elif self.chain_id in CHAINS_WITH_TBR: logger.info("Fetching time based rewards") time_based_rewards = await get_time_based_rewards(self.oracle) - logger.info(f"Time based rewards: {time_based_rewards/1e18:.04f}") + logger.info(f"Time based rewards: {self.to_ether(time_based_rewards):.04f}") if time_based_rewards is not None: self.autopaytip += time_based_rewards return self.autopaytip @@ -288,10 +235,265 @@ async def fetch_datafeed(self) -> Optional[DataFeed[Any]]: if suggested_feed is not None and tip_amount is not None: logger.info(f"Most funded datafeed in Autopay: {suggested_feed.query.type}") - logger.info(f"Tip amount: {tip_amount/1e18}") + logger.info(f"Tip amount: {self.to_ether(tip_amount)}") self.autopaytip += tip_amount self.datafeed = suggested_feed return self.datafeed return self.datafeed + + async def ensure_profitable(self) -> ResponseStatus: + + status = ResponseStatus() + if not self.check_rewards: + return status + + tip = self.to_ether(self.autopaytip) + # Fetch token prices in USD + native_token_feed = get_native_token_feed(self.chain_id) + price_feeds = [native_token_feed, trb_usd_median_feed] + _ = await asyncio.gather(*[feed.source.fetch_new_datapoint() for feed in price_feeds]) + price_native_token = native_token_feed.source.latest[0] + price_trb_usd = trb_usd_median_feed.source.latest[0] + + if price_native_token is None or price_trb_usd is None: + return error_status("Unable to fetch token price", log=logger.warning) + + gas_info = self.get_gas_info_core() + max_fee_per_gas = gas_info["max_fee_per_gas"] + legacy_gas_price = gas_info["legacy_gas_price"] + gas_limit = gas_info["gas_limit"] + + max_gas = max_fee_per_gas if max_fee_per_gas else legacy_gas_price + # multiply gasPrice by gasLimit + if max_gas is None or gas_limit is None: + return error_status("Unable to calculate profitablity, no gas fees set", log=logger.warning) + + txn_fee = max_gas * self.to_gwei(int(gas_limit)) + logger.info( + f"""\n + Tips: {tip} + Transaction fee: {txn_fee} {tkn_symbol(self.chain_id)} + Gas limit: {gas_limit} + Gas price: {legacy_gas_price} gwei + Max fee per gas: {max_fee_per_gas} gwei + Max priority fee per gas: {gas_info["max_priority_fee_per_gas"]} gwei + Txn type: {self.transaction_type}\n""" + ) + + # Calculate profit + rev_usd = tip * price_trb_usd + costs_usd = txn_fee * price_native_token # convert gwei costs to eth, then to usd + profit_usd = rev_usd - costs_usd + logger.info(f"Estimated profit: ${round(profit_usd, 2)}") + logger.info(f"tip price: {round(rev_usd, 2)}, gas costs: {costs_usd}") + + percent_profit = ((profit_usd) / costs_usd) * 100 + logger.info(f"Estimated percent profit: {round(percent_profit, 2)}%") + if (self.expected_profit != "YOLO") and ( + isinstance(self.expected_profit, float) and percent_profit < self.expected_profit + ): + status.ok = False + status.error = "Estimated profitability below threshold." + logger.info(status.error) + return status + # reset autopay tip to check for tips again + self.autopaytip = 0 + + return status + + async def get_num_reports_by_id(self, query_id: bytes) -> Tuple[int, ResponseStatus]: + count, read_status = await self.oracle.read(func_name="getNewValueCountbyQueryId", _queryId=query_id) + return count, read_status + + async def submission_txn_params(self, datafeed: DataFeed[Any]) -> Tuple[Optional[Dict[str, Any]], ResponseStatus]: + """Assemble the submitValue transaction + Params: + datafeed: The datafeed object + + Returns a tuple of the web3 function object and a ResponseStatus object + """ + # Update datafeed value + await datafeed.source.fetch_new_datapoint() + latest_data = datafeed.source.latest + if latest_data[0] is None: + msg = "Unable to retrieve updated datafeed value." + return None, error_status(msg, log=logger.info) + # Get query info & encode value to bytes + query_id = datafeed.query.query_id + query_data = datafeed.query.query_data + try: + value = datafeed.query.value_type.encode(latest_data[0]) + logger.debug(f"Current query: {datafeed.query.descriptor}") + logger.debug(f"Reporter Encoded value: {value.hex()}") + except Exception as e: + msg = f"Error encoding response value {latest_data[0]}" + return None, error_status(msg, e=e, log=logger.error) + + # Get nonce + report_count, read_status = await self.get_num_reports_by_id(query_id) + if not read_status.ok: + msg = f"Unable to retrieve report count for query id: {read_status.error}" + return None, error_status(msg, read_status.e, logger.error) + + params = {"_queryId": query_id, "_value": value, "_nonce": report_count, "_queryData": query_data} + return params, ResponseStatus() + + def assemble_function( + self, function_name: str, **transaction_params: Any + ) -> Tuple[Optional[ContractFunction], ResponseStatus]: + """Assemble a contract function""" + try: + func = self.oracle.contract.get_function_by_name(function_name)(**transaction_params) + return func, ResponseStatus() + except Exception as e: + return None, error_status("Error assembling function", e, logger.error) + + def build_transaction( + self, function_name: str, **transaction_params: Any + ) -> Tuple[Optional[TxParams], ResponseStatus]: + """Build a transaction""" + + contract_function, status = self.assemble_function(function_name=function_name, **transaction_params) + if contract_function is None: + return None, error_status("Error building function to estimate gas", status.e, logger.error) + + # set gas parameters globally + status = self.update_gas_fees() + logger.debug(status) + if not status.ok: + return None, error_status("Error setting gas parameters", status.e, logger.error) + + _, status = self.estimate_gas_amount(contract_function) + if not status.ok: + return None, error_status(f"Error estimating gas for function: {contract_function}", status.e, logger.error) + + params, status = self.tx_params(**self.get_gas_info()) + logger.debug(f"Transaction parameters: {params}") + if params is None: + return None, error_status("Error getting transaction parameters", status.e, logger.error) + + return contract_function.buildTransaction(params), ResponseStatus() # type: ignore + + def sign_n_send_transaction(self, built_tx: Any) -> Tuple[Optional[TxReceipt], ResponseStatus]: + """Send a signed transaction to the blockchain and wait for confirmation + + Params: + tx_signed: The signed transaction object + + Returns a tuple of the transaction receipt and a ResponseStatus object + """ + lazy_unlock_account(self.account) + local_account = self.account.local_account + tx_signed = local_account.sign_transaction(built_tx) + try: + tx_hash = self.web3.eth.send_raw_transaction(tx_signed.rawTransaction) + except Exception as e: + note = "Send transaction failed" + return None, error_status(note, log=logger.error, e=e) + + try: + # Confirm transaction + tx_receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash, timeout=360) + + tx_url = f"{self.endpoint.explorer}/tx/{tx_hash.hex()}" + + if tx_receipt["status"] == 0: + msg = f"Transaction reverted. ({tx_url})" + return tx_receipt, error_status(msg, log=logger.error) + + logger.info(f"View reported data: \n{tx_url}") + return tx_receipt, ResponseStatus() + except Exception as e: + note = "Failed to confirm transaction" + return None, error_status(note, log=logger.error, e=e) + + def get_acct_nonce(self) -> Tuple[Optional[int], ResponseStatus]: + """Get the nonce for the account""" + try: + return self.web3.eth.get_transaction_count(self.acct_address), ResponseStatus() + except ValueError as e: + return None, error_status("Account nonce request timed out", e=e, log=logger.warning) + except Exception as e: + return None, error_status("Unable to retrieve account nonce", e=e, log=logger.error) + + def tx_params(self, **gas_fees: GasParams) -> Tuple[Optional[Dict[str, Any]], ResponseStatus]: + """Return transaction parameters""" + nonce, status = self.get_acct_nonce() + if nonce is None: + return None, status + return { + "nonce": nonce, + "chainId": self.chain_id, + **gas_fees, + }, ResponseStatus() + + def has_native_token(self) -> bool: + """Check if account has native token funds for a network for gas fees + of at least min_native_token_balance that is set in the cli""" + return has_native_token_funds(self.acct_addr, self.web3, min_balance=self.min_native_token_balance) + + async def report_once( + self, + ) -> Tuple[Optional[TxReceipt], ResponseStatus]: + """Report query value once + This method checks to see if a user is able to submit + values to the oracle, given their staker status + and last submission time. Also, this method does not + submit values if doing so won't make a profit.""" + # Check staker status + staked, status = await self.ensure_staked() + if not staked or not status.ok: + return None, status + + status = await self.check_reporter_lock() + if not status.ok: + return None, status + + # Get suggested datafeed if none provided + datafeed = await self.fetch_datafeed() + if not datafeed: + msg = "Unable to suggest datafeed" + return None, error_status(note=msg, log=logger.info) + + params, status = await self.submission_txn_params(datafeed) + if not status.ok or params is None: + return None, status + + build_tx, status = self.build_transaction("submitValue", **params) + if not status.ok or build_tx is None: + return None, status + + # Check if profitable if not YOLO + status = await self.ensure_profitable() + logger.debug(f"Ensure profitibility method status: {status}") + if not status.ok: + return None, status + + logger.debug("Sending submitValue transaction") + tx_receipt, status = self.sign_n_send_transaction(build_tx) + # reset datafeed for a new suggestion if qtag wasn't selected in cli + if self.qtag_selected is False: + self.datafeed = None + + return tx_receipt, status + + async def is_online(self) -> bool: + return await is_online() + + 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: + if await self.is_online(): + if self.has_native_token(): + _, _ = await self.report_once() + 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 diff --git a/src/telliot_feeds/reporters/tellor_flex.py b/src/telliot_feeds/reporters/tellor_flex.py deleted file mode 100644 index a2bb56b6..00000000 --- a/src/telliot_feeds/reporters/tellor_flex.py +++ /dev/null @@ -1,344 +0,0 @@ -"""TellorFlex compatible reporters""" -import asyncio -import time -from datetime import timedelta -from typing import Any -from typing import Optional -from typing import Tuple -from typing import Union - -from chained_accounts import ChainedAccount -from eth_abi.exceptions import EncodingTypeError -from eth_utils import to_checksum_address -from telliot_core.contract.contract import Contract -from telliot_core.model.endpoints import RPCEndpoint -from telliot_core.utils.response import error_status -from telliot_core.utils.response import ResponseStatus - -from telliot_feeds.datafeed import DataFeed -from telliot_feeds.feeds import CATALOG_FEEDS -from telliot_feeds.feeds.trb_usd_feed import trb_usd_median_feed -from telliot_feeds.reporters.interval import IntervalReporter -from telliot_feeds.reporters.reporter_autopay_utils import autopay_suggested_report -from telliot_feeds.reporters.reporter_autopay_utils import CATALOG_QUERY_IDS -from telliot_feeds.reporters.reporter_autopay_utils import get_feed_tip -from telliot_feeds.utils.log import get_logger -from telliot_feeds.utils.reporter_utils import get_native_token_feed -from telliot_feeds.utils.reporter_utils import tellor_suggested_report -from telliot_feeds.utils.reporter_utils import tkn_symbol - - -logger = get_logger(__name__) - - -class TellorFlexReporter(IntervalReporter): - """Reports values from given datafeeds to a TellorFlex.""" - - def __init__( - self, - endpoint: RPCEndpoint, - account: ChainedAccount, - chain_id: int, - oracle: Contract, - token: Contract, - autopay: Contract, - stake: float = 10.0, - datafeed: Optional[DataFeed[Any]] = None, - expected_profit: Union[str, float] = "YOLO", - transaction_type: int = 2, - gas_limit: Optional[int] = None, - max_fee: int = 0, - priority_fee: float = 0.0, - legacy_gas_price: Optional[int] = None, - gas_multiplier: int = 1, # 1 percent - max_priority_fee_range: int = 80, # 80 gwei - wait_period: int = 7, - min_native_token_balance: int = 10**18, - check_rewards: bool = True, - ignore_tbr: bool = False, # relevant only for eth-mainnet and eth-testnets - ) -> None: - - self.endpoint = endpoint - self.oracle = oracle - self.token = token - self.autopay = autopay - self.stake = stake - self.datafeed = datafeed - self.chain_id = chain_id - self.acct_addr = to_checksum_address(account.address) - self.last_submission_timestamp = 0 - self.expected_profit = expected_profit - self.transaction_type = transaction_type - self.gas_limit = gas_limit - self.max_fee = max_fee - self.wait_period = wait_period - self.priority_fee = priority_fee - self.legacy_gas_price = legacy_gas_price - self.gas_multiplier = gas_multiplier - self.max_priority_fee_range = max_priority_fee_range - self.autopaytip = 0 - self.staked_amount: Optional[float] = None - self.qtag_selected = False if self.datafeed is None else True - self.min_native_token_balance = min_native_token_balance - self.check_rewards: bool = check_rewards - self.web3 = self.endpoint.web3 - self.ignore_tbr = ignore_tbr - - self.gas_info: dict[str, Union[float, int]] = {} - logger.info(f"Reporting with account: {self.acct_addr}") - - self.account: ChainedAccount = account - assert self.acct_addr == to_checksum_address(self.account.address) - - async def in_dispute(self, new_stake_amount: Any) -> bool: - """Check if staker balance decreased""" - if self.staked_amount is not None and self.staked_amount > new_stake_amount: - return True - return False - - async def ensure_staked(self) -> Tuple[bool, ResponseStatus]: - """Make sure the current user is staked. - - Returns a bool signifying whether the current address is - staked. If the address is not initially, it attempts to deposit - the given stake amount.""" - staker_info, read_status = await self.oracle.read(func_name="getStakerInfo", _staker=self.acct_addr) - - if (not read_status.ok) or (staker_info is None): - msg = "Unable to read reporters staker info" - return False, error_status(msg, log=logger.info) - - ( - staker_startdate, - staker_balance, - locked_balance, - last_report, - num_reports, - ) = staker_info - - logger.info( - f""" - - STAKER INFO - start date: {staker_startdate} - desired stake: {self.stake} - amount staked: {staker_balance / 1e18} - locked balance: {locked_balance / 1e18} - last report: {last_report} - total reports: {num_reports} - """ - ) - - self.last_submission_timestamp = last_report - # check if staker balance has decreased after initial assignment - if await self.in_dispute(staker_balance): - msg = "Staked balance has decreased, account might be in dispute; restart telliot to keep reporting" - return False, error_status(msg) - # Attempt to stake - if staker_balance / 1e18 < self.stake: - logger.info("Current stake too low. Approving & depositing stake.") - - gas_price_gwei = await self.fetch_gas_price() - if gas_price_gwei is None: - return False, error_status("Unable to fetch gas price for staking", log=logger.info) - amount = int(self.stake * 1e18) - staker_balance - - _, write_status = await self.token.write( - func_name="approve", - gas_limit=100000, - legacy_gas_price=gas_price_gwei, - spender=self.oracle.address, - amount=amount, - ) - if not write_status.ok: - msg = "Unable to approve staking" - return False, error_status(msg, log=logger.error) - - _, write_status = await self.oracle.write( - func_name="depositStake", - gas_limit=300000, - legacy_gas_price=gas_price_gwei, - _amount=amount, - ) - if not write_status.ok: - msg = ( - "Unable to stake deposit: " - + write_status.error - + f"Make sure {self.acct_addr} has enough of the current chain's " - + "currency and the oracle's currency (TRB)" - ) # error won't be none # noqa: E501 - return False, error_status(msg, log=logger.error) - - logger.info(f"Staked {amount / 1e18} TRB") - self.staked_amount = self.stake - elif self.staked_amount is None: - self.staked_amount = staker_balance - - return True, ResponseStatus() - - async def check_reporter_lock(self) -> ResponseStatus: - """Ensure enough time has passed since last report. - - One stake is 10 TRB. Reporter lock is depends on the - total staked: - - reporter_lock = 12hrs / # stakes - - Returns bool signifying whether a given address is in a - reporter lock or not.""" - staker_info, read_status = await self.oracle.read(func_name="getStakerInfo", _staker=self.acct_addr) - - if (not read_status.ok) or (staker_info is None): - msg = "Unable to read reporters staker info" - return error_status(msg, log=logger.error) - - _, staker_balance, _, last_report, _ = staker_info - - if staker_balance < 10 * 1e18: - return error_status("Staker balance too low.", log=logger.info) - - self.last_submission_timestamp = last_report - logger.info(f"Last submission timestamp: {self.last_submission_timestamp}") - - trb = staker_balance / 1e18 - num_stakes = (trb - (trb % 10)) / 10 - reporter_lock = (12 / num_stakes) * 3600 - - time_remaining = round(self.last_submission_timestamp + reporter_lock - time.time()) - if time_remaining > 0: - hr_min_sec = str(timedelta(seconds=time_remaining)) - msg = "Currently in reporter lock. Time left: " + hr_min_sec - return error_status(msg, log=logger.info) - - return ResponseStatus() - - async def rewards(self) -> int: - tip = 0 - datafeed: DataFeed[Any] = self.datafeed # type: ignore - # Skip fetching datafeed & checking profitability - if self.expected_profit == "YOLO": - return tip - - single_tip, status = await self.autopay.get_current_tip(datafeed.query.query_id) - if not status.ok: - logger.warning("Unable to fetch single tip") - else: - tip += single_tip - - feed_tip = await get_feed_tip(datafeed.query.query_id, self.autopay) - if feed_tip is None: - logger.warning("Unable to fetch feed tip") - else: - tip += feed_tip - - return tip - - async def fetch_datafeed(self) -> Optional[DataFeed[Any]]: - """Fetches datafeed suggestion plus the reward amount from autopay if query tag isn't selected - if query tag is selected fetches the rewards, if any, for that query tag""" - if self.datafeed: - # add query id to catalog to fetch tip for legacy autopay - try: - qid = self.datafeed.query.query_id - except EncodingTypeError: - logger.warning(f"Unable to generate data/id for query: {self.datafeed.query}") - return None - if qid not in CATALOG_QUERY_IDS: - CATALOG_QUERY_IDS[qid] = self.datafeed.query.descriptor - self.autopaytip = await self.rewards() - return self.datafeed - - suggested_qtag, autopay_tip = await autopay_suggested_report(self.autopay) - if suggested_qtag: - self.autopaytip = autopay_tip - self.datafeed = CATALOG_FEEDS[suggested_qtag] - return self.datafeed - - if suggested_qtag is None: - suggested_qtag = await tellor_suggested_report(self.oracle) - if suggested_qtag is None: - logger.warning("Could not suggest query tag") - return None - elif suggested_qtag not in CATALOG_FEEDS: - logger.warning(f"Suggested query tag not in catalog: {suggested_qtag}") - return None - else: - self.datafeed = CATALOG_FEEDS[suggested_qtag] - self.autopaytip = await self.rewards() - return self.datafeed - return None - - async def ensure_profitable(self, datafeed: DataFeed[Any]) -> ResponseStatus: - - status = ResponseStatus() - if not self.check_rewards: - return status - - tip = self.autopaytip - # Fetch token prices in USD - native_token_feed = get_native_token_feed(self.chain_id) - price_feeds = [native_token_feed, trb_usd_median_feed] - _ = await asyncio.gather(*[feed.source.fetch_new_datapoint() for feed in price_feeds]) - price_native_token = native_token_feed.source.latest[0] - price_trb_usd = trb_usd_median_feed.source.latest[0] - - if price_native_token is None or price_trb_usd is None: - return error_status("Unable to fetch token price", log=logger.warning) - - if not self.gas_info: - return error_status("Gas info not set", log=logger.warning) - - gas_info = self.gas_info - - if gas_info["type"] == 0: - txn_fee = gas_info["gas_price"] * gas_info["gas_limit"] - logger.info( - f""" - - Tips: {tip/1e18} - Transaction fee: {self.web3.fromWei(txn_fee, 'gwei'):.09f} {tkn_symbol(self.chain_id)} - Gas price: {gas_info["gas_price"]} gwei - Gas limit: {gas_info["gas_limit"]} - Txn type: 0 (Legacy) - """ - ) - if gas_info["type"] == 2: - txn_fee = gas_info["max_fee"] * gas_info["gas_limit"] - logger.info( - f""" - - Tips: {tip/1e18} - Max transaction fee: {self.web3.fromWei(txn_fee, 'gwei'):.18f} {tkn_symbol(self.chain_id)} - Max fee per gas: {gas_info["max_fee"]} gwei - Max priority fee per gas: {gas_info["priority_fee"]} gwei - Gas limit: {gas_info["gas_limit"]} - Txn type: 2 (EIP-1559) - """ - ) - - # Calculate profit - rev_usd = tip / 1e18 * price_trb_usd - costs_usd = txn_fee / 1e9 * price_native_token # convert gwei costs to eth, then to usd - profit_usd = rev_usd - costs_usd - logger.info(f"Estimated profit: ${round(profit_usd, 2)}") - logger.info(f"tip price: {round(rev_usd, 2)}, gas costs: {costs_usd}") - - percent_profit = ((profit_usd) / costs_usd) * 100 - logger.info(f"Estimated percent profit: {round(percent_profit, 2)}%") - if (self.expected_profit != "YOLO") and ( - isinstance(self.expected_profit, float) and percent_profit < self.expected_profit - ): - status.ok = False - status.error = "Estimated profitability below threshold." - logger.info(status.error) - # reset datafeed for a new suggestion if qtag wasn't selected in cli - if self.qtag_selected is False: - self.datafeed = None - return status - # reset autopay tip to check for tips again - self.autopaytip = 0 - # reset datafeed for a new suggestion if qtag wasn't selected in cli - if self.qtag_selected is False: - self.datafeed = None - - return status diff --git a/src/telliot_feeds/reporters/types.py b/src/telliot_feeds/reporters/types.py new file mode 100644 index 00000000..43d7931e --- /dev/null +++ b/src/telliot_feeds/reporters/types.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass +from typing import Optional +from typing import TypedDict + +from web3.types import Wei + + +@dataclass +class StakerInfo: + """Data types for staker info + start_date: TimeStamp + stake_balance: int + locked_balance: int + reward_debt: int + last_report: TimeStamp + reports_count: int + gov_vote_count: int + vote_count: int + in_total_stakers: bool + """ + + start_date: int + stake_balance: int + locked_balance: int + reward_debt: int + last_report: int + reports_count: int + gov_vote_count: int + vote_count: int + in_total_stakers: bool + + +class GasParams(TypedDict): + maxPriorityFeePerGas: Optional[Wei] + maxFeePerGas: Optional[Wei] + gasPrice: Optional[Wei] # Legacy gas price + gas: Optional[int] # Gas limit diff --git a/src/telliot_feeds/sources/sweth_source.py b/src/telliot_feeds/sources/sweth_source.py index 77340641..194446c8 100644 --- a/src/telliot_feeds/sources/sweth_source.py +++ b/src/telliot_feeds/sources/sweth_source.py @@ -7,6 +7,7 @@ from telliot_feeds.dtypes.datapoint import datetime_now_utc from telliot_feeds.dtypes.datapoint import OptionalDataPoint +from telliot_feeds.feeds.eth_usd_feed import eth_usd_median_feed from telliot_feeds.pricing.price_service import WebPriceService from telliot_feeds.pricing.price_source import PriceSource from telliot_feeds.utils.log import get_logger @@ -67,7 +68,6 @@ async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float] if sweth_eth_ratio is None: logger.error("Unable to get sweth_eth_ratio") return None, None - from telliot_feeds.feeds.eth_usd_feed import eth_usd_median_feed source = eth_usd_median_feed.source @@ -80,6 +80,8 @@ async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float] @dataclass class swETHSpotPriceSource(PriceSource): + """Gets data from swETH contract""" + asset: str = "" currency: str = "" service: swETHSpotPriceService = field(default_factory=swETHSpotPriceService, init=False) @@ -91,6 +93,8 @@ def __post_init__(self) -> None: @dataclass class swETHMaverickSpotPriceSource(PriceSource): + """Gets data from Maverick AMM""" + asset: str = "" currency: str = "" service: swETHSpotPriceService = field(default_factory=swETHSpotPriceService) diff --git a/src/telliot_feeds/utils/reporter_utils.py b/src/telliot_feeds/utils/reporter_utils.py index a60e3a88..1f6816c8 100644 --- a/src/telliot_feeds/utils/reporter_utils.py +++ b/src/telliot_feeds/utils/reporter_utils.py @@ -197,7 +197,7 @@ def tkn_symbol(chain_id: int) -> str: return "Unknown native token" -def fee_history_priority_fee_estimate(fee_history: FeeHistory, priority_fee_max: int) -> int: +def fee_history_priority_fee_estimate(fee_history: FeeHistory, priority_fee_max: Wei) -> Wei: """Estimate priority fee based on a percentile of the fee history. Adapted from web3.py fee_utils.py @@ -209,7 +209,7 @@ def fee_history_priority_fee_estimate(fee_history: FeeHistory, priority_fee_max: Returns: Estimated priority fee in wei """ - priority_fee_min = 1_000_000_000 # 1 gwei + priority_fee_min = Wei(1_000_000_000) # 1 gwei # grab only non-zero fees and average against only that list non_empty_block_fees = [fee[0] for fee in fee_history["reward"] if fee[0] != 0] diff --git a/src/telliot_feeds/utils/stake_info.py b/src/telliot_feeds/utils/stake_info.py new file mode 100644 index 00000000..539c39ba --- /dev/null +++ b/src/telliot_feeds/utils/stake_info.py @@ -0,0 +1,101 @@ +from collections import deque +from dataclasses import dataclass +from dataclasses import field +from typing import Deque +from typing import Optional + +from telliot_feeds.utils.log import get_logger + +logger = get_logger(__name__) + + +@dataclass +class StakeInfo: + """Check if a datafeed is in dispute + by tracking staker balance flucutations. + """ + + max_data: int = 2 + last_report: int = 0 + reports_count: int = 0 + + stake_amount_history: Deque[int] = field(default_factory=deque, init=False, repr=False) + staker_balance_history: Deque[int] = field(default_factory=deque, init=False, repr=False) + + def __post_init__(self) -> None: + self.stake_amount_history = deque(maxlen=self.max_data) + self.staker_balance_history = deque(maxlen=self.max_data) + + def store_stake_amount(self, stake_amount: int) -> None: + """Add stake amount to deque and maintain a history of 2""" + self.stake_amount_history.append(stake_amount) + + def store_staker_balance(self, stake_balance: int) -> None: + """Add staker info to deque and maintain a history of 2""" + self.staker_balance_history.append(stake_balance) + + def update_last_report_time(self, last_report: int) -> None: + """Update last report time""" + self.last_report = last_report + + def update_reports_count(self, reports_count: int) -> None: + """Update report count""" + self.reports_count = reports_count + + @property + def last_report_time(self) -> int: + """Return report count""" + return self.last_report + + def is_in_dispute(self) -> bool: + """Check if staker has been disputed""" + if len(self.staker_balance_history) == self.max_data: + if self.staker_balance_history[-1] < self.staker_balance_history[-2]: + logger.warning("Your staked balance has decreased, account might be in dispute") + return True + return False + + def stake_amount_change(self) -> bool: + """Check oracle stake amount change""" + logger.info(f"Current Oracle stakeAmount: {self.stake_amount_history[-1] / 1e18!r}") + if len(self.stake_amount_history) == self.max_data: + if self.stake_amount_history[-1] < self.stake_amount_history[-2]: + logger.info("Oracle stake amount has decreased") + if self.stake_amount_history[-1] > self.stake_amount_history[-2]: + logger.info("Oracle stake amount has increased") + return True + return False + + @property + def stake_amount_gt_staker_balance(self) -> bool: + """Compare staker balance and oracle stake amount""" + if not self.stake_amount_history or not self.staker_balance_history: + logger.debug("Not enough data to compare stake amount and staker balance") + return False + if self.stake_amount_history[-1] > self.staker_balance_history[-1]: + logger.info("Staker balance is less than oracle stake amount") + return True + return False + + @property + def current_stake_amount(self) -> int: + """Return the current stake amount""" + if self.stake_amount_history: + return self.stake_amount_history[-1] + else: + return 0 + + @property + def current_staker_balance(self) -> Optional[int]: + """Return the current staker balance""" + if self.staker_balance_history: + return self.staker_balance_history[-1] + else: + return None + + def update_staker_balance(self, amount: int) -> None: + """Update staker balance""" + balance = self.current_staker_balance + if balance is not None: + new_amount = balance + amount + self.store_staker_balance(new_amount) diff --git a/tests/cli/test_stake_e2e_cmd.py b/tests/cli/test_stake_e2e_cmd.py new file mode 100644 index 00000000..2bf7d3c5 --- /dev/null +++ b/tests/cli/test_stake_e2e_cmd.py @@ -0,0 +1,29 @@ +from click.testing import CliRunner + +from telliot_feeds.cli.main import main as cli_main + + +def test_request_withdraw_cmd(): + """Test request withdraw stake command.""" + runner = CliRunner() + result = runner.invoke(cli_main, ["request-withdraw", "-a", "git-tellorflex-test-key"]) + assert "Missing option '--amount' / '-amt'" in result.stdout + assert result.exception + assert result.exit_code == 2 + + +def test_withdraw_cmd(): + """Test withdraw stake command.""" + runner = CliRunner() + result = runner.invoke(cli_main, ["withdraw"]) + assert result.exception + assert result.exit_code == 2 + + +def test_staking_cmd(): + """Test stake command.""" + runner = CliRunner() + result = runner.invoke(cli_main, ["stake", "-a", "git-tellorflex-test-key"]) + assert "Missing option '--amount' / '-amt'" in result.stdout + assert result.exception + assert result.exit_code == 2 diff --git a/tests/cli/test_utils.py b/tests/cli/test_utils.py index 5743339e..ae63787a 100644 --- a/tests/cli/test_utils.py +++ b/tests/cli/test_utils.py @@ -4,9 +4,11 @@ from hexbytes import HexBytes from telliot_feeds.cli.utils import build_query +from telliot_feeds.cli.utils import call_oracle from telliot_feeds.cli.utils import CustomHexBytes from telliot_feeds.queries.abi_query import AbiQuery from telliot_feeds.queries.price.spot_price import SpotPrice +from telliot_feeds.reporters.stake import Stake def test_build_query(): @@ -24,6 +26,64 @@ def test_build_query(): assert query.currency == "usd" +@pytest.mark.asyncio +async def test_call_oracle(tellor_360, caplog, chain): + """Test calling the oracle.""" + user_inputs = { + "min_native_token_balance": 0.0, + "gas_limit": 350000, + "base_fee_per_gas": None, + "priority_fee_per_gas": None, + "max_fee_per_gas": None, + "legacy_gas_price": 100, + "transaction_type": 0, + "gas_multiplier": None, + } + + contracts, account = tellor_360 + s = Stake( + oracle=contracts.oracle, + token=contracts.token, + endpoint=contracts.oracle.node, + account=account, + **user_inputs, + ) + + _ = await s.deposit_stake(int(1 * 1e18)) + + class ctx: + def __init__(self): + self.obj = {"CHAIN_ID": 80001, "ACCOUNT_NAME": "brownie-acct", "TEST_CONFIG": None} + + user_inputs["password"] = "" + with mock.patch("telliot_core.apps.core.TelliotCore.get_tellor360_contracts", return_value=contracts): + with mock.patch("click.confirm", return_value="y"): + await call_oracle( + ctx=ctx(), + func="requestStakingWithdraw", + user_inputs=user_inputs, + _amount=1, + ) + assert "requestStakingWithdraw transaction succeeded" in caplog.text + user_inputs["password"] = "" + user_inputs["min_native_token_balance"] = 0.0 + await call_oracle( + ctx=ctx(), + func="withdrawStake", + user_inputs=user_inputs, + ) + assert "revert 7 days didn't pass" in caplog.text + chain.sleep(604800) + user_inputs["password"] = "" + user_inputs["min_native_token_balance"] = 0.0 + await call_oracle( + ctx=ctx(), + func="withdrawStake", + user_inputs=user_inputs, + ) + assert "withdrawStake transaction succeeded" in caplog.text + + def test_custom_hexbytes_wrapper(): """Test custom hexbytes wrapper.""" # test when 0x is present and not present diff --git a/tests/conftest.py b/tests/conftest.py index e5c50992..020b50f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,8 +12,6 @@ from brownie import QueryDataStorage from brownie import TellorFlex from brownie import TellorPlayground -from brownie import TellorXMasterMock -from brownie import TellorXOracleMock from chained_accounts import ChainedAccount from chained_accounts import find_accounts from hexbytes import HexBytes @@ -27,7 +25,7 @@ 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.reporters.tellor_flex import TellorFlexReporter +from telliot_feeds.reporters.tellor_360 import Tellor360Reporter @pytest.fixture(scope="module", autouse=True) @@ -212,16 +210,6 @@ def query_data_storage_contract(): return accounts[0].deploy(QueryDataStorage) -@pytest.fixture -def tellorx_oracle_mock_contract(): - return accounts[0].deploy(TellorXOracleMock) - - -@pytest.fixture -def tellorx_master_mock_contract(): - return accounts[0].deploy(TellorXMasterMock) - - @pytest.fixture(autouse=True) def multicall_contract(): # deploy multicall contract to brownie chain and add chain id to multicall module @@ -266,7 +254,7 @@ async def tellor_flex_reporter(mumbai_test_cfg, mock_flex_contract, mock_autopay account = core.get_account() - flex = core.get_tellorflex_contracts() + flex = core.get_tellor360_contracts() flex.oracle.address = mock_flex_contract.address flex.autopay.address = mock_autopay_contract.address flex.token.address = mock_token_contract.address @@ -274,9 +262,9 @@ async def tellor_flex_reporter(mumbai_test_cfg, mock_flex_contract, mock_autopay flex.oracle.connect() flex.token.connect() flex.autopay.connect() - flex = core.get_tellorflex_contracts() + flex = core.get_tellor360_contracts() - r = TellorFlexReporter( + r = Tellor360Reporter( oracle=flex.oracle, token=flex.token, autopay=flex.autopay, diff --git a/tests/reporters/test_360_reporter.py b/tests/reporters/test_360_reporter.py index a99318f3..7add7b85 100644 --- a/tests/reporters/test_360_reporter.py +++ b/tests/reporters/test_360_reporter.py @@ -8,14 +8,22 @@ import pytest from brownie import accounts from brownie import chain +from telliot_core.utils.response import error_status +from telliot_core.utils.response import ResponseStatus +from telliot_feeds.datafeed import DataFeed +from telliot_feeds.feeds import CATALOG_FEEDS +from telliot_feeds.feeds.btc_usd_feed import btc_usd_median_feed from telliot_feeds.feeds.eth_usd_feed import eth_usd_median_feed from telliot_feeds.feeds.matic_usd_feed import matic_usd_median_feed from telliot_feeds.feeds.snapshot_feed import snapshot_manual_feed from telliot_feeds.feeds.trb_usd_feed import trb_usd_median_feed from telliot_feeds.reporters.rewards.time_based_rewards import get_time_based_rewards from telliot_feeds.reporters.tellor_360 import Tellor360Reporter +from telliot_feeds.utils.log import get_logger +from tests.utils.utils import passing_bool_w_status +logger = get_logger(__name__) txn_kwargs = {"gas_limit": 3500000, "legacy_gas_price": 1} CHAIN_ID = 80001 @@ -43,9 +51,9 @@ async def test_report(tellor_360, caplog, guaranteed_price_source, mock_flex_con ) await r.report_once() - assert r.staker_info.stake_balance == int(10e18) + assert r.stake_info.current_staker_balance == int(10e18) # report count before first submission - assert r.staker_info.reports_count == 0 + assert "reports count: 0" in caplog.text # update stakeamount increase causes reporter to deposit more to keep reporting mock_token_contract.faucet(accounts[0].address) @@ -66,10 +74,10 @@ async def test_report(tellor_360, caplog, guaranteed_price_source, mock_flex_con await r.report_once() # staker balance increased due to updateStakeAmount call - assert r.staker_info.stake_balance == stake_amount + assert r.stake_info.current_stake_amount == stake_amount assert "Currently in reporter lock. Time left: 11:59" in caplog.text # 12hr # report count before second report - assert r.staker_info.reports_count == 1 + assert "reports count: 1" in caplog.text # decrease stakeAmount should increase reporting frequency mock_token_contract.approve(mock_flex_contract.address, mock_flex_contract.stakeAmount()) mock_flex_contract.depositStake(mock_flex_contract.stakeAmount()) @@ -86,7 +94,7 @@ async def test_report(tellor_360, caplog, guaranteed_price_source, mock_flex_con assert status.ok assert stake_amount == int(10e18) - assert r.staker_info.stake_balance == int(20e18) + assert r.stake_info.current_staker_balance == int(20e18) await r.report_once() assert "Currently in reporter lock. Time left: 5:59" in caplog.text # 6hr @@ -163,9 +171,11 @@ async def test_360_reporter_rewards(tellor_360, guaranteed_price_source): min_native_token_balance=0, ignore_tbr=False, datafeed=feed, + expected_profit="YOLO", ) assert isinstance(await r.rewards(), int) + await r.report_once() @pytest.mark.asyncio @@ -200,14 +210,14 @@ async def test_adding_stake(tellor_360, guaranteed_price_source): # first should deposits default stake _, status = await reporter.report_once() assert status.ok - assert reporter.staker_info.stake_balance == int(10e18), "Staker balance should be 10e18" + assert reporter.stake_info.current_staker_balance == int(10e18), "Staker balance should be 10e18" # stake more by by changing stake from default similar to how a stake amount chosen in cli # high stake to bypass reporter lock reporter = Tellor360Reporter(**reporter_kwargs, stake=900000) _, status = await reporter.report_once() assert status.ok - assert reporter.staker_info.stake_balance == pytest.approx(900000e18), "Staker balance should be 90000e18" + assert reporter.stake_info.current_staker_balance == pytest.approx(900000e18), "Staker balance should be 90000e18" @pytest.mark.asyncio @@ -274,8 +284,10 @@ async def mock_get_feed_and_tip(*args, **kwargs): # set datafeed to None so fetch_datafeed will call get_feed_and_tip reporter.datafeed = None await reporter.report(report_count=1) - reporter_lock = 43200 / math.floor(reporter.staker_info.stake_balance / reporter.stake_amount) - time_remaining = round(reporter.staker_info.last_report + reporter_lock - time.time()) + reporter_lock = 43200 / math.floor( + reporter.stake_info.current_staker_balance / reporter.stake_info.current_stake_amount + ) + time_remaining = round(reporter.stake_info.last_report_time + reporter_lock - time.time()) if time_remaining > 0: hr_min_sec = str(datetime.timedelta(seconds=time_remaining)) assert f"Currently in reporter lock. Time left: {hr_min_sec}" in caplog.text @@ -370,3 +382,208 @@ def mock_tbr(): # tip amount should not increase assert "Tips: 2.0" not in caplog.text assert "Tips: 3.0" not in caplog.text + + +@pytest.mark.asyncio +async def test_fetch_datafeed(tellor_flex_reporter): + r = tellor_flex_reporter + r.use_random_feeds = True + feed = await r.fetch_datafeed() + assert isinstance(feed, DataFeed) + + r.datafeed = None + assert r.datafeed is None + feed = await r.fetch_datafeed() + assert isinstance(feed, DataFeed) + + +@pytest.mark.skip(reason="EIP-1559 not supported by ganache") +@pytest.mark.asyncio +def test_get_fee_info(tellor_flex_reporter): + """Test fee info for type 2 transactions.""" + tellor_flex_reporter.transaction_type = 2 + tellor_flex_reporter.update_gas_fees() + gas_fees = tellor_flex_reporter.get_gas_info() + + assert isinstance(gas_fees["maxPriorityFeePerGas"], int) + assert isinstance(gas_fees["maxFeePerGas"], int) + + +@pytest.mark.asyncio +async def test_get_num_reports_by_id(tellor_flex_reporter): + r = tellor_flex_reporter + num, status = await r.get_num_reports_by_id(matic_usd_median_feed.query.query_id) + + assert status.ok + assert isinstance(num, int) + + +@pytest.mark.asyncio +async def test_ensure_staked(tellor_flex_reporter): + """Test staking status of reporter.""" + staked, status = await tellor_flex_reporter.ensure_staked() + + assert staked + assert status.ok + + +@pytest.mark.asyncio +async def test_ensure_profitable(tellor_flex_reporter): + """Test profitability check.""" + r = tellor_flex_reporter + r.gas_info = {"maxPriorityFeePerGas": None, "maxFeePerGas": None, "gasPrice": 1e9, "gas": 300000} + + assert r.expected_profit == "YOLO" + + status = await r.ensure_profitable() + + assert status.ok + + r.expected_profit = 1e10 + status = await r.ensure_profitable() + + assert not status.ok + assert status.error == "Estimated profitability below threshold." + + +@pytest.mark.asyncio +async def test_ethgasstation_error(tellor_flex_reporter): + with mock.patch("telliot_feeds.reporters.tellor_360.Tellor360Reporter.update_gas_fees") as func: + func.return_value = error_status("failed", log=logger.error) + r = tellor_flex_reporter + + staked, status = await r.ensure_staked() + assert not staked + assert not status.ok + + +@pytest.mark.asyncio +async def test_no_updated_value(tellor_flex_reporter, bad_datasource): + """Test handling for no updated value returned from datasource.""" + r = tellor_flex_reporter + r.datafeed = btc_usd_median_feed + + # Clear latest datapoint + r.datafeed.source._history.clear() + + # Replace PriceAggregator's sources with test source that + # returns no updated DataPoint + r.datafeed.source.sources = [bad_datasource] + + tx_receipt, status = await r.report_once() + + assert not tx_receipt + assert not status.ok + assert status.error == "Unable to retrieve updated datafeed value." + + +@pytest.mark.asyncio +async def test_ensure_reporter_lock_check_after_submitval_attempt( + tellor_flex_reporter, guaranteed_price_source, caplog +): + r = tellor_flex_reporter + + async def check_reporter_lock(*args, **kwargs): + logger.debug(f"Checking reporter lock: {time.time()}") + return ResponseStatus() + + r.ensure_staked = passing_bool_w_status + r.check_reporter_lock = check_reporter_lock + r.datafeed = matic_usd_median_feed + r.gas_limit = 350000 + + # Simulate fetching latest value + r.datafeed.source.sources = [guaranteed_price_source] + + def send_failure(*args, **kwargs): + raise Exception("bingo") + + with mock.patch("web3.eth.Eth.send_raw_transaction", side_effect=send_failure): + r.wait_period = 0 + await r.report(2) + assert "Send transaction failed: Exception('bingo')" in caplog.text + assert caplog.text.count("Checking reporter lock") == 2 + + +@pytest.mark.asyncio +async def test_check_reporter_lock(tellor_flex_reporter): + status = await tellor_flex_reporter.check_reporter_lock() + + assert isinstance(status, ResponseStatus) + if not status.ok: + assert ("reporter lock" in status.error) or ("Staker balance too low" in status.error) + + +@pytest.mark.asyncio +async def test_reporting_without_internet(tellor_flex_reporter, caplog): + async def offline(): + return False + + with patch("asyncio.sleep", side_effect=InterruptedError): + r = tellor_flex_reporter + r.is_online = lambda: offline() + with pytest.raises(InterruptedError): + await r.report() + assert "Unable to connect to the internet!" in caplog.text + + +@pytest.mark.asyncio +async def test_dispute(tellor_flex_reporter, caplog): + # Test when reporter in dispute + r = tellor_flex_reporter + r.datafeed = matic_usd_median_feed + # initial balance higher than current balance, current balance is 0 since first time staking + r.stake_info.store_staker_balance(1) + + _ = await r.report_once() + assert "Your staked balance has decreased, account might be in dispute" in caplog.text + + +@pytest.mark.asyncio +async def test_reset_datafeed(tellor_flex_reporter): + # Test when reporter selects qtag vs not + # datafeed should persist if qtag selected + r = tellor_flex_reporter + + reporter1 = Tellor360Reporter( + oracle=r.oracle, + token=r.token, + autopay=r.autopay, + endpoint=r.endpoint, + account=r.account, + chain_id=80001, + transaction_type=0, + datafeed=CATALOG_FEEDS["trb-usd-spot"], + min_native_token_balance=0, + ) + reporter2 = Tellor360Reporter( + oracle=r.oracle, + token=r.token, + autopay=r.autopay, + endpoint=r.endpoint, + account=r.account, + chain_id=80001, + transaction_type=0, + min_native_token_balance=0, + ) + + # Unlocker reporter lock checker + async def reporter_lock(): + return ResponseStatus() + + reporter1.check_reporter_lock = lambda: reporter_lock() + reporter2.check_reporter_lock = lambda: reporter_lock() + + async def reprt(): + for _ in range(3): + await reporter1.report_once() + assert reporter1.qtag_selected is True + assert reporter1.datafeed.query.asset == "trb" + chain.sleep(43201) + + for _ in range(3): + await reporter2.report_once() + assert reporter2.qtag_selected is False + chain.sleep(43201) + + _ = await reprt() diff --git a/tests/reporters/test_autopay_multicalls.py b/tests/reporters/test_autopay_multicalls.py deleted file mode 100644 index 27715f5f..00000000 --- a/tests/reporters/test_autopay_multicalls.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Test multicall success outcomes requirement True and False""" -from unittest.mock import patch - -import pytest -from multicall import Call -from multicall import Multicall -from telliot_core.apps.core import TelliotCore -from web3.exceptions import ContractLogicError - -from telliot_feeds.reporters import reporter_autopay_utils -from telliot_feeds.reporters.reporter_autopay_utils import autopay_suggested_report -from telliot_feeds.reporters.reporter_autopay_utils import AutopayCalls -from telliot_feeds.reporters.reporter_autopay_utils import safe_multicall - - -def fake_call(calls: AutopayCalls): - """helper function returning consistent fake multicall""" - return [ - Call( - calls.autopay.address, - ["fakeFunction(bytes32)(uint256)", b""], - [["fake_key", None]], - ) - ] - - -@pytest.fixture(scope="function") -async def setup_autopay_call(mumbai_test_cfg, mock_autopay_contract) -> AutopayCalls: - async with TelliotCore(config=mumbai_test_cfg) as core: - - flex = core.get_tellorflex_contracts() - flex.autopay.address = mock_autopay_contract.address - flex.autopay.connect() - calls = AutopayCalls(flex.autopay) - return calls - - -@pytest.mark.asyncio -async def test_get_current_tips(setup_autopay_call): - """Test Multicall by calling getCurrentTip in autopay""" - calls: AutopayCalls = await setup_autopay_call - tips = await calls.get_current_tip(require_success=False) - assert tips["eth-usd-spot"] == 0 - - -@pytest.mark.asyncio -async def test_get_current_feeds(caplog, setup_autopay_call): - """Test getCurrentFeeds call in autopay using multicall""" - calls: AutopayCalls = await setup_autopay_call - # test proper function for success outcomes - boolean = [True, False] - for i in boolean: - tips = await calls.get_current_feeds(require_success=i) - assert tips["eth-usd-spot"] == () - assert tips[("eth-usd-spot", "current_time")] == 0 - assert tips[("eth-usd-spot", "three_mos_ago")] == 0 - - async def fake_function(require_success=True): - """fake function signature call that doesn't exist in autopay - should revert as a ContractLogicError""" - - return await safe_multicall(calls=fake_call(calls), endpoint=calls.w3, require_success=require_success) - - calls.get_current_feeds = fake_function - - tips = await calls.get_current_feeds(require_success=False) - assert tips["fake_key"] is None - - true_result = await calls.get_current_feeds(require_success=True) - assert true_result is None - assert "Contract reversion in multicall request" in caplog.text - - -@pytest.mark.asyncio -async def test_safe_multicall(caplog, setup_autopay_call): - """Test safe multicall with error handling""" - calls: AutopayCalls = await setup_autopay_call - - def raise_unsupported_block_num(): - raise ValueError({"code": -32000, "message": "unsupported block number 22071635"}) - - def raise_unexpected_rpc_error(): - raise ValueError({"code": -32000, "message": "unexpected rpc error nooooo"}) - - def raise_contract_reversion(): - raise ContractLogicError - - with patch.object(Multicall, "coroutine", side_effect=raise_unsupported_block_num): - - res = await safe_multicall(fake_call(calls), endpoint=calls.w3, require_success=True) - - assert res is None - assert "ValueError" in caplog.text - - with patch.object(Multicall, "coroutine", side_effect=raise_unexpected_rpc_error): - - res = await safe_multicall(fake_call(calls), endpoint=calls.w3, require_success=True) - - assert res is None - assert "ValueError" in caplog.text - - with patch.object(Multicall, "coroutine", side_effect=raise_contract_reversion): - - res = await safe_multicall(fake_call(calls), endpoint=calls.w3, require_success=True) - - assert res is None - assert "Contract reversion in multicall request" in caplog.text - - -@pytest.mark.asyncio -async def test_index_error(setup_autopay_call, caplog): - bad_response = { - "ric-usd-spot": (), - "disregard_boolean": None, - ("ric-usd-spot", "current_time"): None, - ("ric-usd-spot", "three_mos_ago"): None, - } - call: AutopayCalls = await setup_autopay_call - - async def fake_func(): - return bad_response - - call.get_current_feeds = fake_func - res = await call.reward_claim_status() - assert res == (None, None, None) - assert "No feeds balance to check" - - -@pytest.mark.asyncio -async def test_multicall_return_none(setup_autopay_call): - calls: AutopayCalls = await setup_autopay_call - - async def none(x, y, z): - return None - - reporter_autopay_utils.safe_multicall = none - resp = await calls.get_current_feeds() - assert resp is None - resp = await autopay_suggested_report(calls.autopay) - assert resp == (None, None) diff --git a/tests/reporters/test_backup_reporter.py b/tests/reporters/test_backup_reporter.py index 82f28b2c..f9d6c86f 100644 --- a/tests/reporters/test_backup_reporter.py +++ b/tests/reporters/test_backup_reporter.py @@ -12,7 +12,7 @@ @pytest.fixture(scope="function") -async def reporter(tellor_360, guaranteed_price_source, mock_flex_contract, mock_token_contract): +async def reporter(tellor_360, guaranteed_price_source): contracts, account = tellor_360 feed = eth_usd_median_feed feed.source = guaranteed_price_source diff --git a/tests/reporters/test_custom_360_reporter.py b/tests/reporters/test_custom_360_reporter.py index 6852d6ca..3619dbe9 100644 --- a/tests/reporters/test_custom_360_reporter.py +++ b/tests/reporters/test_custom_360_reporter.py @@ -22,8 +22,9 @@ logger = get_logger(__name__) - +# adding account to brownie accounts account_fake = accounts.add("023861e2ceee1ea600e43cbd203e9e01ea2ed059ee3326155453a1ed3b1113a9") +# adding account to telliot accounts try: account = find_accounts(name="fake_flex_custom_reporter_address", chain_id=80001)[0] except IndexError: @@ -86,7 +87,7 @@ def mock_confirm(*args, **kwargs): r = Tellor360Reporter( transaction_type=0, oracle=custom_contract, - token=contracts.token, + token=custom_contract, autopay=contracts.autopay, endpoint=core.endpoint, account=account, @@ -107,9 +108,7 @@ def mock_confirm(*args, **kwargs): ) # send eth from brownie address to reporter address for txn fees accounts[1].transfer(account.address, "10 ether") - - mock_token_contract.approve(mock_flex_contract.address, 10e18, {"from": account_fake}) - mock_flex_contract.depositStake(10e18, {"from": account_fake}) + accounts[1].transfer(mock_reporter_contract.address, "10 ether") return r diff --git a/tests/reporters/test_flex_reporter.py b/tests/reporters/test_flex_reporter.py deleted file mode 100644 index 1c1af1b5..00000000 --- a/tests/reporters/test_flex_reporter.py +++ /dev/null @@ -1,178 +0,0 @@ -from unittest.mock import patch - -import pytest -from brownie import chain -from telliot_core.utils.response import ResponseStatus - -from telliot_feeds.datafeed import DataFeed -from telliot_feeds.feeds import CATALOG_FEEDS -from telliot_feeds.feeds.matic_usd_feed import matic_usd_median_feed -from telliot_feeds.reporters.tellor_flex import TellorFlexReporter - - -@pytest.mark.asyncio -async def test_YOLO_feed_suggestion(tellor_flex_reporter): - tellor_flex_reporter.expected_profit = "YOLO" - feed = await tellor_flex_reporter.fetch_datafeed() - - assert feed is not None - assert isinstance(feed, DataFeed) - - -@pytest.mark.asyncio -async def test_ensure_profitable(tellor_flex_reporter): - r = tellor_flex_reporter - r.expected_profit = "YOLO" - r.gas_info = {"type": 0, "gas_price": 1e9, "gas_limit": 300000} - unused_feed = matic_usd_median_feed - status = await r.ensure_profitable(unused_feed) - - assert isinstance(status, ResponseStatus) - assert status.ok - - r.chain_id = 1 - r.expected_profit = 100.0 - status = await r.ensure_profitable(unused_feed) - - assert not status.ok - - -@pytest.mark.asyncio -async def test_fetch_gas_price(tellor_flex_reporter): - price = await tellor_flex_reporter.fetch_gas_price() - - assert isinstance(price, float) - assert price > 0 - - -@pytest.mark.asyncio -async def test_ensure_staked(tellor_flex_reporter): - staked, status = await tellor_flex_reporter.ensure_staked() - - assert isinstance(status, ResponseStatus) - assert isinstance(staked, bool) - if status.ok: - assert staked - else: - assert "Unable to approve staking" in status.error - - -@pytest.mark.asyncio -async def test_check_reporter_lock(tellor_flex_reporter): - status = await tellor_flex_reporter.check_reporter_lock() - - assert isinstance(status, ResponseStatus) - if not status.ok: - assert ("reporter lock" in status.error) or ("Staker balance too low" in status.error) - - -@pytest.mark.asyncio -async def test_get_num_reports_by_id(tellor_flex_reporter): - qid = matic_usd_median_feed.query.query_id - count, status = await tellor_flex_reporter.get_num_reports_by_id(qid) - - assert isinstance(status, ResponseStatus) - if status.ok: - assert isinstance(count, int) - else: - assert count is None - - -@pytest.mark.asyncio -async def test_fetch_gas_price_error(tellor_flex_reporter, caplog): - # Test invalid gas price speed - r = tellor_flex_reporter - - with patch("telliot_feeds.reporters.tellor_flex.TellorFlexReporter.fetch_gas_price") as func: - func.return_value = None - - r.stake = 1e100 - staked, status = await r.ensure_staked() - assert not staked - assert not status.ok - assert "Unable to fetch gas price for staking" in status.error - _, status = await r.report_once() - assert not status.ok - assert "Unable to fetch gas price" in status.error - - -@pytest.mark.asyncio -async def test_reporting_without_internet(tellor_flex_reporter, caplog): - async def offline(): - return False - - with patch("asyncio.sleep", side_effect=InterruptedError): - - r = tellor_flex_reporter - - r.is_online = lambda: offline() - - with pytest.raises(InterruptedError): - await r.report() - - assert "Unable to connect to the internet!" in caplog.text - - -@pytest.mark.asyncio -async def test_dispute(tellor_flex_reporter: TellorFlexReporter): - # Test when reporter in dispute - r = tellor_flex_reporter - - async def in_dispute(_): - return True - - r.in_dispute = in_dispute - _, status = await r.report_once() - assert ( - "Staked balance has decreased, account might be in dispute; restart telliot to keep reporting" in status.error - ) - - -@pytest.mark.asyncio -async def test_reset_datafeed(tellor_flex_reporter): - # Test when reporter selects qtag vs not - # datafeed should persist if qtag selected - r: TellorFlexReporter = tellor_flex_reporter - - reporter1 = TellorFlexReporter( - oracle=r.oracle, - token=r.token, - autopay=r.autopay, - endpoint=r.endpoint, - account=r.account, - chain_id=80001, - transaction_type=0, - datafeed=CATALOG_FEEDS["trb-usd-spot"], - min_native_token_balance=0, - ) - reporter2 = TellorFlexReporter( - oracle=r.oracle, - token=r.token, - autopay=r.autopay, - endpoint=r.endpoint, - account=r.account, - chain_id=80001, - transaction_type=0, - min_native_token_balance=0, - ) - - # Unlocker reporter lock checker - async def reporter_lock(): - return ResponseStatus() - - reporter1.check_reporter_lock = lambda: reporter_lock() - reporter2.check_reporter_lock = lambda: reporter_lock() - - async def reprt(): - for _ in range(3): - await reporter1.report_once() - assert reporter1.qtag_selected is True - assert reporter1.datafeed.query.asset == "trb" - chain.sleep(43201) - - for _ in range(3): - await reporter2.report_once() - assert reporter2.qtag_selected is False - chain.sleep(43201) - - _ = await reprt() diff --git a/tests/reporters/test_gas_fees.py b/tests/reporters/test_gas_fees.py new file mode 100644 index 00000000..dca8a3ef --- /dev/null +++ b/tests/reporters/test_gas_fees.py @@ -0,0 +1,174 @@ +from unittest.mock import Mock +from unittest.mock import PropertyMock + +import pytest +from telliot_core.apps.core import TelliotCore +from web3.datastructures import AttributeDict + +from telliot_feeds.reporters.gas import GasFees + + +@pytest.fixture(scope="function") +async def gas_fees_object(mumbai_test_cfg): + """Fixture that build GasFees object.""" + async with TelliotCore(config=mumbai_test_cfg) as core: + # get PubKey and PrivKey from config files + account = core.get_account() + + gas = GasFees( + endpoint=core.endpoint, + account=account, + transaction_type=0, + legacy_gas_price=None, + gas_multiplier=1, + priority_fee_per_gas=None, + base_fee_per_gas=None, + max_fee_per_gas=None, + gas_limit=None, + ) + return gas + + +@pytest.mark.asyncio +async def test_get_legacy_gas_price(gas_fees_object): + gas: GasFees = await gas_fees_object + + legacy_gas_price, status = gas.get_legacy_gas_price() + assert status.ok + assert "gasPrice" in legacy_gas_price + assert legacy_gas_price["gasPrice"] is not None, "get_legacy_gas_price returned None" + assert legacy_gas_price["gasPrice"] > 0, "legacy gas price not fetched properly" + gas.web3 = Mock() + type(gas.web3.eth).gas_price = PropertyMock(return_value=None) + legacy_gas_price, status = gas.get_legacy_gas_price() + assert not status.ok + assert legacy_gas_price is None + assert status.error == "Error fetching legacy gas price, rpc returned None" + + type(gas.web3.eth).gas_price = PropertyMock(side_effect=ValueError("Mock Error")) + legacy_gas_price, status = gas.get_legacy_gas_price() + assert not status.ok + assert legacy_gas_price is None + assert status.error == "Error fetching legacy gas price: ValueError('Mock Error')" + + +@pytest.mark.asyncio +async def test_get_eip1559_gas_price(gas_fees_object): + gas: GasFees = await gas_fees_object + mock_fee_history = AttributeDict( + { + "baseFeePerGas": [13676801331, 14273862890, 13972887310, 14813623596, 14046654284, 13615655875], + "reward": [ + [100000000, 100000000, 1500000000], + [100000000, 100000000, 1500000000], + [100000000, 1000000000, 3000000000], + [100000000, 528000000, 2000000000], + [100000000, 103445451, 1650000000], + ], + } + ) + + type(gas.web3.eth).fee_history = Mock(return_value=mock_fee_history) + eip1559_gas_price, status = gas.get_eip1559_gas_price() + assert status.ok + assert eip1559_gas_price["maxPriorityFeePerGas"] == 1000000000 + base_fee = mock_fee_history["baseFeePerGas"][-1] + assert eip1559_gas_price["maxFeePerGas"] == gas.get_max_fee(base_fee) + # fee history is None + type(gas.web3.eth).fee_history = Mock(return_value=None) + eip1559_gas_price, status = gas.get_eip1559_gas_price() + assert not status.ok + assert eip1559_gas_price is None + assert "no base fee set" in status.error + # exception when fetching fee history + type(gas.web3.eth).fee_history = Mock(side_effect=ValueError("Mock Error")) + eip1559_gas_price, status = gas.get_eip1559_gas_price() + assert not status.ok + assert eip1559_gas_price is None + assert "Error fetching fee history" in status.error + assert "unable to fetch history to set base fee" in status.error + # when 2 out 3 fee args are user provided fee history from node isn't used + # instead maxFeePerGas is calculated from user provided values + type(gas.web3.eth).fee_history = Mock(return_value=mock_fee_history) + gas.priority_fee_per_gas = 1 # 1 Gwei. users provide gwei value + gas.base_fee_per_gas = 136 + eip1559_gas_price, status = gas.get_eip1559_gas_price() + assert status.ok + assert eip1559_gas_price["maxPriorityFeePerGas"] == 1 + assert eip1559_gas_price["maxFeePerGas"] == gas.get_max_fee(136) + + # when 1 out 3 fee args are user provided fee history from node used + # and maxFeePerGas and priority fee are calculated from fee history + type(gas.web3.eth).fee_history = Mock(return_value=mock_fee_history) + gas.priority_fee_per_gas = None + gas.base_fee_per_gas = gas.from_gwei(150) + eip1559_gas_price, status = gas.get_eip1559_gas_price() + assert status.ok + assert eip1559_gas_price["maxPriorityFeePerGas"] == 1000000000 # 1 Gwei + assert eip1559_gas_price["maxFeePerGas"] == gas.get_max_fee(gas.from_gwei(150)) + + +@pytest.mark.asyncio +async def test_update_gas_fees(gas_fees_object): + gas: GasFees = await gas_fees_object + status = gas.update_gas_fees() + assert status.ok, "update_gas_fees returned not ok status" + assert gas.gas_info["gasPrice"] is not None, "gas_info['gasPrice'] returned None" + assert gas.gas_info["gasPrice"] > 0, "gas_info['gasPrice'] is not fetched properly" + assert gas.gas_info["gas"] is None + assert gas.gas_info["maxFeePerGas"] is None + assert gas.gas_info["maxPriorityFeePerGas"] is None + gas_info_core = { + "gas_limit": None, + "max_fee_per_gas": None, + "max_priority_fee_per_gas": None, + "legacy_gas_price": 20.2, + } + assert gas.get_gas_info_core() == gas_info_core + + gas.web3 = Mock() + type(gas.web3.eth).gas_price = PropertyMock(return_value=None) + status = gas.update_gas_fees() + assert not status.ok + assert "Failed to update gas fees for legacy type transaction:" in status.error + assert gas.gas_info["gasPrice"] is None, "gas_info['gasPrice'] should be None since gas update failed" + # set gas price to None and check if gas info is updated properly + gas_info_core["legacy_gas_price"] = None + assert gas.get_gas_info_core() == gas_info_core + + # change to type 2 transactions + mock_fee_history = AttributeDict( + { + "baseFeePerGas": [13676801331, 14273862890, 13972887310, 14813623596, 14046654284, 13615655875], + "reward": [ + [100000000, 100000000, 1500000000], + [100000000, 100000000, 1500000000], + [100000000, 1000000000, 3000000000], + [100000000, 528000000, 2000000000], + [100000000, 103445451, 1650000000], + ], + } + ) + gas.transaction_type = 2 + type(gas.web3.eth).fee_history = Mock(return_value=mock_fee_history) + type(gas.web3.eth)._max_priority_fee = Mock(return_value=gas.from_gwei(1)) + status = gas.update_gas_fees() + base_fee = mock_fee_history["baseFeePerGas"][-1] + assert status.ok + assert gas.gas_info["gasPrice"] is None + assert gas.gas_info["gas"] is None + assert gas.gas_info["maxFeePerGas"] is not None + assert gas.gas_info["maxFeePerGas"] == gas.get_max_fee(base_fee) + assert gas.gas_info["maxPriorityFeePerGas"] == 1000000000 + + type(gas.web3.eth).fee_history = Mock(side_effect=ValueError("Mock Error")) + status = gas.update_gas_fees() + assert gas.gas_info["maxFeePerGas"] is None + assert gas.gas_info["maxPriorityFeePerGas"] is None + assert "Failed to update gas fees for EIP1559 type transaction:" in status.error + + gas.transaction_type = 5 + status = gas.update_gas_fees() + assert gas.gas_info["maxFeePerGas"] is None + assert gas.gas_info["maxPriorityFeePerGas"] is None + assert "Failed to update gas fees: invalid transaction type: 5" in status.error diff --git a/tests/reporters/test_interval_reporter.py b/tests/reporters/test_interval_reporter.py deleted file mode 100644 index cdb5a744..00000000 --- a/tests/reporters/test_interval_reporter.py +++ /dev/null @@ -1,232 +0,0 @@ -""" -Tests covering the IntervalReporter class from -telliot's reporters subpackage. -""" -import asyncio -from datetime import datetime -from unittest import mock - -import pytest -from telliot_core.utils.response import ResponseStatus -from web3.datastructures import AttributeDict - -from telliot_feeds.datafeed import DataFeed -from telliot_feeds.feeds.matic_usd_feed import matic_usd_median_feed -from telliot_feeds.sources.etherscan_gas import EtherscanGasPrice -from tests.utils.utils import gas_price -from tests.utils.utils import passing_bool_w_status -from tests.utils.utils import passing_status - - -@pytest.mark.asyncio -async def test_fetch_datafeed(tellor_flex_reporter): - r = tellor_flex_reporter - feed = await r.fetch_datafeed() - assert isinstance(feed, DataFeed) - - r.datafeed = None - assert r.datafeed is None - feed = await r.fetch_datafeed() - assert isinstance(feed, DataFeed) - - -@pytest.mark.skip(reason="EIP-1559 not supported by ganache") -@pytest.mark.asyncio -def test_get_fee_info(tellor_flex_reporter): - info, time = tellor_flex_reporter.get_fee_info() - - assert isinstance(time, datetime) - assert isinstance(info, EtherscanGasPrice) - assert isinstance(info.LastBlock, int) - assert info.LastBlock > 0 - assert isinstance(info.gasUsedRatio, list) - - -@pytest.mark.asyncio -async def test_get_num_reports_by_id(tellor_flex_reporter): - r = tellor_flex_reporter - num, status = await r.get_num_reports_by_id(matic_usd_median_feed.query.query_id) - - assert isinstance(status, ResponseStatus) - - if status.ok: - assert isinstance(num, int) - else: - assert num is None - - -@pytest.mark.asyncio -async def test_ensure_staked(tellor_flex_reporter): - """Test staking status of reporter.""" - staked, status = await tellor_flex_reporter.ensure_staked() - - assert staked - assert status.ok - - -@pytest.mark.asyncio -async def test_ensure_profitable(tellor_flex_reporter): - """Test profitability check.""" - r = tellor_flex_reporter - r.gas_info = {"type": 0, "gas_price": 1e9, "gas_limit": 300000} - - assert r.expected_profit == "YOLO" - - status = await r.ensure_profitable(r.datafeed) - - assert status.ok - - r.expected_profit = 1e10 - status = await r.ensure_profitable(r.datafeed) - - assert not status.ok - assert status.error == "Estimated profitability below threshold." - - -@pytest.mark.asyncio -async def test_ethgasstation_error(tellor_flex_reporter): - with mock.patch("telliot_feeds.reporters.interval.IntervalReporter.fetch_gas_price") as func: - func.return_value = None - r = tellor_flex_reporter - r.stake = 1000000 * 10**18 - - staked, status = await r.ensure_staked() - assert not staked - assert not status.ok - - -@pytest.mark.asyncio -async def test_interval_reporter_submit_once(tellor_flex_reporter): - """Test reporting once to the TellorX playground on Rinkeby - with three retries.""" - r = tellor_flex_reporter - - # Sync reporter - r.datafeed = None - - EXPECTED_ERRORS = { - "Current addess disputed. Switch address to continue reporting.", - "Current address is locked in dispute or for withdrawal.", - "Current address is in reporter lock.", - "Estimated profitability below threshold.", - "Estimated gas price is above maximum gas price.", - "Unable to retrieve updated datafeed value.", - } - - ORACLE_ADDRESSES = {r.oracle.address} - - tx_receipt, status = await r.report_once() - - # Reporter submitted - if tx_receipt is not None and status.ok: - assert isinstance(tx_receipt, AttributeDict) - assert tx_receipt.to in ORACLE_ADDRESSES - # Reporter did not submit - else: - assert not tx_receipt - assert not status.ok - assert status.error in EXPECTED_ERRORS - - -@pytest.mark.asyncio -async def test_no_updated_value(tellor_flex_reporter, bad_datasource): - """Test handling for no updated value returned from datasource.""" - r = tellor_flex_reporter - r.datafeed = matic_usd_median_feed - - # Clear latest datapoint - r.datafeed.source._history.clear() - - # Replace PriceAggregator's sources with test source that - # returns no updated DataPoint - r.datafeed.source.sources = [bad_datasource] - - r.fetch_gas_price = gas_price - r.check_reporter_lock = passing_status - r.ensure_profitable = passing_status - - tx_receipt, status = await r.report_once() - - assert not tx_receipt - assert not status.ok - print("status.error:", status.error) - assert status.error == "Unable to retrieve updated datafeed value." - - -@pytest.mark.skip("ensure_profitable is overritten in TelloFlexReporter") -@pytest.mark.asyncio -async def test_no_token_prices_for_profit_calc(tellor_flex_reporter, bad_datasource, guaranteed_price_source): - """Test handling for no token prices for profit calculation.""" - r = tellor_flex_reporter - - r.fetch_gas_price = gas_price - r.check_reporter_lock = passing_status - - # Simulate TRB/USD price retrieval failure - r.trb_usd_median_feed.source._history.clear() - r.eth_usd_median_feed.source.sources = [guaranteed_price_source] - r.trb_usd_median_feed.source.sources = [bad_datasource] - tx_receipt, status = await r.report_once() - - assert tx_receipt is None - assert not status.ok - assert status.error == "Unable to fetch TRB/USD price for profit calculation" - - # Simulate ETH/USD price retrieval failure - r.eth_usd_median_feed.source._history.clear() - r.eth_usd_median_feed.source.sources = [bad_datasource] - tx_receipt, status = await r.report_once() - - assert tx_receipt is None - assert not status.ok - assert status.error == "Unable to fetch ETH/USD price for profit calculation" - - -@pytest.mark.skip("ensure_staked is overritten in TelloFlexReporter") -@pytest.mark.asyncio -async def test_handle_contract_master_read_timeout(tellor_flex_reporter): - """Test handling for contract master read timeout.""" - - def conn_timeout(url, *args, **kwargs): - raise asyncio.exceptions.TimeoutError() - - with mock.patch("web3.contract.ContractFunction.call", side_effect=conn_timeout): - r = tellor_flex_reporter - r.fetch_gas_price = gas_price - staked, status = await r.ensure_staked() - - assert not staked - assert not status.ok - assert "Unable to read reporters staker status" in status.error - - -@pytest.mark.asyncio -async def test_ensure_reporter_lock_check_after_submitval_attempt(tellor_flex_reporter, guaranteed_price_source): - r = tellor_flex_reporter - r.last_submission_timestamp = 1234 - r.fetch_gas_price = gas_price - r.ensure_staked = passing_bool_w_status - r.ensure_profitable = passing_status - r.check_reporter_lock = passing_status - r.datafeed = matic_usd_median_feed - r.gas_limit = 350000 - - # Simulate fetching latest value - r.datafeed.source.sources = [guaranteed_price_source] - - async def num_reports(*args, **kwargs): - return 1, ResponseStatus() - - r.get_num_reports_by_id = num_reports - - assert r.last_submission_timestamp == 1234 - - def send_failure(*args, **kwargs): - raise Exception("bingo") - - with mock.patch("web3.eth.Eth.send_raw_transaction", side_effect=send_failure): - tx_receipt, status = await r.report_once() - assert tx_receipt is None - assert not status.ok - assert "bingo" in status.error - assert r.last_submission_timestamp == 0 diff --git a/tests/reporters/test_reporter.py b/tests/reporters/test_reporter.py new file mode 100644 index 00000000..64aa99c2 --- /dev/null +++ b/tests/reporters/test_reporter.py @@ -0,0 +1,199 @@ +from unittest.mock import Mock +from unittest.mock import patch + +import pytest +from telliot_core.utils.response import ResponseStatus +from web3.contract import Contract + +from telliot_feeds.feeds import matic_usd_median_feed +from telliot_feeds.feeds import trb_usd_median_feed +from telliot_feeds.reporters.tellor_360 import Tellor360Reporter +from telliot_feeds.reporters.types import StakerInfo + + +@pytest.mark.asyncio +async def test_get_stake_amount(tellor_flex_reporter): + r: Tellor360Reporter = tellor_flex_reporter + stake_amount, status = await r.get_stake_amount() + assert stake_amount > 0 + assert isinstance(stake_amount, int) + assert status.ok + + type(r.oracle.contract).get_function_by_name = Mock(side_effect=Exception("Mocked exception")) + stake_amount, status = await r.get_stake_amount() + assert stake_amount is None + assert not status.ok + assert status.error == ( + "Unable to read current stake amount: error reading from contract: Exception('Mocked exception')" + ) + + +@pytest.mark.asyncio +async def test_get_staker_details(tellor_flex_reporter): + r: Tellor360Reporter = tellor_flex_reporter + staker_details, status = await r.get_staker_details() + # staker details before any staking/reporting + assert isinstance(staker_details, StakerInfo) + assert staker_details.start_date == 0 + assert status.ok + + type(r.oracle.contract).get_function_by_name = Mock(side_effect=Exception("Mocked exception")) + staker_details, status = await r.get_staker_details() + assert staker_details is None + assert not status.ok + assert status.error == ( + "Unable to read account staker info: error reading from contract: Exception('Mocked exception')" + ) + + +@pytest.mark.asyncio +async def test_get_current_token_balance(tellor_flex_reporter): + r: Tellor360Reporter = tellor_flex_reporter + token_balance, status = await r.get_current_token_balance() + assert token_balance > 0 + assert isinstance(token_balance, int) + assert status.ok + + type(r.token.contract).get_function_by_name = Mock(side_effect=Exception("Mocked exception")) + token_balance, status = await r.get_current_token_balance() + assert token_balance is None + assert not status.ok + assert status.error == ( + "Unable to read account balance: error reading from contract: Exception('Mocked exception')" + ) + + +@pytest.mark.asyncio +async def test_ensure_staked(tellor_flex_reporter): + """Test ensure_staked method.""" + r: Tellor360Reporter = tellor_flex_reporter + # staker balance should initiate with None + assert r.stake_info.current_staker_balance is None + # ensure stake method fetches stake amount and staker details + # and stakes reporter if not already staked or has a low stake + known_stake, status = await r.ensure_staked() + # check that reporter got staked + assert r.stake_info.current_staker_balance > 0 + assert status.ok + assert known_stake + # mock the oracle read call to raise an exception + with patch.object(Contract, "get_function_by_name", side_effect=Exception("Mocked exception")): + known_stake, status = await r.ensure_staked() + assert not status.ok + assert not known_stake + + +@pytest.mark.asyncio +async def test_check_stake_amount_change(tellor_flex_reporter): + """Test what happens when the stake amount changes during reporting loops.""" + r: Tellor360Reporter = tellor_flex_reporter + # check stake amount + stake_amount, status = await r.get_stake_amount() + assert stake_amount > 0 + # check staker details + check1, status = await r.get_staker_details() + assert status.ok + assert check1.stake_balance == 0 + # stake by calling ensure_staked method which automatically stakes reporter + # if staked balance is lower than stake amount + known_stake, status = await r.ensure_staked() + assert known_stake + assert status.ok + # check staker details again + check2, status = await r.get_staker_details() + assert status.ok + assert check2.stake_balance > check1.stake_balance + # check staker details again after calling ensure_staked, should be same as check2 + known_stake, status = await r.ensure_staked() + assert known_stake + assert status.ok + check3, status = await r.get_staker_details() + assert status.ok + assert check3.stake_balance == check2.stake_balance + # mock increasing stake amount + with patch.object(Tellor360Reporter, "get_stake_amount", return_value=(stake_amount + 1, ResponseStatus())): + known_stake, status = await r.ensure_staked() + assert known_stake + assert status.ok + check4, status = await r.get_staker_details() + assert status.ok + # reporter staked balance should be increased by 1 + assert check4.stake_balance == check3.stake_balance + 1 + # mock additional stake chosen by user + assert r.stake == 0 + r.stake = stake_amount + 2 + known_stake, status = await r.ensure_staked() + assert known_stake + assert status.ok + b, status = await r.get_staker_details() + assert status.ok + # reporter staked balance should be increased by 2 + assert b.stake_balance == check3.stake_balance + 2 + + +@pytest.mark.asyncio +async def test_staking_after_a_reporter_slashing(tellor_flex_reporter, caplog): + """Test when reporter is disputed that they automatically be staked again""" + r: Tellor360Reporter = tellor_flex_reporter + # ensure reporter is staked + known_stake, status = await r.ensure_staked() + assert known_stake + assert status.ok + # mock reporter being slashed + with patch.object( + Tellor360Reporter, + "get_staker_details", + return_value=(StakerInfo(0, 0, 0, 0, 0, 0, 0, 0, True), ResponseStatus()), + ): + trb_balance, status = await r.get_current_token_balance() + known_stake, status = await r.ensure_staked() + assert known_stake + assert status.ok + assert "Your staked balance has decreased, account might be in dispute" in caplog.text + trb_balance2, status = await r.get_current_token_balance() + assert trb_balance2 < trb_balance + + +@pytest.mark.asyncio +async def test_stake_info(tellor_flex_reporter, guaranteed_price_source, chain): + """Test stake info changes and status""" + r: Tellor360Reporter = tellor_flex_reporter + feed = matic_usd_median_feed + feed.source = guaranteed_price_source + trb_usd_median_feed.source = guaranteed_price_source + r.expected_profit = "YOLO" + r.datafeed = feed + with patch.object(Tellor360Reporter, "check_reporter_lock", return_value=ResponseStatus()): + assert r.stake_info.last_report == 0 + assert r.stake_info.reports_count == 0 + assert not r.stake_info.is_in_dispute() + assert r.stake_info.last_report_time == 0 + assert r.stake_info.last_report_time == 0 + assert len(r.stake_info.stake_amount_history) == 0 + assert len(r.stake_info.staker_balance_history) == 0 + # report and check info + await r.report_once() + assert len(r.stake_info.stake_amount_history) == 1 + # this should be of length 2 since its updated after staking + assert len(r.stake_info.staker_balance_history) == 2 + # bypass 12 hour reporting lock + chain.sleep(84600) + r.datafeed = feed + await r.report_once() + # last report time should update during the second reporting loop + assert r.stake_info.last_report_time > 0 + # reports count should be 1 during the second reporting loop + assert r.stake_info.reports_count == 1 + # should be of length always since thats the max datapoints + assert len(r.stake_info.stake_amount_history) == 2 + assert len(r.stake_info.staker_balance_history) == 2 + + await r.report_once() + # reports count should be 2 during the third reporting loop + assert r.stake_info.reports_count == 2 + assert len(r.stake_info.stake_amount_history) == 2 + assert len(r.stake_info.staker_balance_history) == 2 + # mock a dispute by inputing a bad value to staker balance history deque + r.stake_info.staker_balance_history.append(0) + # dispute should be detected and return True + assert r.stake_info.is_in_dispute() diff --git a/tests/reporters/test_rng_reporter.py b/tests/reporters/test_rng_reporter.py index f427354b..9ee1a25a 100644 --- a/tests/reporters/test_rng_reporter.py +++ b/tests/reporters/test_rng_reporter.py @@ -183,7 +183,7 @@ async def assemble_feed(timestamp): await rng_reporter.report(2) # encoded value for timestamp 1678311000 count = caplog.text.count( - "IntervalReporter Encoded value: 236eabcc1c1dc5c01bd6357576b17e490eaf7aaa37b360485cddcd6877a395c3" + "Reporter Encoded value: 236eabcc1c1dc5c01bd6357576b17e490eaf7aaa37b360485cddcd6877a395c3" ) # count is 2 because second report uses same value assert count == 2 @@ -203,6 +203,6 @@ async def test_unique_rng_report_value(rng_reporter: RNGReporter, monkeypatch, c ) await rng_reporter.report(2) count = caplog.text.count( - "IntervalReporter Encoded value: 236eabcc1c1dc5c01bd6357576b17e490eaf7aaa37b360485cddcd6877a395c3" + "Reporter Encoded value: 236eabcc1c1dc5c01bd6357576b17e490eaf7aaa37b360485cddcd6877a395c3" ) assert count == 1 diff --git a/tests/reporters/test_stake_info.py b/tests/reporters/test_stake_info.py new file mode 100644 index 00000000..92f4dc4d --- /dev/null +++ b/tests/reporters/test_stake_info.py @@ -0,0 +1,16 @@ +from telliot_feeds.utils.stake_info import StakeInfo + +stake_info = StakeInfo() + + +def test_storage_length(): + assert len(stake_info.stake_amount_history) == 0 + # Add some stake amounts + stake_info.store_stake_amount(100) + assert len(stake_info.stake_amount_history) == 1 + stake_info.store_stake_amount(200) + assert len(stake_info.stake_amount_history) == 2 + stake_info.store_stake_amount(300) # This should cause 100 to be removed + assert len(stake_info.stake_amount_history) == 2 + assert stake_info.stake_amount_history[0] == 200 + assert stake_info.stake_amount_history[1] == 300 diff --git a/tests/sources/test_evm_call_source.py b/tests/sources/test_evm_call_source.py index 91842f32..55adacae 100644 --- a/tests/sources/test_evm_call_source.py +++ b/tests/sources/test_evm_call_source.py @@ -143,7 +143,7 @@ async def test_report_for_bad_calldata(tellor_360): await r.report_once() # call r.ensure_staked to update staker info await r.ensure_staked() - assert r.staker_info.reports_count == 1 + assert r.stake_info.reports_count == 1 def test_evm_call_on_previous_block(): diff --git a/tests/sources/test_spot_price_sources.py b/tests/sources/test_spot_price_sources.py index 35d1eee0..1ac3a52a 100644 --- a/tests/sources/test_spot_price_sources.py +++ b/tests/sources/test_spot_price_sources.py @@ -24,6 +24,7 @@ PancakeswapPriceService, ) from telliot_feeds.sources.price.spot.uniswapV3 import UniswapV3PriceService +from telliot_feeds.sources.sweth_source import SWETH_CONTRACT from telliot_feeds.sources.sweth_source import swETHSpotPriceService @@ -312,6 +313,8 @@ async def test_curvefi(): @pytest.mark.asyncio async def test_sweth_source(): """Test swETH price service""" + service["sweth"].contract = SWETH_CONTRACT + service["sweth"].calldata = "0xd68b2cb6" v, t = await get_price("sweth", "usd", service["sweth"]) validate_price(v, t) assert v is not None diff --git a/tests/test_autopay.py b/tests/test_autopay.py deleted file mode 100644 index 4b58daa3..00000000 --- a/tests/test_autopay.py +++ /dev/null @@ -1,238 +0,0 @@ -import pytest -from brownie import accounts -from brownie import chain -from brownie.network.account import Account -from eth_abi import encode_single -from telliot_core.apps.core import TelliotCore -from telliot_core.utils.response import ResponseStatus -from telliot_core.utils.timestamp import TimeStamp -from web3 import Web3 - -from telliot_feeds.queries.query_catalog import query_catalog -from telliot_feeds.reporters.reporter_autopay_utils import autopay_suggested_report -from telliot_feeds.reporters.reporter_autopay_utils import get_feed_tip - - -@pytest.mark.asyncio -async def test_main( - mumbai_test_cfg, - mock_flex_contract, - mock_autopay_contract, - mock_token_contract, - multicall_contract, - mock_gov_contract, -): - async with TelliotCore(config=mumbai_test_cfg) as core: - - # get PubKey and PrivKey from config files - account = core.get_account() - - flex = core.get_tellorflex_contracts() - flex.oracle.address = mock_flex_contract.address - flex.autopay.address = mock_autopay_contract.address - flex.autopay.abi = mock_autopay_contract.abi - flex.token.address = mock_token_contract.address - - flex.oracle.connect() - flex.token.connect() - flex.autopay.connect() - - # mint token and send to reporter address - mock_token_contract.faucet(account.address) - - # send eth from brownie address to reporter address for txn fees - accounts[0].transfer(account.address, "1 ether") - assert Account(account.address).balance() == 1e18 - - # check governance address is brownie address - governance_address = await flex.oracle.get_governance_address() - assert governance_address == mock_gov_contract.address - - # check stake amount is ten - stake_amount = await flex.oracle.get_stake_amount() - assert stake_amount == 10 - - # approve token to be spent by oracle - mock_token_contract.approve(mock_flex_contract.address, 50e18, {"from": account.address}) - - # staking to oracle transaction - timestamp = TimeStamp.now().ts - _, status = await flex.oracle.write("depositStake", gas_limit=350000, legacy_gas_price=1, _amount=10 * 10**18) - # check txn is successful - assert status.ok - - # check staker information - staker_info, status = await flex.oracle.get_staker_info(Web3.toChecksumAddress(account.address)) - assert isinstance(status, ResponseStatus) - assert status.ok - assert staker_info == [pytest.approx(timestamp, 200), 10e18, 0, 0, 0] - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag is None - assert tip is None - - # mkr query id and query data - mkr_query_id = query_catalog._entries["mkr-usd-spot"].query.query_id - mkr_query_data = "0x" + query_catalog._entries["mkr-usd-spot"].query.query_data.hex() - # approve token to be spent by autopay contract - mock_token_contract.approve(mock_autopay_contract.address, 500e18, {"from": account.address}) - _, status = await flex.autopay.write( - "tip", - gas_limit=3500000, - legacy_gas_price=1, - _queryId="0x" + mkr_query_id.hex(), - _amount=int(10e18), - _queryData=mkr_query_data, - ) - # check txn is successful - assert status.ok - - # submit a tip in autopay for reporter to report mkr/usd price - current_tip, status = await flex.autopay.get_current_tip(mkr_query_id) - # check success of txn - assert status.ok - # check tip amount - assert current_tip == 10e18 - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag == "mkr-usd-spot" - assert tip == 10e18 - - # query id and query data for ric - ric_query_id = query_catalog._entries["ric-usd-spot"].query_id - ric_query_data = "0x" + query_catalog._entries["ric-usd-spot"].query.query_data.hex() - - _, status = await flex.autopay.write( - "tip", - gas_limit=3500000, - legacy_gas_price=1, - _queryId=ric_query_id, - _amount=int(20e18), - _queryData=ric_query_data, - ) - - assert status.ok - - current_tip, status = await flex.autopay.get_current_tip(ric_query_id) - assert status.ok - assert current_tip == 20e18 - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag == "ric-usd-spot" - assert tip == 20e18 - - # variables for feed setup and to get feedId - trb_query_id = query_catalog._entries["trb-usd-spot"].query_id - reward = 30 * 10**18 - interval = 100 - window = 99 - price_threshold = 0 - trb_query_data = "0x" + query_catalog._entries["trb-usd-spot"].query.query_data.hex() - - # setup a feed on autopay - response, status = await flex.autopay.write( - "setupDataFeed", - gas_limit=3500000, - legacy_gas_price=1, - _queryId=trb_query_id, - _reward=reward, - _startTime=timestamp, - _interval=interval, - _window=window, - _priceThreshold=price_threshold, - _rewardIncreasePerSecond=0, - _queryData=trb_query_data, - _amount=50 * 10**18, - ) - assert status.ok - - feed_id = response.logs[1].topics[2].hex() - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag == "trb-usd-spot" - assert tip == 30e18 - - tips = await get_feed_tip(query_catalog._entries["trb-usd-spot"].query.query_id, flex.autopay) - assert tips == 30e18 - - # submit report to oracle to get tip - _, status = await flex.oracle.write( - "submitValue", - gas_limit=350000, - legacy_gas_price=1, - _queryId=trb_query_id, - _value="0x" + encode_single("(uint256)", [3000]).hex(), - _nonce=0, - _queryData=trb_query_data, - ) - chain.snapshot() - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag == "ric-usd-spot" - assert tip == 20e18 - - # fast forward to avoid claiming tips buffer 12hr - chain.sleep(43201) - - # get timestamp trb's reported value - read_timestamp, status = await flex.autopay.read("getCurrentValue", _queryId=trb_query_id) - assert status.ok - - _, status = await flex.autopay.write( - "claimTip", - gas_limit=350000, - legacy_gas_price=1, - _feedId=feed_id, - _queryId=trb_query_id, - _timestamps=[read_timestamp[2]], - ) - assert status.ok - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag == "ric-usd-spot" - assert tip == 20e18 - - # submit report for onetime tip to oracle - # should reserve tip for first reporter - _, status = await flex.oracle.write( - "submitValue", - gas_limit=350000, - legacy_gas_price=1, - _queryId=ric_query_id, - _value="0x" + encode_single("(uint256)", [1000]).hex(), - _nonce=0, - _queryData=ric_query_data, - ) - assert status.ok - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag == "mkr-usd-spot" - assert tip == 10e18 - - # fast forward to avoid reporter time lock - chain.sleep(43201) - - # submit report for onetime tip to oracle - # should reserve tip for first reporter - _, status = await flex.oracle.write( - "submitValue", - gas_limit=350000, - legacy_gas_price=1, - _queryId=mkr_query_id, - _value="0x" + encode_single("(uint256)", [1000]).hex(), - _nonce=0, - _queryData=mkr_query_data, - ) - assert status.ok - - # get suggestion from telliot on query with highest tip - suggested_qtag, tip = await autopay_suggested_report(flex.autopay) - assert suggested_qtag is None - assert tip is None diff --git a/tests/test_bct_usd.py b/tests/test_bct_usd.py index 9dc81d0c..34d88630 100644 --- a/tests/test_bct_usd.py +++ b/tests/test_bct_usd.py @@ -4,7 +4,7 @@ from web3.datastructures import AttributeDict from telliot_feeds.feeds.bct_usd_feed import bct_usd_median_feed -from telliot_feeds.reporters.tellor_flex import TellorFlexReporter +from telliot_feeds.reporters.tellor_360 import Tellor360Reporter @pytest.mark.asyncio @@ -16,7 +16,7 @@ async def test_bct_usd_reporter_submit_once( # get PubKey and PrivKey from config files account = core.get_account() - flex = core.get_tellorflex_contracts() + flex = core.get_tellor360_contracts() flex.oracle.address = mock_flex_contract.address flex.autopay.address = mock_autopay_contract.address flex.token.address = mock_token_contract.address @@ -31,7 +31,7 @@ async def test_bct_usd_reporter_submit_once( # send eth from brownie address to reporter address for txn fees accounts[1].transfer(account.address, "1 ether") - r = TellorFlexReporter( + r = Tellor360Reporter( endpoint=core.endpoint, account=account, chain_id=80001, @@ -40,7 +40,7 @@ async def test_bct_usd_reporter_submit_once( autopay=flex.autopay, transaction_type=0, datafeed=bct_usd_median_feed, - max_fee=100, + max_fee_per_gas=100, ) ORACLE_ADDRESSES = {mock_flex_contract.address} diff --git a/tests/test_dai_usd.py b/tests/test_dai_usd.py index 9b23d2f9..659c77eb 100644 --- a/tests/test_dai_usd.py +++ b/tests/test_dai_usd.py @@ -4,7 +4,7 @@ from web3.datastructures import AttributeDict from telliot_feeds.feeds.dai_usd_feed import dai_usd_median_feed -from telliot_feeds.reporters.tellor_flex import TellorFlexReporter +from telliot_feeds.reporters.tellor_360 import Tellor360Reporter @pytest.mark.asyncio @@ -16,7 +16,7 @@ async def test_dai_usd_reporter_submit_once( # get PubKey and PrivKey from config files account = core.get_account() - flex = core.get_tellorflex_contracts() + flex = core.get_tellor360_contracts() flex.oracle.address = mock_flex_contract.address flex.autopay.address = mock_autopay_contract.address flex.token.address = mock_token_contract.address @@ -31,7 +31,7 @@ async def test_dai_usd_reporter_submit_once( # send eth from brownie address to reporter address for txn fees accounts[2].transfer(account.address, "1 ether") - r = TellorFlexReporter( + r = Tellor360Reporter( endpoint=core.endpoint, account=account, chain_id=80001, @@ -40,7 +40,7 @@ async def test_dai_usd_reporter_submit_once( autopay=flex.autopay, transaction_type=0, datafeed=dai_usd_median_feed, - max_fee=100, + max_fee_per_gas=100, ) ORACLE_ADDRESSES = {mock_flex_contract.address} diff --git a/tests/test_numeric_api_response_feed.py b/tests/test_numeric_api_response_feed.py index 9b1088ca..0c6aa377 100644 --- a/tests/test_numeric_api_response_feed.py +++ b/tests/test_numeric_api_response_feed.py @@ -5,7 +5,7 @@ from telliot_feeds.datafeed import DataFeed from telliot_feeds.queries.numeric_api_response_query import NumericApiResponse -from telliot_feeds.reporters.tellor_flex import TellorFlexReporter +from telliot_feeds.reporters.tellor_360 import Tellor360Reporter from telliot_feeds.sources.numeric_api_response import NumericApiResponseSource url = "https://taylorswiftapi.herokuapp.com/get" @@ -25,7 +25,7 @@ async def test_api_reporter_submit_once( # get PubKey and PrivKey from config files account = core.get_account() - flex = core.get_tellorflex_contracts() + flex = core.get_tellor360_contracts() flex.oracle.address = mock_flex_contract.address flex.autopay.address = mock_autopay_contract.address flex.token.address = mock_token_contract.address @@ -40,7 +40,7 @@ async def test_api_reporter_submit_once( # send eth from brownie address to reporter address for txn fees accounts[1].transfer(account.address, "1 ether") - r = TellorFlexReporter( + r = Tellor360Reporter( endpoint=core.endpoint, account=account, chain_id=80001, @@ -49,7 +49,7 @@ async def test_api_reporter_submit_once( autopay=flex.autopay, transaction_type=0, datafeed=numeric_api_rsp_feed, - max_fee=100, + max_fee_per_gas=100, ) ORACLE_ADDRESSES = {mock_flex_contract.address}