diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 203771154c..96b5c9c289 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -36,7 +36,7 @@ def create_app(config=None): register_extensions(app) register_errorhandlers(app) register_epoch_settings() - register_services() + register_services(app) return app diff --git a/backend/app/context/epoch_details.py b/backend/app/context/epoch_details.py index f6976c82c6..11c670db1b 100644 --- a/backend/app/context/epoch_details.py +++ b/backend/app/context/epoch_details.py @@ -1,7 +1,6 @@ from datetime import datetime from typing import List, Tuple -from app.settings import config from app.context.epoch_state import EpochState from app.context.helpers import check_if_future from app.exceptions import InvalidBlocksRange @@ -9,7 +8,6 @@ from app.infrastructure import graphql from app.infrastructure.external_api.etherscan.blocks import get_block_num_from_ts from app.legacy.utils.time import from_timestamp_s, sec_to_days -from app.shared.blockchain_types import compare_blockchain_types, ChainTypes class EpochDetails: @@ -20,6 +18,7 @@ def __init__( duration, decision_window, remaining_sec=None, + with_block_range=False, ): self.epoch_num = int(epoch_num) self.duration_sec = int(duration) @@ -28,9 +27,9 @@ def __init__( self.decision_window_days = sec_to_days(self.decision_window_sec) self.start_sec = int(start) self.end_sec = self.start_sec + self.duration_sec - self.start_block, self.end_block = self._calc_blocks_range() self.finalized_sec = self.end_sec + self.decision_window_sec self.finalized_timestamp = from_timestamp_s(self.finalized_sec) + self.start_block, self.end_block = self._calc_blocks_range(with_block_range) if remaining_sec is None: now_sec = int(datetime.utcnow().timestamp()) @@ -44,14 +43,13 @@ def __init__( self.remaining_sec = remaining_sec self.remaining_days = sec_to_days(self.remaining_sec) - self.block_rewards = None @property def duration_range(self) -> Tuple[int, int]: return self.start_sec, self.end_sec @property - def no_blocks(self): + def blocks_range(self): """ Returns the number of blocks within [start_block, end_block) in the epoch. """ @@ -59,14 +57,10 @@ def no_blocks(self): raise InvalidBlocksRange return self.end_block - self.start_block - def _calc_blocks_range(self) -> tuple: - can_blocks_be_calced = ( - compare_blockchain_types(config.CHAIN_ID, ChainTypes.MAINNET) - and self.start_sec - and self.end_sec - ) + def _calc_blocks_range(self, with_block_range=False) -> tuple: start_block, end_block = None, None - if can_blocks_be_calced: + + if with_block_range: is_start_future = check_if_future(self.start_sec) is_end_future = check_if_future(self.end_sec) @@ -80,22 +74,26 @@ def _calc_blocks_range(self) -> tuple: return start_block, end_block -def get_epoch_details(epoch_num: int, epoch_state: EpochState) -> EpochDetails: +def get_epoch_details( + epoch_num: int, epoch_state: EpochState, with_block_range=False +) -> EpochDetails: if epoch_state == EpochState.FUTURE: return get_future_epoch_details(epoch_num) - return get_epoch_details_by_number(epoch_num) + return get_epoch_details_by_number(epoch_num, with_block_range) -def get_epoch_details_by_number(epoch_num: int) -> EpochDetails: +def get_epoch_details_by_number(epoch_num: int, with_block_range=False) -> EpochDetails: epoch_details = graphql.epochs.get_epoch_by_number(epoch_num) - return _epoch_details_from_graphql_result(epoch_details) + return _epoch_details_from_graphql_result(epoch_details, with_block_range) -def get_epochs_details(from_epoch: int, to_epoch: int) -> List[EpochDetails]: +def get_epochs_details( + from_epoch: int, to_epoch: int, with_block_range=False +) -> List[EpochDetails]: epochs_details = graphql.epochs.get_epochs_by_range(from_epoch, to_epoch) return [ - _epoch_details_from_graphql_result(epoch_details) + _epoch_details_from_graphql_result(epoch_details, with_block_range) for epoch_details in epochs_details ] @@ -111,10 +109,13 @@ def get_future_epoch_details(epoch_num: int) -> EpochDetails: ) -def _epoch_details_from_graphql_result(epoch_details: dict) -> EpochDetails: +def _epoch_details_from_graphql_result( + epoch_details: dict, with_block_range=False +) -> EpochDetails: return EpochDetails( epoch_num=epoch_details["epoch"], start=epoch_details["fromTs"], duration=epoch_details["duration"], decision_window=epoch_details["decisionWindow"], + with_block_range=with_block_range, ) diff --git a/backend/app/context/manager.py b/backend/app/context/manager.py index 3435198147..e44742fc1f 100644 --- a/backend/app/context/manager.py +++ b/backend/app/context/manager.py @@ -1,10 +1,12 @@ from flask import current_app as app +from app.extensions import cache from app.context.epoch_details import get_epoch_details, EpochDetails from app.context.epoch_state import EpochState, get_epoch_state, get_epoch_number from app.context.projects import ProjectsDetails, get_projects_details from app.engine.epochs_settings import get_epoch_settings, EpochSettings from app.pydantic import Model +from app.shared.blockchain_types import compare_blockchain_types, ChainTypes class Context(Model): @@ -24,8 +26,13 @@ def state_context(epoch_state: EpochState) -> Context: return build_context(epoch_num, epoch_state) +@cache.memoize(timeout=600) def build_context(epoch_num: int, epoch_state: EpochState) -> Context: - epoch_details = get_epoch_details(epoch_num, epoch_state) + is_mainnet = compare_blockchain_types(app.config["CHAIN_ID"], ChainTypes.MAINNET) + + epoch_details = get_epoch_details( + epoch_num, epoch_state, with_block_range=is_mainnet + ) epoch_settings = get_epoch_settings(epoch_num) projects_details = get_projects_details(epoch_num) context = Context( diff --git a/backend/app/infrastructure/external_api/bitquery/blocks_reward.py b/backend/app/infrastructure/external_api/bitquery/blocks_reward.py index 2d90af92b5..6f41928c0c 100644 --- a/backend/app/infrastructure/external_api/bitquery/blocks_reward.py +++ b/backend/app/infrastructure/external_api/bitquery/blocks_reward.py @@ -11,25 +11,20 @@ ) -def get_blocks_rewards( - address: str, start_time: str, end_time: str, limit: int -) -> list: +def get_blocks_rewards(address: str, start_block: int, end_block: int) -> float: """ - Fetch Ethereum blocks within a specified time range in ascending order by timestamp. + Fetch Ethereum blocks rewards for given address and start and end block. Args: - - start_time (str): The start time in ISO 8601 format. - - end_time (str): The end time in ISO 8601 format. + - start_block (str): The start block number. + - end_block (str): The end block number. - address (str): The miner (fee recipient) address. - - limit (int): The number of blocks to retrieve starting from start_time. - Useful whilst getting end_blocks exclusively from epochs. """ payload = produce_payload( action_type=BitQueryActions.GET_BLOCK_REWARDS, address=address, - start_time=start_time, - end_time=end_time, - limit=limit, + start_block=start_block, + end_block=end_block, ) headers = get_bitquery_header() @@ -43,5 +38,4 @@ def get_blocks_rewards( app_module.ExceptionHandler.print_stacktrace(e) raise ExternalApiException(api_url, e, 500) - blocks = json_response.json()["data"]["ethereum"]["blocks"] - return blocks + return json_response["data"]["ethereum"]["blocks"][0]["reward"] diff --git a/backend/app/infrastructure/external_api/bitquery/req_producer.py b/backend/app/infrastructure/external_api/bitquery/req_producer.py index 870eda3256..1a7ea9fe77 100644 --- a/backend/app/infrastructure/external_api/bitquery/req_producer.py +++ b/backend/app/infrastructure/external_api/bitquery/req_producer.py @@ -25,32 +25,20 @@ def produce_payload(action_type: BitQueryActions, **query_values) -> str: def _block_rewards_payload( - start_time: str, end_time: str, address: str, limit: int, **kwargs + start_block: int, end_block: int, address: str, **kwargs ) -> str: payload = json.dumps( { - "query": f"""query ($network: EthereumNetwork!, $from: ISO8601DateTime, $till: ISO8601DateTime) {{ - ethereum(network: $network) {{ - blocks(time: {{since: $from, till: $till}}, options: {{asc: "timestamp.unixtime", limit: {limit}}}) {{ - timestamp {{ - unixtime - }} - reward - address: miner(miner: {{is: "{address}"}}) {{ - address - }} + "query": f"""query {{ + ethereum {{ + blocks( + height: {{gteq: {start_block}, lteq: {end_block}}}, + miner: {{is: "{address}"}} + ) {{ + reward(blockReward: {{gt: 0}}) }} }} - }}""", - "variables": json.dumps( - { - "network": "ethereum", - "from": start_time, - "till": end_time, - "limit": limit, - "dateFormat": "%Y-%m-%d", - } - ), + }}""" } ) diff --git a/backend/app/legacy/utils/time.py b/backend/app/legacy/utils/time.py index 93d53a0640..27e4392202 100644 --- a/backend/app/legacy/utils/time.py +++ b/backend/app/legacy/utils/time.py @@ -63,8 +63,3 @@ def sec_to_days(sec: int) -> int: def days_to_sec(days: int) -> int: return int(days * 86400) - - -def timestamp_to_isoformat(timestamp_sec: int) -> str: - timestamp = from_timestamp_s(timestamp_sec) - return timestamp.to_isoformat() diff --git a/backend/app/modules/modules_factory/pre_pending.py b/backend/app/modules/modules_factory/pre_pending.py index e97ce2842b..a9cd7573ac 100644 --- a/backend/app/modules/modules_factory/pre_pending.py +++ b/backend/app/modules/modules_factory/pre_pending.py @@ -2,7 +2,6 @@ import app.modules.staking.proceeds.service.aggregated as aggregated import app.modules.staking.proceeds.service.contract_balance as contract_balance -from app.shared.blockchain_types import ChainTypes, compare_blockchain_types from app.modules.modules_factory.protocols import ( AllUserEffectiveDeposits, OctantRewards, @@ -16,7 +15,7 @@ DbAndGraphEventsGenerator, ) from app.pydantic import Model -from app.settings import config +from app.shared.blockchain_types import compare_blockchain_types, ChainTypes class PrePendingUserDeposits(UserEffectiveDeposits, AllUserEffectiveDeposits, Protocol): @@ -29,13 +28,12 @@ class PrePendingServices(Model): pending_snapshots_service: PendingSnapshots @staticmethod - def create() -> "PrePendingServices": + def create(chain_id: int) -> "PrePendingServices": + is_mainnet = compare_blockchain_types(chain_id, ChainTypes.MAINNET) + user_deposits = CalculatedUserDeposits( events_generator=DbAndGraphEventsGenerator() ) - current_chain_id = config.CHAIN_ID - is_mainnet = compare_blockchain_types(current_chain_id, ChainTypes.MAINNET) - octant_rewards = CalculatedOctantRewards( staking_proceeds=aggregated.AggregatedStakingProceeds() if is_mainnet diff --git a/backend/app/modules/registry.py b/backend/app/modules/registry.py index 80c9d24cd6..2ff781aec3 100644 --- a/backend/app/modules/registry.py +++ b/backend/app/modules/registry.py @@ -15,10 +15,12 @@ def get_services(epoch_state: EpochState): return SERVICE_REGISTRY[epoch_state] -def register_services(): +def register_services(app): + chain_id = app.config["CHAIN_ID"] + SERVICE_REGISTRY[EpochState.FUTURE] = FutureServices.create() SERVICE_REGISTRY[EpochState.CURRENT] = CurrentServices.create() - SERVICE_REGISTRY[EpochState.PRE_PENDING] = PrePendingServices.create() + SERVICE_REGISTRY[EpochState.PRE_PENDING] = PrePendingServices.create(chain_id) SERVICE_REGISTRY[EpochState.PENDING] = PendingServices.create() SERVICE_REGISTRY[EpochState.FINALIZING] = FinalizingServices.create() SERVICE_REGISTRY[EpochState.FINALIZED] = FinalizedServices.create() diff --git a/backend/app/modules/staking/proceeds/core.py b/backend/app/modules/staking/proceeds/core.py index e156a0300a..c88816503c 100644 --- a/backend/app/modules/staking/proceeds/core.py +++ b/backend/app/modules/staking/proceeds/core.py @@ -1,5 +1,3 @@ -from decimal import Decimal - import pandas as pd from gmpy2 import mpz @@ -56,15 +54,8 @@ def sum_withdrawals(withdrawals_txs: list[dict]) -> int: return w3.to_wei(int(total_gwei), "gwei") -def sum_blocks_rewards(blocks_rewards: list) -> int: - df = pd.DataFrame(blocks_rewards) - blocks_reward_eth = df["reward"].apply(Decimal).sum() - - return int(w3.to_wei(blocks_reward_eth, "ether")) - - -def aggregate_proceeds(mev: int, withdrawals: int, blocks_rewards: list) -> int: - return mev + withdrawals + sum_blocks_rewards(blocks_rewards) +def aggregate_proceeds(mev: int, withdrawals: int, blocks_rewards_eth: float) -> int: + return mev + withdrawals + int(w3.to_wei(blocks_rewards_eth, "ether")) def _filter_deposit_withdrawals(amount: mpz) -> mpz: diff --git a/backend/app/modules/staking/proceeds/service/aggregated.py b/backend/app/modules/staking/proceeds/service/aggregated.py index ae121b87f4..e734dba005 100644 --- a/backend/app/modules/staking/proceeds/service/aggregated.py +++ b/backend/app/modules/staking/proceeds/service/aggregated.py @@ -6,7 +6,6 @@ AccountAction, ) from app.infrastructure.external_api.bitquery.blocks_reward import get_blocks_rewards -from app.legacy.utils.time import timestamp_to_isoformat from app.modules.staking.proceeds.core import ( sum_mev, sum_withdrawals, @@ -16,24 +15,6 @@ class AggregatedStakingProceeds(Model): - def _retrieve_blocks_rewards( - self, start_sec: int, end_sec: int, withdrawals_target: str, limit: int - ) -> list: - blocks_rewards = [] - - if end_sec is None: - return blocks_rewards - - start_datetime, end_datetime = ( - timestamp_to_isoformat(start_sec), - timestamp_to_isoformat(end_sec), - ) - - blocks_rewards = get_blocks_rewards( - withdrawals_target, start_datetime, end_datetime, limit=limit - ) - return blocks_rewards - def get_staking_proceeds(self, context: Context) -> int: """ Retrieves a list of transactions, calculates MEV value and aggregates it with withdrawals. @@ -50,15 +31,8 @@ def get_staking_proceeds(self, context: Context) -> int: context.epoch_details.start_block, context.epoch_details.end_block, ) - - start_sec, end_sec = context.epoch_details.duration_range - no_blocks_to_get = context.epoch_details.no_blocks - - blocks_rewards = self._retrieve_blocks_rewards( - start_sec, end_sec, withdrawals_target, limit=no_blocks_to_get - ) - end_block_for_transactions = end_block - 1 + normal = get_transactions( withdrawals_target, start_block, @@ -77,7 +51,10 @@ def get_staking_proceeds(self, context: Context) -> int: end_block_for_transactions, tx_type=AccountAction.BEACON_WITHDRAWAL, ) + block_rewards_value = get_blocks_rewards( + withdrawals_target, start_block, end_block_for_transactions + ) mev_value = sum_mev(withdrawals_target, normal, internal) withdrawals_value = sum_withdrawals(withdrawals) - return aggregate_proceeds(mev_value, withdrawals_value, blocks_rewards) + return aggregate_proceeds(mev_value, withdrawals_value, block_rewards_value) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 41a8654a8e..f9c43ec2b3 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -113,26 +113,14 @@ def mock_bitquery_api_get_blocks_rewards(*args, **kwargs): "ethereum": { "blocks": [ { - "timestamp": {"unixtime": 1708448963}, - "reward": 0.024473700594149782, - "address": { - "address": "0x1f9090aae28b8a3dceadf281b0f12828e676c326" - }, - }, - { - "timestamp": {"unixtime": 1708449035}, - "reward": 0.05342909432569912, - "address": { - "address": "0x1f9090aae28b8a3dceadf281b0f12828e676c326" - }, + "reward": 0.077902794919848902, }, ] } } } - blocks = example_resp_json["data"]["ethereum"]["blocks"] - return blocks + return example_resp_json["data"]["ethereum"]["blocks"][0]["reward"] def pytest_addoption(parser): @@ -738,26 +726,6 @@ def mock_user_rewards(alice, bob): return user_rewards_service_mock -@pytest.fixture(scope="function") -def patch_compare_blockchain_types_for_mainnet(monkeypatch): - monkeypatch.setattr( - "app.modules.modules_factory.pre_pending.compare_blockchain_types", - lambda *args: True, - ) - monkeypatch.setattr( - "app.context.epoch_details.compare_blockchain_types", - lambda *args: True, - ) - - -@pytest.fixture(scope="function") -def patch_compare_blockchain_types_for_not_mainnet(monkeypatch): - monkeypatch.setattr( - "app.modules.modules_factory.pre_pending.compare_blockchain_types", - lambda *args: False, - ) - - def allocate_user_rewards( user_account: Account, proposal_account, allocation_amount, nonce: int = 0 ): diff --git a/backend/tests/context/test_epoch_details.py b/backend/tests/context/test_epoch_details.py index 4970525d34..daa3864cf8 100644 --- a/backend/tests/context/test_epoch_details.py +++ b/backend/tests/context/test_epoch_details.py @@ -1,18 +1,16 @@ from app.context.epoch_details import EpochDetails -def test_check_if_epoch_details_gets_blocks_when_mainnet( - patch_compare_blockchain_types_for_mainnet, patch_etherscan_get_block_api -): +def test_check_if_epoch_details_gets_blocks_when_mainnet(patch_etherscan_get_block_api): mocked_etherscan_block = 12712551 - epoch_details = EpochDetails(1, 1, 1, 1) + epoch_details = EpochDetails(1, 1, 1, 1, with_block_range=True) assert epoch_details.start_block == mocked_etherscan_block assert epoch_details.end_block == mocked_etherscan_block def test_check_if_epoch_details_gets_blocks_when_not_mainnet(): - epoch_details = EpochDetails(1, 1, 1, 1) + epoch_details = EpochDetails(1, 1, 1, 1, with_block_range=False) assert epoch_details.start_block is None assert epoch_details.end_block is None diff --git a/backend/tests/helpers/context.py b/backend/tests/helpers/context.py index 92d36dc935..f0ceb50fe8 100644 --- a/backend/tests/helpers/context.py +++ b/backend/tests/helpers/context.py @@ -11,8 +11,7 @@ def get_epoch_details( duration=1000, decision_window=500, remaining_sec=1000, - start_block: int = 12712551, - end_block: int = 12712551, + with_block_range=False, ): return EpochDetails( epoch_num=epoch_num, @@ -20,8 +19,7 @@ def get_epoch_details( start=start, decision_window=decision_window, remaining_sec=remaining_sec, - start_block=start_block, - end_block=end_block, + with_block_range=with_block_range, ) diff --git a/backend/tests/infrastracture/external_api/bitquery/test_acc_blocks_reward.py b/backend/tests/infrastracture/external_api/bitquery/test_acc_blocks_reward.py deleted file mode 100644 index 66bb7b4ef2..0000000000 --- a/backend/tests/infrastracture/external_api/bitquery/test_acc_blocks_reward.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -from app.modules.staking.proceeds.core import ( - sum_blocks_rewards, -) - - -@pytest.mark.parametrize( - "blocks, result", - [ - ( - [{"reward": 0.024473700594149782}, {"reward": 0.05342909432569912}], - 77902794919848899, - ) - ], -) -def test_sum_blocks_rewards(blocks, result): - actual_result = sum_blocks_rewards(blocks) - assert actual_result == result diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 6dd985a2bc..65c350798d 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -1,8 +1,12 @@ -from app.context.epoch_state import EpochState +from app.modules.modules_factory.current import CurrentServices +from app.modules.modules_factory.finalized import FinalizedServices +from app.modules.modules_factory.finalizing import FinalizingServices +from app.modules.modules_factory.future import FutureServices +from app.modules.modules_factory.pending import PendingServices +from app.modules.modules_factory.pre_pending import PrePendingServices from app.modules.octant_rewards.service.calculated import CalculatedOctantRewards from app.modules.octant_rewards.service.finalized import FinalizedOctantRewards from app.modules.octant_rewards.service.pending import PendingOctantRewards -from app.modules.registry import get_services, register_services from app.modules.snapshots.pending.service.pre_pending import PrePendingSnapshots from app.modules.staking.proceeds.service.aggregated import AggregatedStakingProceeds from app.modules.staking.proceeds.service.contract_balance import ( @@ -26,8 +30,7 @@ def test_future_services_factory(): - register_services() - result = get_services(EpochState.FUTURE) + result = FutureServices.create() assert result.octant_rewards_service == CalculatedOctantRewards( staking_proceeds=EstimatedStakingProceeds(), @@ -36,8 +39,7 @@ def test_future_services_factory(): def test_current_services_factory(): - register_services() - result = get_services(EpochState.CURRENT) + result = CurrentServices.create() user_deposits = CalculatedUserDeposits(events_generator=DbAndGraphEventsGenerator()) assert result.user_deposits_service == user_deposits @@ -47,11 +49,8 @@ def test_current_services_factory(): ) -def test_pre_pending_services_factory_when_mainnet( - patch_compare_blockchain_types_for_mainnet, -): - register_services() - result = get_services(EpochState.PRE_PENDING) +def test_pre_pending_services_factory_when_mainnet(): + result = PrePendingServices.create(1) user_deposits = CalculatedUserDeposits(events_generator=DbAndGraphEventsGenerator()) octant_rewards = CalculatedOctantRewards( @@ -65,11 +64,8 @@ def test_pre_pending_services_factory_when_mainnet( ) -def test_pre_pending_services_factory_when_not_mainnet( - patch_compare_blockchain_types_for_not_mainnet, -): - register_services() - result = get_services(EpochState.PRE_PENDING) +def test_pre_pending_services_factory_when_not_mainnet(): + result = PrePendingServices.create(1337) user_deposits = CalculatedUserDeposits(events_generator=DbAndGraphEventsGenerator()) octant_rewards = CalculatedOctantRewards( @@ -84,8 +80,7 @@ def test_pre_pending_services_factory_when_not_mainnet( def test_pending_services_factory(): - register_services() - result = get_services(EpochState.PENDING) + result = PendingServices.create() events_based_patron_mode = EventsBasedUserPatronMode() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) @@ -105,8 +100,7 @@ def test_pending_services_factory(): def test_finalizing_services_factory(): - register_services() - result = get_services(EpochState.FINALIZING) + result = FinalizingServices.create() events_based_patron_mode = EventsBasedUserPatronMode() saved_user_allocations = SavedUserAllocations() @@ -126,8 +120,7 @@ def test_finalizing_services_factory(): def test_finalized_services_factory(): - register_services() - result = get_services(EpochState.FINALIZED) + result = FinalizedServices.create() events_based_patron_mode = EventsBasedUserPatronMode() saved_user_allocations = SavedUserAllocations() diff --git a/backend/tests/modules/staking/test_aggegated_staking_proceeds.py b/backend/tests/modules/staking/test_aggegated_staking_proceeds.py index ec5c4396e6..d881953a7c 100644 --- a/backend/tests/modules/staking/test_aggegated_staking_proceeds.py +++ b/backend/tests/modules/staking/test_aggegated_staking_proceeds.py @@ -9,17 +9,19 @@ def before(app): def test_aggregated_staking_proceeds( - patch_etherscan_transactions_api, patch_bitquery_get_blocks_rewards + patch_etherscan_transactions_api, + patch_bitquery_get_blocks_rewards, + patch_etherscan_get_block_api, ): """ Expected results for the test: MEV 66813166811131780 WITHDRAWALS 1498810000000000 - BLOCKS REWARD 77902794919848899 + BLOCKS REWARD 77902794919848900 """ - context = get_context(1) + context = get_context(1, with_block_range=True) service = AggregatedStakingProceeds() result = service.get_staking_proceeds(context) - assert result == 146214771_730980679 + assert result == 146214771_730980680