From 85591176b4f116e62b67a3032bedbdc4fffd093b Mon Sep 17 00:00:00 2001 From: Yoshihito Aso Date: Fri, 30 Jan 2026 17:01:21 +0900 Subject: [PATCH 1/2] refactor: Remove blockchain explorer --- Dockerfile | 2 +- ENV_LIST.md | 5 - README.md | 2 +- README_JA.md | 2 +- app/api/routers/bc_explorer.py | 395 ----------- app/config.py | 5 - app/main.py | 2 - app/model/db/__init__.py | 2 - app/model/db/idx_block_data.py | 64 -- app/model/db/idx_tx_data.py | 41 -- app/model/schema/__init__.py | 10 - app/model/schema/bc_explorer.py | 126 ---- batch/indexer_Block_Tx_Data.py | 219 ------- bin/healthcheck_indexer.sh | 4 - bin/run_indexer.sh | 4 - cmd/explorer/Makefile | 10 - cmd/explorer/README.md | 44 -- cmd/explorer/pyproject.toml | 23 - cmd/explorer/src/__init__.py | 18 - cmd/explorer/src/connector/__init__.py | 93 --- cmd/explorer/src/gui/__init__.py | 18 - cmd/explorer/src/gui/consts.py | 58 -- cmd/explorer/src/gui/error.py | 35 - cmd/explorer/src/gui/explorer.css | 110 ---- cmd/explorer/src/gui/explorer.py | 101 --- cmd/explorer/src/gui/rendarable/__init__.py | 18 - .../src/gui/rendarable/block_detail_info.py | 94 --- .../src/gui/rendarable/tx_detail_info.py | 68 -- cmd/explorer/src/gui/screen/__init__.py | 18 - cmd/explorer/src/gui/screen/base.py | 31 - cmd/explorer/src/gui/screen/block.py | 290 -------- cmd/explorer/src/gui/screen/traceback.py | 54 -- cmd/explorer/src/gui/screen/transaction.py | 126 ---- cmd/explorer/src/gui/styles.py | 28 - cmd/explorer/src/gui/widget/__init__.py | 18 - cmd/explorer/src/gui/widget/base.py | 38 -- .../src/gui/widget/block_detail_view.py | 61 -- .../src/gui/widget/block_list_table.py | 109 ---- .../src/gui/widget/block_list_view.py | 171 ----- cmd/explorer/src/gui/widget/choice.py | 56 -- cmd/explorer/src/gui/widget/menu.py | 66 -- cmd/explorer/src/gui/widget/query_panel.py | 291 --------- cmd/explorer/src/gui/widget/traceback.py | 54 -- cmd/explorer/src/gui/widget/tx_detail_view.py | 68 -- cmd/explorer/src/gui/widget/tx_list_table.py | 63 -- cmd/explorer/src/gui/widget/tx_list_view.py | 45 -- cmd/explorer/src/main.py | 43 -- cmd/explorer/src/utils/time.py | 78 --- cmd/explorer/uv.lock | 8 - docs/ibet_wallet_api.yaml | 617 +----------------- .../1cd2ee459858_v26_3_0_feature_1748.py | 180 +++++ pyproject.toml | 12 - tests/Dockerfile_unittest | 2 +- tests/app/node_info_GetBlockData_test.py | 193 ------ tests/app/node_info_GetTxData_test.py | 188 ------ tests/app/node_info_ListBlockData_test.py | 435 ------------ tests/app/node_info_ListTxData_test.py | 381 ----------- tests/batch/indexer_Block_Tx_Data_test.py | 356 ---------- uv.lock | 29 +- 59 files changed, 192 insertions(+), 5490 deletions(-) delete mode 100644 app/api/routers/bc_explorer.py delete mode 100644 app/model/db/idx_block_data.py delete mode 100644 app/model/db/idx_tx_data.py delete mode 100644 app/model/schema/bc_explorer.py delete mode 100644 batch/indexer_Block_Tx_Data.py delete mode 100644 cmd/explorer/Makefile delete mode 100644 cmd/explorer/README.md delete mode 100644 cmd/explorer/pyproject.toml delete mode 100644 cmd/explorer/src/__init__.py delete mode 100644 cmd/explorer/src/connector/__init__.py delete mode 100644 cmd/explorer/src/gui/__init__.py delete mode 100644 cmd/explorer/src/gui/consts.py delete mode 100644 cmd/explorer/src/gui/error.py delete mode 100644 cmd/explorer/src/gui/explorer.css delete mode 100644 cmd/explorer/src/gui/explorer.py delete mode 100644 cmd/explorer/src/gui/rendarable/__init__.py delete mode 100644 cmd/explorer/src/gui/rendarable/block_detail_info.py delete mode 100644 cmd/explorer/src/gui/rendarable/tx_detail_info.py delete mode 100644 cmd/explorer/src/gui/screen/__init__.py delete mode 100644 cmd/explorer/src/gui/screen/base.py delete mode 100644 cmd/explorer/src/gui/screen/block.py delete mode 100644 cmd/explorer/src/gui/screen/traceback.py delete mode 100644 cmd/explorer/src/gui/screen/transaction.py delete mode 100644 cmd/explorer/src/gui/styles.py delete mode 100644 cmd/explorer/src/gui/widget/__init__.py delete mode 100644 cmd/explorer/src/gui/widget/base.py delete mode 100644 cmd/explorer/src/gui/widget/block_detail_view.py delete mode 100644 cmd/explorer/src/gui/widget/block_list_table.py delete mode 100644 cmd/explorer/src/gui/widget/block_list_view.py delete mode 100644 cmd/explorer/src/gui/widget/choice.py delete mode 100644 cmd/explorer/src/gui/widget/menu.py delete mode 100644 cmd/explorer/src/gui/widget/query_panel.py delete mode 100644 cmd/explorer/src/gui/widget/traceback.py delete mode 100644 cmd/explorer/src/gui/widget/tx_detail_view.py delete mode 100644 cmd/explorer/src/gui/widget/tx_list_table.py delete mode 100644 cmd/explorer/src/gui/widget/tx_list_view.py delete mode 100644 cmd/explorer/src/main.py delete mode 100644 cmd/explorer/src/utils/time.py delete mode 100644 cmd/explorer/uv.lock create mode 100644 migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py delete mode 100644 tests/app/node_info_GetBlockData_test.py delete mode 100644 tests/app/node_info_GetTxData_test.py delete mode 100644 tests/app/node_info_ListBlockData_test.py delete mode 100644 tests/app/node_info_ListTxData_test.py delete mode 100644 tests/batch/indexer_Block_Tx_Data_test.py diff --git a/Dockerfile b/Dockerfile index c997788bce..9dab5f0b04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,7 +56,7 @@ RUN echo '. $HOME/.venv/bin/activate' >> ~apl/.bashrc COPY --chown=apl:apl . /app/ibet-Wallet-API RUN cd /app/ibet-Wallet-API \ && uv venv $UV_PROJECT_ENVIRONMENT \ - && uv sync --frozen --no-install-project --no-dev --extra ibet-explorer \ + && uv sync --frozen --no-install-project --no-dev \ && rm -f /app/ibet-Wallet-API/pyproject.toml \ && rm -f /app/ibet-Wallet-API/uv.lock \ && rm -rf /app/ibet-Wallet-API/tests/ diff --git a/ENV_LIST.md b/ENV_LIST.md index 651c3b04bf..74b41eb0e8 100644 --- a/ENV_LIST.md +++ b/ENV_LIST.md @@ -71,11 +71,6 @@ See [Gunicorn's official documentation](https://docs.gunicorn.org/en/stable/run. | IBET_COUPON_EXCHANGE_CONTRACT_ADDRESS | False | IbetExchange contract address for Coupon tokens | 0x0000000000000000000000000000000000000000 | -- | | EXCHANGE_NOTIFICATION_ENABLED | True* | Use of exchange-related notification (*Set only if you use IbetExchange) | 0 (not using) / 1 (using) | -- | -### Blockchain Explorer -| Variable Name | Required | Details | Example | Default | -|---------------------|----------|-----------------------------------------------------|---------------------------|---------| -| BC_EXPLORER_ENABLED | False | Parameter for starting the Blockchain Explorer | 0 (not using) / 1 (using) | 0 | - ### Email Common diff --git a/README.md b/README.md index 65630926ec..d52d1bc43d 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ $ uv venv Install python packages with: ```bash -$ uv sync --frozen --no-install-project --no-dev --extra ibet-explorer +$ uv sync --frozen --no-install-project --no-dev ``` ### Setting environment variables diff --git a/README_JA.md b/README_JA.md index 975ff27117..31b2cbfefc 100644 --- a/README_JA.md +++ b/README_JA.md @@ -59,7 +59,7 @@ $ uv venv 以下のコマンドで Python パッケージをインストールします。 ```bash -$ uv sync --frozen --no-install-project --no-dev --extra ibet-explorer +$ uv sync --frozen --no-install-project --no-dev ``` ### 環境変数の設定 diff --git a/app/api/routers/bc_explorer.py b/app/api/routers/bc_explorer.py deleted file mode 100644 index fd6c71ace4..0000000000 --- a/app/api/routers/bc_explorer.py +++ /dev/null @@ -1,395 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Annotated, Any, Dict, Sequence, Tuple - -from eth_utils import to_checksum_address -from fastapi import APIRouter, Path, Query -from sqlalchemy import and_, desc, func, select -from starlette.requests import Request -from web3.contract.contract import ContractFunction - -from app import config, log -from app.contracts import AsyncContract -from app.database import DBAsyncSession -from app.errors import DataNotExistsError, NotSupportedError, ResponseLimitExceededError -from app.model.db import ( - IDXBlockData, - IDXBlockDataBlockNumber, - IDXTokenListRegister, - IDXTxData, -) -from app.model.schema import ( - BlockDataListResponse, - BlockDataResponse, - ListBlockDataQuery, - ListTxDataQuery, - TxDataListResponse, - TxDataResponse, -) -from app.model.schema.base import GenericSuccessResponse, SuccessResponse -from app.utils.docs_utils import get_routers_responses -from app.utils.fastapi_utils import json_response - -LOG = log.get_logger() -BLOCK_RESPONSE_LIMIT = 1000 -TX_RESPONSE_LIMIT = 10000 - -router = APIRouter(prefix="/NodeInfo", tags=["node_info"]) - - -# ------------------------------ -# [BC-Explorer] List Block data -# ------------------------------ -@router.get( - "/BlockData", - summary="[ibet Blockchain Explorer] List block data", - operation_id="ListBlockData", - response_model=GenericSuccessResponse[BlockDataListResponse], - responses=get_routers_responses(NotSupportedError, ResponseLimitExceededError), -) -async def list_block_data( - async_session: DBAsyncSession, - req: Request, - request_query: Annotated[ListBlockDataQuery, Query()], -): - """ - Returns a list of block data within the specified block number range. - The maximum number of search results is 1000. - """ - if config.BC_EXPLORER_ENABLED is False: - raise NotSupportedError(method="GET", url=req.url.path) - - offset = request_query.offset - limit = request_query.limit - from_block_number = request_query.from_block_number - to_block_number = request_query.to_block_number - sort_order = request_query.sort_order # default: asc - - # NOTE: The more data, the slower the SELECT COUNT(1) query becomes. - # To get total number of block data, latest block number where block data synced is used here. - idx_block_data_block_number = ( - await async_session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ) - ).first() - if idx_block_data_block_number is None: - return json_response( - { - **SuccessResponse.default(), - "data": { - "result_set": { - "count": 0, - "offset": offset, - "limit": limit, - "total": 0, - }, - "block_data": [], - }, - } - ) - - total = idx_block_data_block_number.latest_block_number + 1 - - stmt = select(IDXBlockData) - - # Search Filter - if from_block_number is not None and to_block_number is not None: - stmt = stmt.where( - and_( - IDXBlockData.number >= from_block_number, - IDXBlockData.number <= to_block_number, - ) - ) - elif from_block_number is not None: - stmt = stmt.where(IDXBlockData.number >= from_block_number) - elif to_block_number is not None: - stmt = stmt.where(IDXBlockData.number <= to_block_number) - - count = await async_session.scalar( - stmt.with_only_columns(func.count()).select_from(IDXBlockData).order_by(None) - ) - - # Sort - if sort_order == 0: - stmt = stmt.order_by(IDXBlockData.number) - else: - stmt = stmt.order_by(desc(IDXBlockData.number)) - - if ( - await async_session.scalar( - stmt.with_only_columns(func.count()) - .select_from(IDXBlockData) - .order_by(None) - ) - > BLOCK_RESPONSE_LIMIT - ): - raise ResponseLimitExceededError("Search results exceed the limit") - - # Pagination - if limit is not None: - stmt = stmt.limit(limit) - if offset is not None: - stmt = stmt.offset(offset) - - block_data_tmp: Sequence[IDXBlockData] = (await async_session.scalars(stmt)).all() - - block_data = [ - { - "number": bd.number, - "hash": bd.hash, - "transactions": bd.transactions, - "timestamp": bd.timestamp, - "gas_limit": bd.gas_limit, - "gas_used": bd.gas_used, - "size": bd.size, - } - for bd in block_data_tmp - ] - data = { - "result_set": { - "count": count, - "offset": offset, - "limit": limit, - "total": total, - }, - "block_data": block_data, - } - - return json_response({**SuccessResponse.default(), "data": data}) - - -# ------------------------------ -# [BC-Explorer] Retrieve Block data -# ------------------------------ -@router.get( - "/BlockData/{block_number}", - summary="[ibet Blockchain Explorer] Retrieve block data", - operation_id="GetBlockData", - response_model=GenericSuccessResponse[BlockDataResponse], - responses=get_routers_responses(NotSupportedError, DataNotExistsError), -) -async def get_block_data( - async_session: DBAsyncSession, - req: Request, - block_number: Annotated[int, Path(description="Block number", ge=0)], -): - """ - Returns block data in the specified block number. - """ - if config.BC_EXPLORER_ENABLED is False: - raise NotSupportedError(method="GET", url=req.url.path) - - block_data = ( - await async_session.scalars( - select(IDXBlockData).where(IDXBlockData.number == block_number).limit(1) - ) - ).first() - if block_data is None: - raise DataNotExistsError - - return json_response( - { - **SuccessResponse.default(), - "data": { - "number": block_data.number, - "parent_hash": block_data.parent_hash, - "sha3_uncles": block_data.sha3_uncles, - "miner": block_data.miner, - "state_root": block_data.state_root, - "transactions_root": block_data.transactions_root, - "receipts_root": block_data.receipts_root, - "logs_bloom": block_data.logs_bloom, - "difficulty": block_data.difficulty, - "gas_limit": block_data.gas_limit, - "gas_used": block_data.gas_used, - "timestamp": block_data.timestamp, - "proof_of_authority_data": block_data.proof_of_authority_data, - "mix_hash": block_data.mix_hash, - "nonce": block_data.nonce, - "hash": block_data.hash, - "size": block_data.size, - "transactions": block_data.transactions, - }, - } - ) - - -# ------------------------------ -# [BC-Explorer] List Tx data -# ------------------------------ -@router.get( - "/TxData", - summary="[ibet Blockchain Explorer] List tx data", - operation_id="ListTxData", - response_model=GenericSuccessResponse[TxDataListResponse], - responses=get_routers_responses(NotSupportedError, ResponseLimitExceededError), -) -async def list_tx_data( - async_session: DBAsyncSession, - req: Request, - request_query: Annotated[ListTxDataQuery, Query()], -): - """ - Returns a list of transactions by various search parameters. - The maximum number of search results is 10000. - """ - if config.BC_EXPLORER_ENABLED is False: - raise NotSupportedError(method="GET", url=req.url.path) - - offset = request_query.offset - limit = request_query.limit - block_number = request_query.block_number - from_address = request_query.from_address - to_address = request_query.to_address - - stmt = select(IDXTxData) - total = await async_session.scalar(select(func.count(IDXTxData.hash))) - - # Search Filter - if block_number is not None: - stmt = stmt.where(IDXTxData.block_number == block_number) - if from_address is not None: - stmt = stmt.where(IDXTxData.from_address == to_checksum_address(from_address)) - if to_address is not None: - stmt = stmt.where(IDXTxData.to_address == to_checksum_address(to_address)) - - count = await async_session.scalar( - stmt.with_only_columns(func.count()).select_from(IDXTxData).order_by(None) - ) - - # Sort - stmt = stmt.order_by(desc(IDXTxData.created)) - - if ( - await async_session.scalar( - stmt.with_only_columns(func.count()).select_from(IDXTxData).order_by(None) - ) - > TX_RESPONSE_LIMIT - ): - raise ResponseLimitExceededError("Search results exceed the limit") - - # Pagination - if limit is not None: - stmt = stmt.limit(limit) - if offset is not None: - stmt = stmt.offset(offset) - - tx_data_tmp: Sequence[IDXTxData] = (await async_session.scalars(stmt)).all() - - tx_data = [ - { - "hash": txd.hash, - "block_hash": txd.block_hash, - "block_number": txd.block_number, - "transaction_index": txd.transaction_index, - "from_address": txd.from_address, - "to_address": txd.to_address, - } - for txd in tx_data_tmp - ] - data = { - "result_set": { - "count": count, - "offset": offset, - "limit": limit, - "total": total, - }, - "tx_data": tx_data, - } - return json_response({**SuccessResponse.default(), "data": data}) - - -# ------------------------------ -# [BC-Explorer] Retrieve Tx data -# ------------------------------ -@router.get( - "/TxData/{hash}", - summary="[ibet Blockchain Explorer] Retrieve transaction data", - operation_id="GetTxData", - response_model=GenericSuccessResponse[TxDataResponse], - responses=get_routers_responses(NotSupportedError, DataNotExistsError), -) -async def get_tx_data( - async_session: DBAsyncSession, - req: Request, - hash: Annotated[str, Path(description="Transaction hash")], -): - """ - Searching for the transaction by transaction hash - """ - if config.BC_EXPLORER_ENABLED is False: - raise NotSupportedError(method="GET", url=req.url.path) - - # Search tx data - tx_data = ( - await async_session.scalars( - select(IDXTxData).where(IDXTxData.hash == hash).limit(1) - ) - ).first() - if tx_data is None: - raise DataNotExistsError - - # Decode contract input parameters - contract_name: str | None = None - contract_function: str | None = None - contract_parameters: dict | None = None - token_contract = ( - await async_session.scalars( - select(IDXTokenListRegister) - .where(IDXTokenListRegister.token_address == tx_data.to_address) - .limit(1) - ) - ).first() - if token_contract is not None: - contract_name = token_contract.token_template - try: - contract = AsyncContract.get_contract( - contract_name=contract_name, address=tx_data.to_address - ) - decoded_input: Tuple["ContractFunction", Dict[str, Any]] = ( - contract.decode_function_input(tx_data.input) - ) - contract_function = decoded_input[0].fn_name - contract_parameters = decoded_input[1] - except FileNotFoundError: - pass - - return json_response( - { - **SuccessResponse.default(), - "data": { - "hash": tx_data.hash, - "block_hash": tx_data.block_hash, - "block_number": tx_data.block_number, - "transaction_index": tx_data.transaction_index, - "from_address": tx_data.from_address, - "to_address": tx_data.to_address, - "contract_name": contract_name, - "contract_function": contract_function, - "contract_parameters": contract_parameters, - "gas": tx_data.gas, - "gas_price": tx_data.gas_price, - "value": tx_data.value, - "nonce": tx_data.nonce, - }, - } - ) diff --git a/app/config.py b/app/config.py index c63235a299..daeab53a82 100644 --- a/app/config.py +++ b/app/config.py @@ -274,11 +274,6 @@ os.environ.get("TOKEN_SHORT_TERM_FETCH_INTERVAL_MSEC") or 100 ) -#################################################### -# Blockchain explorer settings -#################################################### -BC_EXPLORER_ENABLED = True if os.environ.get("BC_EXPLORER_ENABLED") == "1" else False - #################################################### # Email settings #################################################### diff --git a/app/main.py b/app/main.py index 4f470a2e80..ffa539b858 100644 --- a/app/main.py +++ b/app/main.py @@ -35,7 +35,6 @@ from app import log from app.api.routers import ( admin as routers_admin, - bc_explorer as routers_bc_explorer, company_info as routers_company_info, contract_abi as routers_contract_abi, dex_market as routers_dex_market, @@ -154,7 +153,6 @@ def root() -> dict[str, str]: app.include_router(routers_company_info.router) app.include_router(routers_admin.router) app.include_router(routers_node_info.router) -app.include_router(routers_bc_explorer.router) app.include_router(routers_contract_abi.router) app.include_router(routers_user_info.router) app.include_router(routers_eth.router) diff --git a/app/model/db/__init__.py b/app/model/db/__init__.py index 05240b1aee..341533cd15 100644 --- a/app/model/db/__init__.py +++ b/app/model/db/__init__.py @@ -21,7 +21,6 @@ from .company import Company from .executable_contract import ExecutableContract from .idx_agreement import AgreementStatus, IDXAgreement -from .idx_block_data import IDXBlockData, IDXBlockDataBlockNumber from .idx_consume_coupon import IDXConsumeCoupon from .idx_lock_unlock import IDXLock, IDXUnlock, LockDataMessage, UnlockDataMessage from .idx_order import IDXOrder @@ -49,7 +48,6 @@ TransferDataMessage, ) from .idx_transfer_approval import IDXTransferApproval, IDXTransferApprovalBlockNumber -from .idx_tx_data import IDXTxData from .listing import Listing from .messaging import ChatWebhook, Mail from .node import Node diff --git a/app/model/db/idx_block_data.py b/app/model/db/idx_block_data.py deleted file mode 100644 index 1ca6ee478c..0000000000 --- a/app/model/db/idx_block_data.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from sqlalchemy import JSON, BigInteger, Integer, String, Text -from sqlalchemy.orm import Mapped, mapped_column - -from app.model.db.base import Base - - -class IDXBlockData(Base): - """Block data (INDEX)""" - - __tablename__ = "block_data" - - # Header data - number: Mapped[int] = mapped_column( - BigInteger, primary_key=True, autoincrement=False - ) - parent_hash: Mapped[str] = mapped_column(String(66), nullable=False) - sha3_uncles: Mapped[str | None] = mapped_column(String(66)) - miner: Mapped[str | None] = mapped_column(String(42)) - state_root: Mapped[str | None] = mapped_column(String(66)) - transactions_root: Mapped[str | None] = mapped_column(String(66)) - receipts_root: Mapped[str | None] = mapped_column(String(66)) - logs_bloom: Mapped[str | None] = mapped_column(String(514)) - difficulty: Mapped[int | None] = mapped_column(BigInteger) - gas_limit: Mapped[int | None] = mapped_column(Integer) - gas_used: Mapped[int | None] = mapped_column(Integer) - timestamp: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - proof_of_authority_data: Mapped[str | None] = mapped_column(Text) - mix_hash: Mapped[str | None] = mapped_column(String(66)) - nonce: Mapped[str | None] = mapped_column(String(18)) - - # Other data - hash: Mapped[str] = mapped_column(String(66), nullable=False, index=True) - size: Mapped[int | None] = mapped_column(Integer) - transactions: Mapped[list[str] | None] = mapped_column(JSON) - - -class IDXBlockDataBlockNumber(Base): - """Synchronized blockNumber of IDXBlockData""" - - __tablename__ = "idx_block_data_block_number" - - # Chain id - chain_id: Mapped[str] = mapped_column(String(10), primary_key=True) - # Latest blockNumber - latest_block_number: Mapped[int | None] = mapped_column(BigInteger) diff --git a/app/model/db/idx_tx_data.py b/app/model/db/idx_tx_data.py deleted file mode 100644 index 8886c36617..0000000000 --- a/app/model/db/idx_tx_data.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from sqlalchemy import BigInteger, Integer, String, Text -from sqlalchemy.orm import Mapped, mapped_column - -from app.model.db.base import Base - - -class IDXTxData(Base): - """Transaction data (INDEX)""" - - __tablename__ = "tx_data" - - hash: Mapped[str] = mapped_column(String(66), primary_key=True) - block_hash: Mapped[str | None] = mapped_column(String(66)) - block_number: Mapped[int | None] = mapped_column(BigInteger, index=True) - transaction_index: Mapped[int | None] = mapped_column(Integer) - from_address: Mapped[str | None] = mapped_column(String(42), index=True) - to_address: Mapped[str | None] = mapped_column(String(42), index=True) - input: Mapped[str | None] = mapped_column(Text) - gas: Mapped[int | None] = mapped_column(Integer) - gas_price: Mapped[int | None] = mapped_column(BigInteger) - value: Mapped[int | None] = mapped_column(BigInteger) - nonce: Mapped[int | None] = mapped_column(Integer) diff --git a/app/model/schema/__init__.py b/app/model/schema/__init__.py index c3913ad82e..ac4d6f316e 100644 --- a/app/model/schema/__init__.py +++ b/app/model/schema/__init__.py @@ -27,16 +27,6 @@ UpdateAdminTokenRequest, ) from .base import TokenType -from .bc_explorer import ( - BlockDataDetail, - BlockDataListResponse, - BlockDataResponse, - ListBlockDataQuery, - ListTxDataQuery, - TxDataDetail, - TxDataListResponse, - TxDataResponse, -) from .company_info import ( ListAllCompaniesQuery, ListAllCompaniesResponse, diff --git a/app/model/schema/bc_explorer.py b/app/model/schema/bc_explorer.py deleted file mode 100644 index d69337afc2..0000000000 --- a/app/model/schema/bc_explorer.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Optional - -from pydantic import BaseModel, Field, NonNegativeInt, RootModel - -from app.model.schema.base import ( - BasePaginationQuery, - ResultSet, - SortOrder, -) -from app.model.type import EthereumAddress - - -############################ -# COMMON -############################ -class BlockData(BaseModel): - number: NonNegativeInt = Field(description="Block number") - hash: str = Field(description="Block hash") - transactions: list[str] = Field(description="Transaction list") - timestamp: int - gas_limit: int - gas_used: int - size: NonNegativeInt - - -class BlockDataDetail(BaseModel): - number: NonNegativeInt = Field(description="Block number") - parent_hash: str - sha3_uncles: str - miner: str - state_root: str - transactions_root: str - receipts_root: str - logs_bloom: str - difficulty: int - gas_limit: int - gas_used: int - timestamp: int - proof_of_authority_data: str - mix_hash: str - nonce: str - hash: str = Field(description="Block hash") - size: NonNegativeInt - transactions: list[str] = Field(description="Transaction list") - - -class TxData(BaseModel): - hash: str = Field(description="Transaction hash") - block_hash: str - block_number: NonNegativeInt - transaction_index: NonNegativeInt - from_address: EthereumAddress - to_address: Optional[EthereumAddress] - - -class TxDataDetail(BaseModel): - hash: str = Field(description="Transaction hash") - block_hash: str - block_number: NonNegativeInt - transaction_index: NonNegativeInt - from_address: EthereumAddress - to_address: Optional[EthereumAddress] - contract_name: Optional[str] - contract_function: Optional[str] - contract_parameters: Optional[dict[str, object]] - gas: NonNegativeInt - gas_price: NonNegativeInt - value: NonNegativeInt - nonce: NonNegativeInt - - -############################ -# REQUEST -############################ -class ListBlockDataQuery(BasePaginationQuery): - from_block_number: Optional[NonNegativeInt] = Field(None) - to_block_number: Optional[NonNegativeInt] = Field(None) - sort_order: Optional[SortOrder] = Field( - SortOrder.ASC, description=SortOrder.__doc__ - ) - - -class ListTxDataQuery(BasePaginationQuery): - block_number: Optional[NonNegativeInt] = Field(None, description="block number") - from_address: Optional[EthereumAddress] = Field(None, description="tx from") - to_address: Optional[EthereumAddress] = Field(None, description="tx to") - - -############################ -# RESPONSE -############################ -class BlockDataResponse(RootModel[BlockDataDetail]): - pass - - -class BlockDataListResponse(BaseModel): - result_set: ResultSet - block_data: list[BlockData] - - -class TxDataResponse(RootModel[TxDataDetail]): - pass - - -class TxDataListResponse(BaseModel): - result_set: ResultSet - tx_data: list[TxData] diff --git a/batch/indexer_Block_Tx_Data.py b/batch/indexer_Block_Tx_Data.py deleted file mode 100644 index 9582569341..0000000000 --- a/batch/indexer_Block_Tx_Data.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import asyncio -import sys -from collections.abc import Sequence -from typing import cast - -from eth_utils.address import to_checksum_address -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.ext.asyncio import AsyncSession -from web3.types import BlockData, TxData - -from app.config import WEB3_CHAINID -from app.database import BatchAsyncSessionLocal -from app.errors import ServiceUnavailable -from app.model.db import IDXBlockData, IDXBlockDataBlockNumber, IDXTxData -from app.utils.web3_utils import AsyncWeb3Wrapper -from batch import free_malloc, log - -process_name = "INDEXER-BLOCK_TX_DATA" -LOG = log.get_logger(process_name=process_name) - -async_web3 = AsyncWeb3Wrapper() - - -class Processor: - """Processor for indexing Block and Transaction data""" - - @staticmethod - def __get_db_session(): - return BatchAsyncSessionLocal() - - async def process(self): - local_session = self.__get_db_session() - try: - latest_block = int(await async_web3.eth.block_number) - from_block = (await self.__get_indexed_block_number(local_session)) + 1 - - if from_block > latest_block: - LOG.info("Skip process: from_block > latest_block") - return - - LOG.info("Syncing from={}, to={}".format(from_block, latest_block)) - for block_number in range(from_block, latest_block + 1): - block_data: BlockData = await async_web3.eth.get_block( - block_number, full_transactions=True - ) - assert "number" in block_data - assert "parentHash" in block_data - assert "timestamp" in block_data - assert "hash" in block_data - - # Synchronize block data - block_model = IDXBlockData() - block_model.number = block_data["number"] - block_model.parent_hash = block_data["parentHash"].to_0x_hex() - block_model.sha3_uncles = ( - block_data["sha3Uncles"].to_0x_hex() - if "sha3Uncles" in block_data - else None - ) - block_model.miner = block_data.get("miner") - block_model.state_root = ( - block_data["stateRoot"].to_0x_hex() - if "stateRoot" in block_data - else None - ) - block_model.transactions_root = ( - block_data["transactionsRoot"].to_0x_hex() - if "transactionsRoot" in block_data - else None - ) - block_model.receipts_root = ( - block_data["receiptsRoot"].to_0x_hex() - if "receiptsRoot" in block_data - else None - ) - block_model.logs_bloom = ( - block_data["logsBloom"].to_0x_hex() - if "logsBloom" in block_data - else None - ) - block_model.difficulty = block_data.get("difficulty") - block_model.gas_limit = block_data.get("gasLimit") - block_model.gas_used = block_data.get("gasUsed") - block_model.timestamp = block_data["timestamp"] - block_model.proof_of_authority_data = ( - block_data["proofOfAuthorityData"].to_0x_hex() - if "proofOfAuthorityData" in block_data - else None - ) - block_model.mix_hash = ( - block_data["mixHash"].to_0x_hex() - if "mixHash" in block_data - else None - ) - block_model.nonce = ( - block_data["nonce"].to_0x_hex() if "nonce" in block_data else None - ) - block_model.hash = block_data["hash"].to_0x_hex() - block_model.size = block_data.get("size") - - transactions = cast( - Sequence[TxData], block_data.get("transactions", []) - ) - transaction_hash_list: list[str] = [] - - for transaction in transactions: - assert "hash" in transaction - # Synchronize tx data - tx_model = IDXTxData() - tx_model.hash = transaction["hash"].to_0x_hex() - tx_model.block_hash = ( - transaction["blockHash"].to_0x_hex() - if "blockHash" in transaction - else None - ) - tx_model.block_number = transaction.get("blockNumber") - tx_model.transaction_index = transaction.get("transactionIndex") - tx_model.from_address = ( - to_checksum_address(transaction["from"]) - if "from" in transaction - else None - ) - tx_model.to_address = ( - to_checksum_address(transaction["to"]) - if "to" in transaction and transaction["to"] is not None # type: ignore to_address can be None - else None - ) - tx_model.input = ( - transaction["input"].to_0x_hex() - if "input" in transaction - else None - ) - tx_model.gas = transaction.get("gas") - tx_model.gas_price = transaction.get("gasPrice") - tx_model.value = transaction.get("value") - tx_model.nonce = transaction.get("nonce") - local_session.add(tx_model) - - transaction_hash_list.append(transaction["hash"].to_0x_hex()) - - block_model.transactions = transaction_hash_list - local_session.add(block_model) - - await self.__set_indexed_block_number(local_session, block_number) - - await local_session.commit() - except Exception: - await local_session.rollback() - raise - finally: - await local_session.close() - LOG.info("Sync job has been completed") - - @staticmethod - async def __get_indexed_block_number(db_session: AsyncSession) -> int: - indexed_block_number = ( - await db_session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == WEB3_CHAINID) - .limit(1) - ) - ).first() - if indexed_block_number is None: - return -1 - else: - assert indexed_block_number.latest_block_number is not None - return indexed_block_number.latest_block_number - - @staticmethod - async def __set_indexed_block_number(db_session: AsyncSession, block_number: int): - indexed_block_number = IDXBlockDataBlockNumber() - indexed_block_number.chain_id = WEB3_CHAINID - indexed_block_number.latest_block_number = block_number - await db_session.merge(indexed_block_number) - - -async def main(): - LOG.info("Service started successfully") - processor = Processor() - - while True: - try: - await processor.process() - except ServiceUnavailable: - LOG.notice("An external service was unavailable") - except SQLAlchemyError as sa_err: - LOG.error(f"A database error has occurred: code={sa_err.code}\n{sa_err}") - except Exception: - LOG.exception("An exception occurred during event synchronization") - - await asyncio.sleep(5) - free_malloc() - - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - sys.exit(1) diff --git a/bin/healthcheck_indexer.sh b/bin/healthcheck_indexer.sh index 00f8dfc2c2..ad7b1a1abe 100755 --- a/bin/healthcheck_indexer.sh +++ b/bin/healthcheck_indexer.sh @@ -53,10 +53,6 @@ if [ -z $TOKEN_CACHE ] || [ $TOKEN_CACHE -ne 0 ]; then PROC_LIST="${PROC_LIST} batch/indexer_Token_Detail_ShortTerm.py" fi -if [[ $BC_EXPLORER_ENABLED = 1 ]]; then - PROC_LIST="${PROC_LIST} batch/indexer_Block_Tx_Data.py" -fi - for i in ${PROC_LIST}; do # shellcheck disable=SC2009 ps -ef | grep -v grep | grep "$i" diff --git a/bin/run_indexer.sh b/bin/run_indexer.sh index 661f7a14ae..7faa07a2a3 100755 --- a/bin/run_indexer.sh +++ b/bin/run_indexer.sh @@ -105,8 +105,4 @@ if [ -z $TOKEN_CACHE ] || [ $TOKEN_CACHE -ne 0 ]; then python batch/indexer_Token_Detail_ShortTerm.py & fi -if [[ $BC_EXPLORER_ENABLED = 1 ]]; then - python batch/indexer_Block_Tx_Data.py & -fi - tail -f /dev/null diff --git a/cmd/explorer/Makefile b/cmd/explorer/Makefile deleted file mode 100644 index 93d8b41343..0000000000 --- a/cmd/explorer/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -.PHONY: console dev run - -console: - uv run textual console - -dev: - TEXTUAL=devtools uv run python src/main.py - -run: - uv run python src/main.py \ No newline at end of file diff --git a/cmd/explorer/README.md b/cmd/explorer/README.md deleted file mode 100644 index 18930c8038..0000000000 --- a/cmd/explorer/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# ibet blockchain explorer - -## Run - -### with container - -```bash -> docker exec -it -e "TERM=xterm-256color" ibet-wallet-api bash --login -> apl@2e5a80e06fcb:/$ ibet-explorer - - Usage: ibet-explorer [OPTIONS] - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ --url TEXT ibet-Wallet-API server URL to connect [default: http://localhost:5000] │ -│ --lot-size INTEGER Lot size to fetch Block Data list [default: 100] │ -│ --install-completion [bash|zsh|fish|powershell|pwsh] Install completion for the specified shell. [default: None] │ -│ --show-completion [bash|zsh|fish|powershell|pwsh] Show completion for the specified shell, to copy it or customize the installation. [default: None] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ -``` - -- **URL**: ibet-Wallet-API URL. -- You can run this on pythonic way in local. - -### Poetry -```bash -> poetry install -> poetry run python src/main.py --url http://localhost:5000 -``` - -### Pip -```bash -> pip install -e ./ -> python src/main.py --url http://localhost:5000 -``` - -## Screenshots 👀 - -![query-setting](https://user-images.githubusercontent.com/15183665/221606898-6795e176-b286-42d9-bc81-6b73117cf978.png) - -![block](https://user-images.githubusercontent.com/15183665/221606911-fbb9b9ba-97f4-4eb8-9f3e-c8bffc7b18ae.png) - -![transaction](https://user-images.githubusercontent.com/15183665/218406277-05eaa4c9-9433-42a8-8cc4-08d83a003f64.png) - diff --git a/cmd/explorer/pyproject.toml b/cmd/explorer/pyproject.toml deleted file mode 100644 index 9b1b963dfa..0000000000 --- a/cmd/explorer/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[project] -name = "ibet-wallet-api-explorer" -version = "0.1.0" -description = "ibet-Wallet-API Terminal UI for Block Chain Explorer" -authors = [ - {name = "BOOSTRY Co., Ltd.", email = "dev@boostry.co.jp"}, -] -readme = "README.md" -requires-python = "==3.13.11" -dependencies = [] - -[project.scripts] -ibet-explorer = "main:app" - -[tool.mypy] -python_version = "3.13" -no_strict_optional = true -ignore_missing_imports = true -check_untyped_defs = true - -[tool.setuptools.packages.find] -where = ["src/"] -include = ["*"] diff --git a/cmd/explorer/src/__init__.py b/cmd/explorer/src/__init__.py deleted file mode 100644 index 5ebbd9417f..0000000000 --- a/cmd/explorer/src/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" diff --git a/cmd/explorer/src/connector/__init__.py b/cmd/explorer/src/connector/__init__.py deleted file mode 100644 index fa4843d2fa..0000000000 --- a/cmd/explorer/src/connector/__init__.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Any - -from aiohttp import ClientSession -from cache import AsyncTTL - -from app.model.schema import ( - BlockDataDetail, - BlockDataListResponse, - GetBlockSyncStatusResponse, - ListBlockDataQuery, - ListTxDataQuery, - TxDataDetail, - TxDataListResponse, -) - - -class ApiNotEnabledException(Exception): - pass - - -async def health_check(url: str, session: ClientSession) -> None: - async with session.get(url=f"{url}/") as resp: - await resp.json() - - -@AsyncTTL(time_to_live=10, skip_args=1) -async def get_node_info(session: ClientSession, url: str) -> GetBlockSyncStatusResponse: - async with session.get(url=f"{url}/NodeInfo/BlockSyncStatus") as resp: - data = await resp.json() - return GetBlockSyncStatusResponse.model_validate(data.get("data")) - - -def dict_factory(x: list[tuple[str, Any]]): - return {k: v for (k, v) in x if v is not None} - - -@AsyncTTL(time_to_live=3600, skip_args=1) -async def list_block_data( - session: ClientSession, url: str, query: ListBlockDataQuery -) -> BlockDataListResponse: - async with session.get( - url=f"{url}/NodeInfo/BlockData", params=query.model_dump_json() - ) as resp: - data = await resp.json() - if resp.status == 404: - raise ApiNotEnabledException(data) - return BlockDataListResponse.model_validate(data.get("data")) - - -@AsyncTTL(time_to_live=3600, skip_args=1) -async def get_block_data( - session: ClientSession, url: str, block_number: int -) -> BlockDataDetail: - async with session.get(url=f"{url}/NodeInfo/BlockData/{block_number}") as resp: - data = await resp.json() - return BlockDataDetail.model_validate(data.get("data")) - - -@AsyncTTL(time_to_live=3600, skip_args=1) -async def list_tx_data( - session: ClientSession, url: str, query: ListTxDataQuery -) -> TxDataListResponse: - async with session.get( - url=f"{url}/NodeInfo/TxData", params=query.model_dump_json() - ) as resp: - data = await resp.json() - return TxDataListResponse.model_validate(data.get("data")) - - -@AsyncTTL(time_to_live=3600, skip_args=1) -async def get_tx_data(session: ClientSession, url: str, tx_hash: str) -> TxDataDetail: - async with session.get(url=f"{url}/NodeInfo/TxData/{tx_hash}") as resp: - data = await resp.json() - return TxDataDetail.model_validate(data.get("data")) diff --git a/cmd/explorer/src/gui/__init__.py b/cmd/explorer/src/gui/__init__.py deleted file mode 100644 index 5ebbd9417f..0000000000 --- a/cmd/explorer/src/gui/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" diff --git a/cmd/explorer/src/gui/consts.py b/cmd/explorer/src/gui/consts.py deleted file mode 100644 index 5cfef5a1ad..0000000000 --- a/cmd/explorer/src/gui/consts.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from enum import StrEnum - -UP = "\u2191" -DOWN = "\u2193" -LEFT = "\u2190" -RIGHT = "\u2192" -RIGHT_TRIANGLE = "\u25b6" -BIG_RIGHT_TRIANGLE = "\ue0b0" -DOWN_TRIANGLE = "\u25bc" - -THINKING_FACE = ":thinking_face:" -FIRE = ":fire:" -INFO = "[blue]:information:[/]" - - -class ID(StrEnum): - BLOCK_CONNECTED = "block_connected" - BLOCK_CURRENT_BLOCK_NUMBER = "block_current_block_number" - BLOCK_IS_SYNCED = "block_is_synced" - BLOCK_NOTION = "block_notion" - BLOCK_SCREEN_HEADER = "block_screen_header" - - BLOCK_LIST_FILTER = "block_list_filter" - BLOCK_LIST_LOADED_TIME = "block_list_loaded_time" - BLOCK_LIST_LOADING = "block_list_loading" - BLOCK_LIST_DESCRIPTION = "block_list_description" - BLOCK_LIST_TABLE = "block_list_table" - - TX_SELECTED_BLOCK_NUMBER = "tx_selected_block_number" - - MENU = "menu" - MENU_CANCEL = "menu_cancel" - MENU_SHOW_TX = "menu_show_tx" - - QUERY_PANEL = "query_panel" - QUERY_PANEL_FROM_BLOCK_INPUT = "query_panel_from_block_input" - QUERY_PANEL_TO_BLOCK_INPUT = "query_panel_to_block_input" - QUERY_PANEL_SORT_ORDER_CHOICE = "query_panel_sort_order_choice" - QUERY_PANEL_ENTER = "query_panel_enter" diff --git a/cmd/explorer/src/gui/error.py b/cmd/explorer/src/gui/error.py deleted file mode 100644 index 83929c9416..0000000000 --- a/cmd/explorer/src/gui/error.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Any - -from textual.message import Message - - -class Error(Message): - """ - A message sent when there was an error encoding the content. - """ - - def __init__(self, error: Exception, *args: Any, **kwargs: Any) -> None: - """ - Initialise the error message. - """ - super().__init__(*args, **kwargs) - self.error = error diff --git a/cmd/explorer/src/gui/explorer.css b/cmd/explorer/src/gui/explorer.css deleted file mode 100644 index cecd0927e3..0000000000 --- a/cmd/explorer/src/gui/explorer.css +++ /dev/null @@ -1,110 +0,0 @@ -/***************************** - Screens - *****************************/ -BlockScreen { - layer: 0; - height: 100%; - layers: one two three; - align: center middle; -} -BlockScreen.menu { - align: left top; -} -TransactionScreen { - layer: 0; -} -TracebackScreen { - layer: 0; -} - -/***************************** - Widgets - *****************************/ -Menu { - visibility: hidden; - height: auto; - width: auto; - layer: two; - opacity: 0; -} - -DataTable { - width: 100%; - height: 100%; - padding: 0; - border: $surface -} - -DataTable:focus { - border: heavy $accent-darken-3; -} - -QuerySetting { - layer: three; - visibility: hidden; - margin-top: 3; - width: auto; - height: auto; - align: center middle; - content-align: center middle; - text-align: center; - border: heavy $accent-darken-3; -} - -QuerySetting.visible { - align: center middle; -} - -/***************************** - Class&Id - *****************************/ - -.column { - width: 1fr; -} -.visible { - visibility: visible; - opacity: 100; -} -.column_auto { - width: auto; -} -#block_screen_header { - height: 1; - background: $accent-darken-3; -} -#block_list_description { - dock: top; - height: auto; -} -#block_list_table { - -} -#query_panel_from_block_input { - width: 50; - color: grey; -} -#query_panel_to_block_input { - width: 50; -} -#query_panel_sort_order_choice { - align: center middle; - width: 50; - height: 3; -} -#query_panel_enter { - width: 50; - height: 3; -} -#tx_list_header { - height: 1; - background: $accent-darken-3; -} -.menubutton { - color: $text; - width: 30; - height: 3; - margin: 0; - padding: 0; - border: heavy $accent-darken-3; -} diff --git a/cmd/explorer/src/gui/explorer.py b/cmd/explorer/src/gui/explorer.py deleted file mode 100644 index 60f0afed25..0000000000 --- a/cmd/explorer/src/gui/explorer.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import os - -from connector import ApiNotEnabledException -from gui.error import Error -from gui.screen.block import BlockScreen -from gui.screen.traceback import TracebackScreen -from gui.screen.transaction import TransactionScreen -from pydantic import ValidationError -from textual.app import App, ReturnType -from textual.binding import Binding - -from app.model.schema import ListBlockDataQuery, ListTxDataQuery - - -class AppState: - tx_list_query: ListTxDataQuery | None = None - block_list_query: ListBlockDataQuery | None = None - current_block_number: int | None = None - error: Exception | None = None - - -class ExplorerApp(App): - """A Textual app to explorer ibet-Network.""" - - # Base App Setting - BINDINGS = [Binding("ctrl+c", "quit", "Quit")] - CSS_PATH = f"{os.path.dirname(os.path.abspath(__file__))}/explorer.css" - SCREENS = { - "transaction_screen": TransactionScreen, - "traceback_screen": TracebackScreen, - } - - # Injectable App Setting - url: str - lot_size: int - - # App State - state: AppState = AppState() - - # Run App - async def run_async( - self, - *, - url: str = "http://localhost:5000", - lot_size: int = 30, - headless: bool = False, - size: tuple[int, int] | None = None, - auto_pilot: None = None, - ) -> ReturnType | None: - self.url = url - self.lot_size = lot_size - return await super().run_async( - headless=headless, size=size, auto_pilot=auto_pilot - ) - - ################################################## - # Event - ################################################## - - def on_mount(self): - """ - Occurs when Self is mounted - """ - self.push_screen(BlockScreen(name="block_screen")) - - def on_error(self, event: Error) -> None: - if isinstance(event.error, ApiNotEnabledException): - raise event.error from None - if isinstance(event.error, ValidationError): - raise ValueError(event.error.json()) from None - self.state.error = event.error - self.push_screen("traceback_screen") - - ################################################## - # Key binding - ################################################## - - async def action_quit(self) -> None: - """ - Occurs when keybind related to `quit` is called. - """ - self.exit() diff --git a/cmd/explorer/src/gui/rendarable/__init__.py b/cmd/explorer/src/gui/rendarable/__init__.py deleted file mode 100644 index 5ebbd9417f..0000000000 --- a/cmd/explorer/src/gui/rendarable/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" diff --git a/cmd/explorer/src/gui/rendarable/block_detail_info.py b/cmd/explorer/src/gui/rendarable/block_detail_info.py deleted file mode 100644 index 10e1b2ec05..0000000000 --- a/cmd/explorer/src/gui/rendarable/block_detail_info.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import time - -from rich.console import Group -from rich.panel import Panel -from rich.progress_bar import ProgressBar -from rich.table import Table -from rich.text import Text -from utils.time import human_time, unix_to_iso - -from app.model.schema import BlockDataDetail - - -class BlockDetailInfo: - def __init__(self, block_detail: BlockDataDetail) -> None: - self.block_detail = block_detail - - def __rich__(self) -> Group: - current = int(round(time.time())) - - basic_table = Table(box=None, expand=False, show_header=False, show_edge=False) - basic_table.add_column(style="deep_pink2 bold") - basic_table.add_column() - - basic_table.add_row( - Text.from_markup("Block Height:"), str(self.block_detail.number) - ) - basic_table.add_row( - Text.from_markup("Timestamp:"), - f"{human_time(current - self.block_detail.timestamp)} ({unix_to_iso(self.block_detail.timestamp)})", - ) - basic_table.add_row( - Text.from_markup("Transactions:"), - f"{len(self.block_detail.transactions)} transactions in this block", - ) - - content_table = Table( - box=None, expand=False, show_header=False, show_edge=False - ) - content_table.add_column(style="deep_pink2 bold") - content_table.add_column() - content_table.add_column() - content_table.add_row( - Text.from_markup("Total Difficulty:"), str(self.block_detail.difficulty), "" - ) - content_table.add_row( - Text.from_markup("Gas Used:"), - f"{self.block_detail.gas_used} ({(self.block_detail.gas_used / self.block_detail.gas_limit) * 100:.4f} %)", - ProgressBar( - completed=(self.block_detail.gas_used / self.block_detail.gas_limit) - * 100, - width=10, - ), - ) - content_table.add_row( - Text.from_markup("Gas Limit:"), f"{self.block_detail.gas_limit}" - ) - content_table.add_row( - Text.from_markup("Size:"), f"{self.block_detail.size} Bytes" - ) - - hash_table = Table(box=None, expand=False, show_header=False, show_edge=False) - hash_table.add_column(style="deep_pink2 bold") - hash_table.add_column() - hash_table.add_row(Text.from_markup("Hash:"), self.block_detail.hash) - hash_table.add_row( - Text.from_markup("Parent Hash:"), self.block_detail.parent_hash - ) - hash_table.add_row("StateRoot: ", self.block_detail.state_root) - hash_table.add_row("Nonce: ", self.block_detail.nonce) - - return Group( - Panel(basic_table, expand=True, title="Common", title_align="left"), - Panel(content_table, expand=True, title="Content", title_align="left"), - Panel(hash_table, expand=True, title="Hash", title_align="left"), - ) diff --git a/cmd/explorer/src/gui/rendarable/tx_detail_info.py b/cmd/explorer/src/gui/rendarable/tx_detail_info.py deleted file mode 100644 index cdd717a5a1..0000000000 --- a/cmd/explorer/src/gui/rendarable/tx_detail_info.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from rich.console import Group -from rich.panel import Panel -from rich.table import Table - -from app.model.schema import TxDataDetail - - -class TxDetailInfo: - def __init__(self, tx_detail: TxDataDetail) -> None: - self.tx_detail = tx_detail - - def __str__(self) -> str: - return str(self.tx_detail) - - def __rich__(self) -> Group: - common_table = Table(box=None, expand=False, show_header=False, show_edge=False) - common_table.add_column(style="deep_pink2 bold") - common_table.add_column() - - common_table.add_row("Transaction Hash:", self.tx_detail.hash) - common_table.add_row("Block:", str(self.tx_detail.block_number)) - common_table.add_row("From:", self.tx_detail.from_address) - common_table.add_row("Nonce:", str(self.tx_detail.nonce)) - common_table.add_row("To:", self.tx_detail.to_address) - - common_table.add_row("Value:", str(self.tx_detail.value)) - common_table.add_row("Gas Price:", str(self.tx_detail.gas_price)) - common_table.add_row("Gas:", str(self.tx_detail.gas)) - - contract_table = Table( - box=None, expand=False, show_header=False, show_edge=False - ) - contract_table.add_column(style="deep_pink2 bold") - contract_table.add_row("Contract Name:", self.tx_detail.contract_name) - contract_table.add_row("Contract Function:", self.tx_detail.contract_function) - if self.tx_detail.contract_parameters is not None: - function_arguments_table = Table( - box=None, expand=False, show_header=False, show_edge=False - ) - for k, v in self.tx_detail.contract_parameters.items(): - function_arguments_table.add_row(f"{k}: ", str(v)) - contract_table.add_row( - "Contract Function Arguments:", Panel(function_arguments_table) - ) - - return Group( - Panel(common_table, expand=True, title="Common", title_align="left"), - Panel(contract_table, expand=True, title="Contract", title_align="left"), - ) diff --git a/cmd/explorer/src/gui/screen/__init__.py b/cmd/explorer/src/gui/screen/__init__.py deleted file mode 100644 index 5ebbd9417f..0000000000 --- a/cmd/explorer/src/gui/screen/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" diff --git a/cmd/explorer/src/gui/screen/base.py b/cmd/explorer/src/gui/screen/base.py deleted file mode 100644 index 6624a72608..0000000000 --- a/cmd/explorer/src/gui/screen/base.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import TYPE_CHECKING, cast - -from textual.screen import Screen - -if TYPE_CHECKING: - from gui.explorer import ExplorerApp - - -class TuiScreen(Screen): - @property - def tui(self) -> "ExplorerApp": - return cast("ExplorerApp", self.app) diff --git a/cmd/explorer/src/gui/screen/block.py b/cmd/explorer/src/gui/screen/block.py deleted file mode 100644 index 8da6fe99eb..0000000000 --- a/cmd/explorer/src/gui/screen/block.py +++ /dev/null @@ -1,290 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import asyncio -from asyncio import Event, Lock -from datetime import datetime -from typing import Optional - -import connector -from aiohttp import ClientSession, ClientTimeout, TCPConnector -from gui.consts import ID -from gui.error import Error -from gui.screen.base import TuiScreen -from gui.widget.block_detail_view import BlockDetailView -from gui.widget.block_list_table import BlockListTable -from gui.widget.block_list_view import ( - BlockListQueryPanel, - BlockListSummaryPanel, - BlockListView, -) -from gui.widget.menu import Menu, MenuInstruction -from gui.widget.query_panel import QuerySetting -from rich.text import Text -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, Vertical -from textual.reactive import Reactive -from textual.widgets import Button, DataTable, Footer, Label, Static - -from app.model.schema import ( - BlockDataDetail, - BlockDataListResponse, - GetBlockSyncStatusResponse, - ListBlockDataQuery, - ListTxDataQuery, -) -from app.model.schema.base.base import SortOrder - - -class BlockScreen(TuiScreen): - BINDINGS = [ - Binding("e", "edit_query", "Edit list block data query"), - ] - dark = Reactive(True) - mutex_reload_block = Reactive(Lock()) - background_lock: Optional[Event] = None - - def __init__( - self, name: str | None = None, id: str | None = None, classes: str | None = None - ): - super().__init__(name=name, id=id, classes=classes) - self.base_url = self.tui.url - self.refresh_rate = 5.0 - self.block_detail_header_widget = BlockDetailView(classes="column") - - def compose(self) -> ComposeResult: - yield Horizontal( - Vertical( - Horizontal( - Label( - Text.from_markup(" [bold]ibet-Wallet-API BC Explorer[/bold]") - ), - Label(" | "), - Label( - "Fetching current block...", id=ID.BLOCK_CURRENT_BLOCK_NUMBER - ), - Label(" | "), - Label("Fetching current status...", id=ID.BLOCK_IS_SYNCED), - Label(" | "), - Label("Loading...", id=ID.BLOCK_NOTION), - id=ID.BLOCK_SCREEN_HEADER, - ), - Horizontal( - BlockListView(classes="column"), self.block_detail_header_widget - ), - classes="column", - ) - ) - yield Footer() - yield Menu(id=ID.MENU) - yield QuerySetting(id=ID.QUERY_PANEL) - - ################################################## - # Event - ################################################## - - async def on_mount(self) -> None: - """ - Occurs when Self is mounted - """ - self.query_one(Menu).hide() - self.remove_class("menu") - self.query_one(QuerySetting).hide() - self.query(BlockListTable)[0].focus() - self.set_interval(self.refresh_rate, self.fetch_sync_status) - - async def on_button_pressed(self, event: Button.Pressed) -> None: - """ - Occurs when Button is pressed - """ - event.stop() - event.prevent_default() - match event.button.id: - case ID.MENU_CANCEL: - self.query_one(Menu).hide() - self.remove_class("menu") - self.query(BlockListTable)[0].can_focus = True - self.query_one(BlockListTable).focus() - case ID.MENU_SHOW_TX: - ix = self.query_one(Menu).hide() - self.remove_class("menu") - get_query = ListTxDataQuery() - get_query.block_number = ix.block_number - self.tui.state.tx_list_query = get_query - await self.app.push_screen("transaction_screen") - case ID.QUERY_PANEL_ENTER: - self.reload_block() - - async def on_query_setting_enter(self, event: QuerySetting.Enter): - """ - Occurs when QuerySetting.Enter is emitted - """ - event.stop() - event.prevent_default() - self.reload_block() - - async def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: - """ - Occurs when DataTable row is selected - """ - event.stop() - event.prevent_default() - selected_row = self.query_one(BlockListTable)._data.get(event.row_key) - if selected_row is None: - return - block_number = selected_row.get(list(selected_row.keys())[0]) - block_hash = selected_row.get(list(selected_row.keys())[3]) - await self.fetch_block_detail(int(block_number)) - - if int(selected_row.get(list(selected_row.keys())[2], 0)) == 0: - # If the number of transaction is 0, menu is not pop up. - return - - self.query_one(Menu).show( - MenuInstruction( - block_number=block_number, - block_hash=block_hash, - selected_row=event.cursor_row, - ) - ) - self.add_class("menu") - self.query(BlockListTable)[0].can_focus = False - - def reload_block(self) -> None: - if ( - self.tui.state.current_block_number is None - or self.tui.state.block_list_query is None - ): - return - - self.query_one( - BlockListQueryPanel - ).block_list_query = self.tui.state.block_list_query - asyncio.create_task(self.fetch_block_list()) - - ################################################## - # Key binding - ################################################## - - def action_edit_query(self) -> None: - """ - Occurs when keybind related to `edit_query` is called. - """ - if ( - self.tui.state.current_block_number is None - or self.tui.state.block_list_query is None - ): - return - - self.query_one(QuerySetting).show() - self.query(BlockListTable)[0].can_focus = False - - ################################################## - # Fetch data - ################################################## - - async def fetch_sync_status(self): - async with TCPConnector(limit=2, keepalive_timeout=0) as tcp_connector: - async with ClientSession( - connector=tcp_connector, timeout=ClientTimeout(30) - ) as session: - try: - node_info: GetBlockSyncStatusResponse = ( - await connector.get_node_info(session, self.base_url) - ) - except Exception as e: - if hasattr(self, "emit_no_wait"): - self.emit_no_wait(Error(e, self)) - return - self.update_current_block(node_info.latest_block_number) - self.update_is_synced(node_info.is_synced) - if ( - self.tui.state.current_block_number is None - and self.tui.state.block_list_query is None - ): - # initialize block list query - query = ListBlockDataQuery() - query.to_block_number = node_info.latest_block_number - query.from_block_number = max( - node_info.latest_block_number - self.tui.lot_size - 1, 0 - ) - query.sort_order = SortOrder.DESC - self.tui.state.block_list_query = query - self.query_one(BlockListQueryPanel).block_list_query = query - - self.tui.state.current_block_number = node_info.latest_block_number - - async def fetch_block_list(self) -> None: - if self.tui.state.current_block_number == 0: - return - try: - self.query_one(BlockListSummaryPanel).loading = True - await asyncio.sleep(5) - async with TCPConnector(limit=1, keepalive_timeout=0) as tcp_connector: - async with ClientSession( - connector=tcp_connector, timeout=ClientTimeout(30) - ) as session: - try: - block_data_list: BlockDataListResponse = ( - await connector.list_block_data( - session, self.base_url, self.tui.state.block_list_query - ) - ) - except Exception as e: - if hasattr(self, "emit_no_wait"): - self.emit_no_wait(Error(e, self)) - return - self.query_one(BlockListTable).update_rows( - block_data_list.block_data - ) - self.query_one(BlockListSummaryPanel).loaded_time = datetime.now() - - finally: - self.query_one(BlockListSummaryPanel).loading = False - - async def fetch_block_detail(self, block_number: int): - async with TCPConnector(limit=1, keepalive_timeout=0) as tcp_connector: - async with ClientSession( - connector=tcp_connector, timeout=ClientTimeout(30) - ) as session: - try: - block_detail: BlockDataDetail = await connector.get_block_data( - session, - self.base_url, - block_number, - ) - except Exception as e: - if hasattr(self, "emit_no_wait"): - self.emit_no_wait(Error(e, self)) - return - self.query_one(BlockDetailView).block_detail = block_detail - - def update_current_block(self, latest_block_number: int): - self.query_one(f"#{ID.BLOCK_CURRENT_BLOCK_NUMBER}", Static).update( - f"Current Block: {latest_block_number}" - ) - - def update_is_synced(self, is_synced: bool): - self.query_one(f"#{ID.BLOCK_IS_SYNCED}", Static).update( - f"Is Synced: {is_synced}" - ) - self.query_one(f"#{ID.BLOCK_NOTION}", Static).update( - "Press [E] To Load Block List" - ) diff --git a/cmd/explorer/src/gui/screen/traceback.py b/cmd/explorer/src/gui/screen/traceback.py deleted file mode 100644 index 2f27589d0b..0000000000 --- a/cmd/explorer/src/gui/screen/traceback.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from gui.screen.base import TuiScreen -from gui.widget.block_list_table import BlockListTable -from gui.widget.traceback import TracebackWidget -from textual.app import ComposeResult -from textual.binding import Binding -from textual.widgets import Footer - - -class TracebackScreen(TuiScreen): - BINDINGS = [Binding("q,enter,space", "quit", "Close", priority=True)] - - def compose(self) -> ComposeResult: - yield TracebackWidget() - yield Footer() - - ################################################## - # Event - ################################################## - - async def on_mount(self) -> None: - """ - Occurs when Self is mounted - """ - self.query(TracebackWidget)[0].focus() - - ################################################## - # Key binding - ################################################## - - def action_quit(self): - """ - Occurs when keybind related to `quit` is called. - """ - self.tui.pop_screen() - self.tui.query_one(BlockListTable).focus() diff --git a/cmd/explorer/src/gui/screen/transaction.py b/cmd/explorer/src/gui/screen/transaction.py deleted file mode 100644 index 9fca85e637..0000000000 --- a/cmd/explorer/src/gui/screen/transaction.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import connector -from aiohttp import ClientSession, ClientTimeout, TCPConnector -from gui.consts import ID -from gui.screen.base import TuiScreen -from gui.widget.block_list_table import BlockListTable -from gui.widget.tx_detail_view import TxDetailView -from gui.widget.tx_list_table import TxListTable -from gui.widget.tx_list_view import TxListView -from rich.text import Text -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, Vertical -from textual.widgets import DataTable, Footer, Label - -from app.model.schema.bc_explorer import TxDataDetail - - -class TransactionScreen(TuiScreen): - BINDINGS = [Binding("q", "quit", "Close", priority=True)] - - def compose(self) -> ComposeResult: - yield Horizontal( - Vertical( - Horizontal( - Label( - Text.from_markup(" [bold]ibet-Wallet-API BC Explorer[/bold]") - ), - Label(" | "), - Label("Selected block: -", id=ID.TX_SELECTED_BLOCK_NUMBER), - id="tx_list_header", - ), - Horizontal( - TxListView(classes="column"), TxDetailView(classes="column") - ), - classes="column", - ) - ) - yield Footer() - - ################################################## - # Event - ################################################## - - async def on_mount(self) -> None: - """ - Occurs when Self is mounted - """ - self.query(TxListTable)[0].focus() - - async def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: - """ - Occurs when DataTable row is selected - """ - event.stop() - event.prevent_default() - selected_row = self.query_one(TxListTable)._data.get(event.row_key) - if selected_row is None: - return - - tx_hash = selected_row.get(list(selected_row.keys())[0]) - async with TCPConnector(limit=1, keepalive_timeout=0) as tcp_connector: - async with ClientSession( - connector=tcp_connector, timeout=ClientTimeout(30) - ) as session: - tx_detail: TxDataDetail = await connector.get_tx_data( - session, - self.tui.url, - tx_hash, - ) - self.query_one(TxDetailView).tx_detail = tx_detail - - async def on_screen_suspend(self): - """ - Occurs when Self is suspended - """ - self.query_one(TxListTable).update_rows([]) - - async def on_screen_resume(self): - """ - Occurs when Self is resumed - """ - if self.tui.state.tx_list_query is not None: - async with TCPConnector(limit=1, keepalive_timeout=0) as tcp_connector: - async with ClientSession( - connector=tcp_connector, timeout=ClientTimeout(30) - ) as session: - tx_list = await connector.list_tx_data( - session=session, - url=self.tui.url, - query=self.tui.state.tx_list_query, - ) - self.query_one(TxListTable).update_rows(tx_list.tx_data) - self.query_one(f"#{ID.TX_SELECTED_BLOCK_NUMBER}", Label).update( - f"Selected block: {self.tui.state.tx_list_query.block_number}" - ) - - ################################################## - # Key binding - ################################################## - - def action_quit(self): - """ - Occurs when keybind related to `quit` is called. - """ - self.tui.pop_screen() - self.tui.query(BlockListTable)[0].can_focus = True - self.tui.query(BlockListTable)[0].focus() diff --git a/cmd/explorer/src/gui/styles.py b/cmd/explorer/src/gui/styles.py deleted file mode 100644 index fec17d67f5..0000000000 --- a/cmd/explorer/src/gui/styles.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from rich import box - -BORDER_FOCUSED = "green" -BORDER_ERROR = "red" -BORDER_MOUSE_OVER = "grey100" -BORDER = "grey82" -BOX = box.SQUARE - -TABLE_BOX = box.SIMPLE_HEAD diff --git a/cmd/explorer/src/gui/widget/__init__.py b/cmd/explorer/src/gui/widget/__init__.py deleted file mode 100644 index 5ebbd9417f..0000000000 --- a/cmd/explorer/src/gui/widget/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" diff --git a/cmd/explorer/src/gui/widget/base.py b/cmd/explorer/src/gui/widget/base.py deleted file mode 100644 index ef0f553a90..0000000000 --- a/cmd/explorer/src/gui/widget/base.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import TYPE_CHECKING, cast - -from textual.widget import Widget -from textual.widgets import Static - -if TYPE_CHECKING: - from gui.explorer import ExplorerApp - - -class TuiWidget(Widget): - @property - def tui(self) -> "ExplorerApp": - return cast("ExplorerApp", self.app) - - -class TuiStatic(Static): - @property - def tui(self) -> "ExplorerApp": - return cast("ExplorerApp", self.app) diff --git a/cmd/explorer/src/gui/widget/block_detail_view.py b/cmd/explorer/src/gui/widget/block_detail_view.py deleted file mode 100644 index 788610c3e4..0000000000 --- a/cmd/explorer/src/gui/widget/block_detail_view.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Literal, Union - -from gui import styles -from gui.rendarable.block_detail_info import BlockDetailInfo -from gui.widget.base import TuiWidget -from rich.align import Align -from rich.panel import Panel -from rich.style import Style -from textual.reactive import Reactive, reactive - -from app.model.schema import BlockDataDetail - - -class BlockDetailView(TuiWidget): - block_detail: Reactive[BlockDataDetail | None] = reactive(None) - - def watch_block_detail(self, old: BlockDetailInfo, new: BlockDetailInfo): - """ - Occurs when `block_detail` is changed - """ - self.render() - - def render(self) -> Panel: - block_detail: Union[Align, BlockDetailInfo] = Align.center( - "Press [E] to set query", vertical="middle" - ) - style: Style | Literal["none"] = Style(bgcolor="#004578") - - if self.block_detail is not None: - block_detail = BlockDetailInfo(self.block_detail) - style = "none" - - panel = Panel( - block_detail, - title="[bold]Block[/]", - title_align="left", - style=style, - border_style=styles.BORDER, - box=styles.BOX, - ) - - return panel diff --git a/cmd/explorer/src/gui/widget/block_list_table.py b/cmd/explorer/src/gui/widget/block_list_table.py deleted file mode 100644 index fac7a3fbe0..0000000000 --- a/cmd/explorer/src/gui/widget/block_list_table.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import time -from typing import Iterable - -from rich.progress_bar import ProgressBar -from textual.binding import Binding -from textual.coordinate import Coordinate -from textual.reactive import reactive -from textual.widgets import DataTable -from utils.time import human_time - -from app.model.schema.bc_explorer import BlockData - - -class BlockListTable(DataTable): - BINDINGS = [ - Binding("ctrl+n", "cursor_down", "Down", show=False), - Binding("ctrl+p", "cursor_up", "Up", show=False), - ] - only_include_tx = reactive(False) - raw_data: Iterable[BlockData] = [] - - def __init__(self, name: str, complete_refresh: bool, id: str): - super().__init__(name=name, id=id) - self.table_name = name - self.cursor_type = "row" - self.complete_refresh = complete_refresh - - def on_mount(self) -> None: - """ - Occurs when Self is mounted - """ - self.add_column("Block", width=10) - self.add_column("Age", width=24) - self.add_column("Txn", width=4) - self.add_column("Hash", width=70) - self.add_column("Gas Used") - - def toggle_filter(self) -> bool: - self.only_include_tx = not self.only_include_tx - self.update_rows(self.raw_data) - return self.only_include_tx - - def update_rows(self, data: Iterable[BlockData]): - self.raw_data = data - selected_row = self.cursor_row - if len(self._data) > 0: - selected_block_number = list(self._data.keys())[selected_row] - else: - selected_block_number = None - - if self.complete_refresh: - self.clear() - - current = int(round(time.time())) - rows = [ - [ - str(d.number), - human_time(current - d.timestamp) + " ago", - str(len(d.transactions)), - d.hash, - ProgressBar(completed=(d.gas_used / d.gas_limit) * 100, width=10), - ] - for d in data - ] - if self.only_include_tx: - rows = list(filter(lambda r: r[2] != "0", rows)) - self.add_rows(rows) - - # Keep current selected position - if selected_block_number is not None: - row_to_be_selected = next( - (i for i, row in enumerate(rows) if row[0] == selected_block_number), - len(rows) - 1 if len(rows) > 0 else 0, - ) - self.cursor_cell = Coordinate(row_to_be_selected, 0) - self.hover_cell = Coordinate(row_to_be_selected, 0) - else: - self.cursor_cell = Coordinate(0, 0) - self.hover_cell = Coordinate(0, 0) - - self._scroll_cursor_into_view(animate=False) - self.refresh() - - def action_select_cursor(self) -> None: - """ - Occurs when keybind related to `select_cursor` is called. - """ - self._set_hover_cursor(False) - if self.show_cursor and self.cursor_type != "none" and self.has_focus: - self._post_selected_message() diff --git a/cmd/explorer/src/gui/widget/block_list_view.py b/cmd/explorer/src/gui/widget/block_list_view.py deleted file mode 100644 index d9405b0e0f..0000000000 --- a/cmd/explorer/src/gui/widget/block_list_view.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from datetime import datetime - -from gui import styles -from gui.consts import ID -from gui.widget.base import TuiStatic, TuiWidget -from gui.widget.block_list_table import BlockListTable -from rich.panel import Panel -from rich.spinner import Spinner -from rich.table import Table -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal -from textual.reactive import Reactive, reactive -from textual.timer import Timer - -from app.model.schema import ListBlockDataQuery - - -class BlockListQueryPanel(TuiStatic): - block_list_query: Reactive[ListBlockDataQuery | None] = reactive(None) - - def watch_block_list_query(self, old: ListBlockDataQuery, new: ListBlockDataQuery): - """ - Occurs when `block_list_query` is changed - """ - self.render() - - def render(self) -> Panel: - content = Table( - show_header=True, header_style="bold", show_edge=False, show_lines=False - ) - content.add_column("From", justify="center", width=10) - content.add_column("To", justify="center", width=10) - content.add_column("Sort", justify="center", width=7) - - if self.block_list_query is not None: - content.add_row( - *[ - str(self.block_list_query.from_block_number), - str(self.block_list_query.to_block_number), - "Asc" if self.block_list_query.sort_order == 0 else "Desc", - ] - ) - else: - content.add_row(*["", "", ""]) - - style = "none" - panel = Panel( - content, - title="[bold]Query[/]", - title_align="left", - style=style, - border_style=styles.BORDER, - box=styles.BOX, - ) - - return panel - - -class BlockListSummaryPanel(TuiStatic): - loading: reactive[bool | None] = reactive(False) - loaded_time: reactive[datetime | None] = reactive(None) - only_block_filter: reactive[bool | None] = reactive(False) - - update_render: Timer | None = None - - def __init__(self, classes: str | None = None): - super().__init__(classes=classes) - self._spinner = Spinner("dots") - - def watch_loading(self, new: bool): - """ - Occurs when `loading` is changed - """ - if new: - if self.update_render is None: - self.update_render = self.set_interval(1 / 60, self.update_spinner) - else: - self.update_render.resume() - else: - if self.update_render is not None: - self.update_render.pause() - - def watch_loaded_time(self, new: datetime): - """ - Occurs when `loaded_time` is changed - """ - self.render() - - def watch_only_block_filter(self, new: bool): - """ - Occurs when `only_block_filter` is changed - """ - self.render() - - def update_spinner(self) -> None: - self.render() - self.refresh() - - def render(self) -> Panel: - content = Table( - show_header=True, header_style="bold", show_edge=False, show_lines=False - ) - content.add_column("Loading", justify="center") - content.add_column("Only Blocks Including Tx", style="dim", justify="center") - content.add_column("Loaded Time", style="dim", justify="center") - - content.add_row( - self._spinner if self.loading else "", - f"{self.only_block_filter}", - ( - f"{self.loaded_time.year:0>4}/{self.loaded_time.month:0>2}/{self.loaded_time.day:0>2} {self.loaded_time.hour:0>2}:{self.loaded_time.minute:0>2}:{self.loaded_time.second:0>2}" - if self.loaded_time is not None - else "" - ), - ) - - style = "none" - panel = Panel( - content, - title="[bold]Result[/]", - title_align="left", - style=style, - border_style=styles.BORDER, - box=styles.BOX, - ) - - return panel - - -class BlockListView(TuiWidget): - BINDINGS = [ - Binding("t", "filter", "Toggle Only Blocks Including Tx"), - ] - - def compose(self) -> ComposeResult: - yield Horizontal( - BlockListQueryPanel(classes="column_auto"), - BlockListSummaryPanel(classes="column"), - id=ID.BLOCK_LIST_DESCRIPTION, - ) - yield BlockListTable( - name="blocks", complete_refresh=True, id=ID.BLOCK_LIST_TABLE - ) - - def action_filter(self): - """ - Occurs when keybind related to `filter` is called. - """ - if self.query_one(BlockListTable).can_focus: - toggle = self.query_one(BlockListTable).toggle_filter() - self.query_one(BlockListSummaryPanel).only_block_filter = toggle diff --git a/cmd/explorer/src/gui/widget/choice.py b/cmd/explorer/src/gui/widget/choice.py deleted file mode 100644 index 4909159b1d..0000000000 --- a/cmd/explorer/src/gui/widget/choice.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from textual.app import ComposeResult -from textual.binding import Binding -from textual.widgets import Label, ListItem, ListView - - -class Choices(ListView): - BINDINGS = [ - Binding("enter", "select_cursor", "Select", show=False), - Binding("up", "cursor_up", "Cursor Up", show=False, priority=True), - Binding("ctrl+p", "cursor_up", "Cursor Up", show=False, priority=True), - Binding("down", "cursor_down", "Cursor Down", show=False, priority=True), - Binding("ctrl+n", "cursor_down", "Cursor Down", show=False, priority=True), - ] - - def __init__(self, choices: list[str], *, id: str | None = None) -> None: - super().__init__(id=id) - self.choices = choices - - def compose(self) -> ComposeResult: - for choice in self.choices: - yield ListItem(Label(choice)) - - @property - def value(self) -> ListItem | None: - return self.highlighted_child - - @value.setter - def value(self, v: str): - idx = self.choices.index(v) - if idx is None: - return - else: - self.index = idx - self.render() - - def current_value(self): - return self.choices[self.index] diff --git a/cmd/explorer/src/gui/widget/menu.py b/cmd/explorer/src/gui/widget/menu.py deleted file mode 100644 index 89d49e1aa7..0000000000 --- a/cmd/explorer/src/gui/widget/menu.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from gui.consts import ID -from gui.widget.base import TuiWidget -from pydantic import BaseModel -from rich.text import Text -from textual.app import ComposeResult -from textual.binding import Binding -from textual.widgets import Button - - -class MenuInstruction(BaseModel): - block_number: int - block_hash: str - selected_row: int - - -class Menu(TuiWidget): - BINDINGS = [ - Binding("tab,down,ctrl+n", "focus_next", "Focus Next", show=False), - Binding("shift+tab,up,ctrl+p", "focus_previous", "Focus Previous", show=False), - Binding("ctrl+r", "", "", show=False), - Binding("t", "click('menu_show_tx')", "Show Transactions"), - Binding("c,q", "click('menu_cancel')", "Cancel", key_display="Q, C"), - ] - ix: MenuInstruction | None = None - - def compose(self) -> ComposeResult: - yield Button( - Text.from_markup(r"\[t] Show Transactions :package:", overflow="crop"), - id=ID.MENU_SHOW_TX, - classes="menubutton", - ) - yield Button(r"\[c] Cancel", id=ID.MENU_CANCEL, classes="menubutton") - - def show(self, ix: MenuInstruction): - self.ix = ix - self.add_class("visible") - self.query_one(f"#{ID.MENU_SHOW_TX}", Button).focus() - - def hide(self) -> MenuInstruction | None: - self.remove_class("visible") - return self.ix - - def action_click(self, _id: str): - """ - Occurs when keybind related to `click` is called. - """ - self.query_one(f"#{_id}", Button).press() diff --git a/cmd/explorer/src/gui/widget/query_panel.py b/cmd/explorer/src/gui/widget/query_panel.py deleted file mode 100644 index aa53168fe5..0000000000 --- a/cmd/explorer/src/gui/widget/query_panel.py +++ /dev/null @@ -1,291 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import TYPE_CHECKING, cast - -from gui.consts import ID -from gui.widget.base import TuiWidget -from gui.widget.block_list_table import BlockListTable -from gui.widget.block_list_view import BlockListQueryPanel -from gui.widget.choice import Choices -from rich.markdown import Markdown -from textual import events -from textual.app import ComposeResult -from textual.binding import Binding -from textual.message import Message -from textual.widgets import Button, Input, Label - -from app.model.schema import ListBlockDataQuery -from app.model.schema.base.base import SortOrder - -if TYPE_CHECKING: - from gui.explorer import ExplorerApp - - -class ToBlockInput(Input): - @property - def tui(self) -> "ExplorerApp": - return cast("ExplorerApp", self.app) - - def increment_value(self, increment: int) -> None: - """ - Increments the value by the increment - """ - if self.value is None: - self.value = str(0) - - if ( - self.tui.state.current_block_number is not None - and int(self.value) + increment > self.tui.state.current_block_number - ): - self.value = f"{self.tui.state.current_block_number}" - elif int(self.value) + increment < 0: - self.value = str(0) - else: - self.value = str(int(self.value) + increment) - - def insert_text_at_cursor(self, text: str) -> None: - """ - Insert new text at the cursor, move the cursor to the end of the new text. - """ - if self.value == "0": - if text == "0": - return - self.value = str(int(self.value) + int(text)) - self.cursor_position = 1 - return - - if self.cursor_position > len(self.value): - if ( - self.tui.state.current_block_number is not None - and int(self.value + text) > self.tui.state.current_block_number - ): - self.value = f"{self.tui.state.current_block_number}" - else: - self.value += text - self.cursor_position = len(self.value) - else: - value = self.value - before = value[: self.cursor_position] - after = value[self.cursor_position :] - if ( - self.tui.state.current_block_number is not None - and int(before + text + after) > self.tui.state.current_block_number - ): - self.value = f"{self.tui.state.current_block_number}" - else: - self.value = f"{before}{text}{after}" - self.cursor_position += len(text) - - async def on_key(self, event: events.Key) -> None: - """ - Occurs when `Key` is emitted. - """ - self._cursor_visible = True - if self.cursor_blink: - self._blink_timer.reset() - - event.prevent_default() - event.stop() - if await self.handle_key(event): - return - - if event.character is not None and event.character.isdigit(): - if event.character == "0" and self.cursor_position == 0: - self.cursor_position += 1 - return - self.insert_text_at_cursor(event.character) - elif event.key in ["left", "ctrl+b"]: - if self.cursor_position != 0: - self.cursor_position -= 1 - elif event.key in ["right", "ctrl+f"]: - if self.cursor_position != len(self.value): - self.cursor_position += 1 - elif event.key in ["up", "ctrl+p"]: - self.increment_value(10) - self.cursor_position = len(self.value) - elif event.key in ["down", "ctrl+n"]: - self.increment_value(-10) - self.cursor_position = len(self.value) - elif event.key in ["home", "ctrl+a"]: - self.cursor_position = 0 - elif event.key in ["end", "ctrl+e"]: - self.cursor_position = len(self.value) - elif event.key == "backspace": - if self.cursor_position == 0: - return - elif len(self.value) == 1: - self.value = str(0) - self.cursor_position = 0 - elif len(self.value) == 2: - if self.cursor_position == 1: - self.value = self.value[1] - self.cursor_position = 0 - else: - self.value = self.value[0] - self.cursor_position = 1 - else: - if self.cursor_position == 1: - self.value = self.value[1:] - self.cursor_position = 0 - elif self.cursor_position == len(self.value): - self.value = self.value[:-1] - self.cursor_position -= 1 - else: - new_value = ( - self.value[: self.cursor_position - 1] - + self.value[self.cursor_position :] - ) - if new_value != "": - self.value = new_value - self.cursor_position -= 1 - - -class QuerySetting(TuiWidget): - BINDINGS = [ - Binding("c,q,escape", "cancel()", "Cancel", key_display="Q, C", priority=True), - Binding("tab", "focus_next", "Focus Next", show=True, priority=True), - Binding( - "shift+tab", "focus_previous", "Focus Previous", show=True, priority=True - ), - Binding("enter", "enter()", "Enter", priority=True), - Binding("e", "", "", show=False), - Binding("ctrl+c", "", "", show=False), - Binding("ctrl+r", "", "", show=False), - Binding("ctrl+n", "cursor_down", "Down", show=False), - Binding("ctrl+p", "cursor_up", "Up", show=False), - ] - - def compose(self) -> ComposeResult: - yield Label(Markdown("# Query Setting")) - yield Label(Markdown("#### To Block")) - yield ToBlockInput( - placeholder="100", name="to_block", id=ID.QUERY_PANEL_TO_BLOCK_INPUT - ) - yield Label(Markdown("#### From Block(Auto set)")) - yield Input( - placeholder="0", name="from_block", id=ID.QUERY_PANEL_FROM_BLOCK_INPUT - ) - yield Label(Markdown("#### Sort Order")) - yield Choices(["DESC", "ASC"], id=ID.QUERY_PANEL_SORT_ORDER_CHOICE) - yield Button("Enter", id=ID.QUERY_PANEL_ENTER) - - def show(self): - self.add_class("visible") - self.query_one(f"#{ID.QUERY_PANEL_TO_BLOCK_INPUT}", Input).focus() - self.query_one(f"#{ID.QUERY_PANEL_FROM_BLOCK_INPUT}", Input).can_focus = False - - if self.tui.state.block_list_query is not None: - query = self.tui.state.block_list_query - self.query_one(f"#{ID.QUERY_PANEL_FROM_BLOCK_INPUT}", Input).value = str( - query.from_block_number - ) - self.query_one(f"#{ID.QUERY_PANEL_TO_BLOCK_INPUT}", Input).value = str( - query.to_block_number - ) - - item = "ASC" if query.sort_order.value == 0 else "DESC" - self.query_one( - f"#{ID.QUERY_PANEL_SORT_ORDER_CHOICE}", Choices - ).index = self.query_one( - f"#{ID.QUERY_PANEL_SORT_ORDER_CHOICE}", Choices - ).choices.index(item) - - def hide(self) -> None: - self.remove_class("visible") - self.tui.query(BlockListTable)[0].can_focus = True - self.tui.query(BlockListTable)[0].focus() - - def on_key(self, event: events.Key): - """ - Occurs when `Key` is emitted. - """ - if event.key == "Enter": - self.action_enter() - from_block = self.query_one( - f"#{ID.QUERY_PANEL_FROM_BLOCK_INPUT}", Input - ).value - to_block = self.query_one(f"#{ID.QUERY_PANEL_TO_BLOCK_INPUT}", Input).value - sort_order = self.query_one( - f"#{ID.QUERY_PANEL_SORT_ORDER_CHOICE}", Choices - ).current_value() - - query = ListBlockDataQuery( - from_block_number=int(from_block), - to_block_number=int(to_block), - sort_order=SortOrder.DESC if sort_order == "DESC" else SortOrder.ASC, - ) - self.tui.state.block_list_query = query - self.tui.query_one(BlockListQueryPanel).block_list_query = query - - self.hide() - else: - event.stop() - event.prevent_default() - - async def on_input_changed(self, event: ToBlockInput.Changed): - """ - Occurs when `Input.Changed` is emitted. - """ - event.prevent_default() - event.stop() - if event.input.id == ID.QUERY_PANEL_TO_BLOCK_INPUT: - self.query_one(f"#{ID.QUERY_PANEL_FROM_BLOCK_INPUT}", Input).value = str( - max(int(event.input.value) - self.tui.lot_size - 1, 0) - ) - - async def on_button_pressed(self, event: Button.Pressed) -> None: - """ - Occurs when `Button.Pressed` is emitted. - """ - if event.button.id == ID.QUERY_PANEL_ENTER: - self.action_enter() - else: - event.stop() - event.prevent_default() - - class Enter(Message): - pass - - def action_enter(self): - """ - Occurs when keybind related to `enter` is called. - """ - from_block = self.query_one(f"#{ID.QUERY_PANEL_FROM_BLOCK_INPUT}", Input).value - to_block = self.query_one(f"#{ID.QUERY_PANEL_TO_BLOCK_INPUT}", Input).value - sort_order = self.query_one( - f"#{ID.QUERY_PANEL_SORT_ORDER_CHOICE}", Choices - ).current_value() - - query = ListBlockDataQuery( - from_block_number=int(from_block), - to_block_number=int(to_block), - sort_order=SortOrder.DESC if sort_order == "DESC" else SortOrder.ASC, - ) - self.tui.state.block_list_query = query - self.tui.query_one(BlockListQueryPanel).block_list_query = query - - self.post_message(self.Enter()) - self.hide() - - def action_cancel(self): - """ - Occurs when keybind related to `cancel` is called. - """ - self.hide() diff --git a/cmd/explorer/src/gui/widget/traceback.py b/cmd/explorer/src/gui/widget/traceback.py deleted file mode 100644 index ce5d641aa4..0000000000 --- a/cmd/explorer/src/gui/widget/traceback.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Literal - -from gui import styles -from gui.widget.base import TuiWidget -from rich.align import Align -from rich.panel import Panel -from rich.style import Style -from rich.traceback import Traceback - - -class TracebackWidget(TuiWidget): - def render(self) -> Panel: - style: Style | Literal["none"] = Style() - if self.tui.state.error is not None: - trace_back = Traceback.from_exception( - exc_type=type(self.tui.state.error), - exc_value=self.tui.state.error, - traceback=self.tui.state.error.__traceback__, - ) - content: Align = Align.center(trace_back, vertical="middle") - else: - content = Align.center("", vertical="middle") - - panel = Panel( - content, - title="[bold]Exception[/]", - style=style, - border_style=styles.BORDER, - box=styles.BOX, - title_align="left", - padding=0, - highlight=True, - ) - - return panel diff --git a/cmd/explorer/src/gui/widget/tx_detail_view.py b/cmd/explorer/src/gui/widget/tx_detail_view.py deleted file mode 100644 index cb7b97d800..0000000000 --- a/cmd/explorer/src/gui/widget/tx_detail_view.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Literal, Union - -from gui import styles -from gui.rendarable.tx_detail_info import TxDetailInfo -from gui.widget.base import TuiWidget -from rich.align import Align -from rich.panel import Panel -from rich.style import Style -from textual.reactive import Reactive, reactive - -from app.model.schema import TxDataDetail - - -class TxDetailView(TuiWidget): - tx_detail: Reactive[TxDataDetail | None] = reactive(None) - - def on_mount(self) -> None: - """ - Occurs when Self is mounted - """ - pass - - def watch_tx_detail(self, old: TxDetailInfo, new: TxDetailInfo): - """ - Occurs when `tx_detail` is changed - """ - self.render() - - def render(self) -> Panel: - tx_detail: Union[Align, TxDetailInfo] = Align.center( - "Not selected", vertical="middle" - ) - style: Style | Literal["none"] = Style(bgcolor="#004578") - - if self.tx_detail is not None: - tx_detail = TxDetailInfo(self.tx_detail) - style = "none" - - panel = Panel( - tx_detail, - title="[bold]Transaction[/]", - style=style, - border_style=styles.BORDER, - box=styles.BOX, - title_align="left", - padding=0, - ) - - return panel diff --git a/cmd/explorer/src/gui/widget/tx_list_table.py b/cmd/explorer/src/gui/widget/tx_list_table.py deleted file mode 100644 index 76860dbe2e..0000000000 --- a/cmd/explorer/src/gui/widget/tx_list_table.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Iterable - -from textual.binding import Binding -from textual.reactive import reactive -from textual.widgets import DataTable - -from app.model.schema.bc_explorer import TxData - - -class TxListTable(DataTable): - BINDINGS = [ - Binding("ctrl+n", "cursor_down", "Down", show=False), - Binding("ctrl+p", "cursor_up", "Up", show=False), - ] - only_include_tx = reactive(False) - raw_data: Iterable[TxData] = [] - - def __init__(self, name: str, complete_refresh: bool): - super().__init__() - self.table_name = name - self.column_labels = ["Txn Hash", "Block"] - self.cursor_type = "row" - self.complete_refresh = complete_refresh - - def on_mount(self) -> None: - """ - Occurs when Self is mounted - """ - self.add_columns(*self.column_labels) - - def update_rows(self, data: Iterable[TxData]): - if self.complete_refresh: - self.clear() - rows = [[d.hash, str(d.block_number)] for d in data] - self.add_rows(rows) - self.refresh() - - def action_select_cursor(self) -> None: - """ - Occurs when keybind related to `select_cursor` is called. - """ - self._set_hover_cursor(False) - if self.show_cursor and self.cursor_type != "none" and self.has_focus: - self._post_selected_message() diff --git a/cmd/explorer/src/gui/widget/tx_list_view.py b/cmd/explorer/src/gui/widget/tx_list_view.py deleted file mode 100644 index d66a2b404f..0000000000 --- a/cmd/explorer/src/gui/widget/tx_list_view.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from gui.widget.base import TuiWidget -from gui.widget.tx_list_table import TxListTable -from textual.app import ComposeResult -from textual.containers import Horizontal -from textual.widget import Widget -from textual.widgets import Static - - -class TxListView(TuiWidget): - BINDINGS = [] - - def __init__( - self, - *children: Widget, - name: str | None = None, - id: str | None = None, - classes: str | None = None, - ): - super().__init__(*children, name=name, id=id, classes=classes) - - def compose(self) -> ComposeResult: - yield TxListTable(name="transactions", complete_refresh=True) - yield Horizontal( - Static("", classes="column"), - id="tx_list_description", - ) diff --git a/cmd/explorer/src/main.py b/cmd/explorer/src/main.py deleted file mode 100644 index 6d3f880b72..0000000000 --- a/cmd/explorer/src/main.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import asyncio -import logging - -logging.getLogger("asyncio").setLevel(logging.WARNING) - -import typer -from gui.explorer import ExplorerApp - -app = typer.Typer(pretty_exceptions_show_locals=False) - - -@app.command() -def run( - url: str = typer.Option( - "http://localhost:5000", help="ibet-Wallet-API server URL to connect" - ), - lot_size: int = typer.Option(100, help="Lot size to fetch Block Data list"), -): - explorer = ExplorerApp() - asyncio.run(explorer.run_async(url=url, lot_size=lot_size)) - - -if __name__ == "__main__": - app() diff --git a/cmd/explorer/src/utils/time.py b/cmd/explorer/src/utils/time.py deleted file mode 100644 index 1d6b598dd6..0000000000 --- a/cmd/explorer/src/utils/time.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from collections import OrderedDict -from datetime import datetime - -INTERVALS = OrderedDict( - [ - ("year", 31536000), # 60 * 60 * 24 * 365 - ("month", 2627424), # 60 * 60 * 24 * 30.41 (assuming 30.41 days in a month) - ("week", 604800), # 60 * 60 * 24 * 7 - ("day", 86400), # 60 * 60 * 24 - ("hr", 3600), # 60 * 60 - ("minute", 60), - ("sec", 1), - ] -) - - -def human_time(secs: int): - """Human-readable time from secs (ie. 5 days and 2 hrs). - Examples: - >>> human_time(15) - "less than minutes" - >>> human_time(3600) - "1 hr" - >>> human_time(3720) - "1 hr" - >>> human_time(266400) - "3 days" - >>> human_time(0) - "0 secs" - >>> human_time(1) - "less than minutes" - Args: - secs (int): Duration in secs. - Returns: - str: Human-readable time. - """ - if secs < 0: - return "0 secs" - elif secs == 0: - return "0 secs" - elif 1 < secs < INTERVALS["minute"]: - return "less than minutes" - - res = [] - for interval, count in INTERVALS.items(): - quotient, remainder = divmod(secs, count) - if quotient >= 1: - secs = remainder - if quotient > 1: - interval += "s" - res.append("%i %s" % (int(quotient), interval)) - if remainder == 0: - break - - return res[0] - - -def unix_to_iso(unix_time: int): - return datetime.fromtimestamp(unix_time).isoformat() diff --git a/cmd/explorer/uv.lock b/cmd/explorer/uv.lock deleted file mode 100644 index f3495bf744..0000000000 --- a/cmd/explorer/uv.lock +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 -revision = 3 -requires-python = "==3.13.11" - -[[package]] -name = "ibet-wallet-api-explorer" -version = "0.1.0" -source = { virtual = "." } diff --git a/docs/ibet_wallet_api.yaml b/docs/ibet_wallet_api.yaml index 1bbd4b7c8b..0da87443b7 100644 --- a/docs/ibet_wallet_api.yaml +++ b/docs/ibet_wallet_api.yaml @@ -514,251 +514,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ServiceUnavailableResponse' - /NodeInfo/BlockData: - get: - tags: - - node_info - summary: '[ibet Blockchain Explorer] List block data' - description: |- - Returns a list of block data within the specified block number range. - The maximum number of search results is 1000. - operationId: ListBlockData - parameters: - - name: offset - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - description: Offset for pagination - title: Offset - description: Offset for pagination - - name: limit - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - description: Limit for pagination - title: Limit - description: Limit for pagination - - name: from_block_number - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - title: From Block Number - - name: to_block_number - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - title: To Block Number - - name: sort_order - in: query - required: false - schema: - anyOf: - - $ref: '#/components/schemas/SortOrder' - - type: 'null' - description: 'sort order(0: ASC, 1: DESC)' - default: 0 - title: Sort Order - description: 'sort order(0: ASC, 1: DESC)' - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/GenericSuccessResponse_BlockDataListResponse_' - '400': - content: - application/json: - schema: - anyOf: - - $ref: '#/components/schemas/RequestValidationErrorResponse' - - $ref: '#/components/schemas/ResponseLimitExceededErrorResponse' - title: Response 400 Listblockdata - description: Bad Request - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/NotSupportedErrorResponse' - description: Not Found - /NodeInfo/BlockData/{block_number}: - get: - tags: - - node_info - summary: '[ibet Blockchain Explorer] Retrieve block data' - description: Returns block data in the specified block number. - operationId: GetBlockData - parameters: - - name: block_number - in: path - required: true - schema: - type: integer - minimum: 0 - description: Block number - title: Block Number - description: Block number - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/GenericSuccessResponse_BlockDataResponse_' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/RequestValidationErrorResponse' - description: Bad Request - '404': - content: - application/json: - schema: - anyOf: - - $ref: '#/components/schemas/DataNotExistsErrorResponse' - - $ref: '#/components/schemas/NotSupportedErrorResponse' - title: Response 404 Getblockdata - description: Not Found - /NodeInfo/TxData: - get: - tags: - - node_info - summary: '[ibet Blockchain Explorer] List tx data' - description: |- - Returns a list of transactions by various search parameters. - The maximum number of search results is 10000. - operationId: ListTxData - parameters: - - name: offset - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - description: Offset for pagination - title: Offset - description: Offset for pagination - - name: limit - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - description: Limit for pagination - title: Limit - description: Limit for pagination - - name: block_number - in: query - required: false - schema: - anyOf: - - type: integer - minimum: 0 - - type: 'null' - description: block number - title: Block Number - description: block number - - name: from_address - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - description: tx from - title: From Address - description: tx from - - name: to_address - in: query - required: false - schema: - anyOf: - - type: string - - type: 'null' - description: tx to - title: To Address - description: tx to - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/GenericSuccessResponse_TxDataListResponse_' - '400': - content: - application/json: - schema: - anyOf: - - $ref: '#/components/schemas/RequestValidationErrorResponse' - - $ref: '#/components/schemas/ResponseLimitExceededErrorResponse' - title: Response 400 Listtxdata - description: Bad Request - '404': - content: - application/json: - schema: - $ref: '#/components/schemas/NotSupportedErrorResponse' - description: Not Found - /NodeInfo/TxData/{hash}: - get: - tags: - - node_info - summary: '[ibet Blockchain Explorer] Retrieve transaction data' - description: Searching for the transaction by transaction hash - operationId: GetTxData - parameters: - - name: hash - in: path - required: true - schema: - type: string - description: Transaction hash - title: Hash - description: Transaction hash - responses: - '200': - description: Successful Response - content: - application/json: - schema: - $ref: '#/components/schemas/GenericSuccessResponse_TxDataResponse_' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/RequestValidationErrorResponse' - description: Bad Request - '404': - content: - application/json: - schema: - anyOf: - - $ref: '#/components/schemas/DataNotExistsErrorResponse' - - $ref: '#/components/schemas/NotSupportedErrorResponse' - title: Response 404 Gettxdata - description: Not Found /ABI/StraightBond: get: tags: @@ -6201,7 +5956,7 @@ components: AgreementSet_TokenAddress_: properties: token: - $ref: '#/components/schemas/app__model__schema__dex_order_list__TokenAddress' + $ref: '#/components/schemas/app__model__schema__position__TokenAddress' agreement: $ref: '#/components/schemas/Agreement' sort_id: @@ -6262,147 +6017,6 @@ components: - 3 - 4 title: ApprovalStatus - BlockData: - properties: - number: - type: integer - minimum: 0.0 - title: Number - description: Block number - hash: - type: string - title: Hash - description: Block hash - transactions: - items: - type: string - type: array - title: Transactions - description: Transaction list - timestamp: - type: integer - title: Timestamp - gas_limit: - type: integer - title: Gas Limit - gas_used: - type: integer - title: Gas Used - size: - type: integer - minimum: 0.0 - title: Size - type: object - required: - - number - - hash - - transactions - - timestamp - - gas_limit - - gas_used - - size - title: BlockData - BlockDataDetail: - properties: - number: - type: integer - minimum: 0.0 - title: Number - description: Block number - parent_hash: - type: string - title: Parent Hash - sha3_uncles: - type: string - title: Sha3 Uncles - miner: - type: string - title: Miner - state_root: - type: string - title: State Root - transactions_root: - type: string - title: Transactions Root - receipts_root: - type: string - title: Receipts Root - logs_bloom: - type: string - title: Logs Bloom - difficulty: - type: integer - title: Difficulty - gas_limit: - type: integer - title: Gas Limit - gas_used: - type: integer - title: Gas Used - timestamp: - type: integer - title: Timestamp - proof_of_authority_data: - type: string - title: Proof Of Authority Data - mix_hash: - type: string - title: Mix Hash - nonce: - type: string - title: Nonce - hash: - type: string - title: Hash - description: Block hash - size: - type: integer - minimum: 0.0 - title: Size - transactions: - items: - type: string - type: array - title: Transactions - description: Transaction list - type: object - required: - - number - - parent_hash - - sha3_uncles - - miner - - state_root - - transactions_root - - receipts_root - - logs_bloom - - difficulty - - gas_limit - - gas_used - - timestamp - - proof_of_authority_data - - mix_hash - - nonce - - hash - - size - - transactions - title: BlockDataDetail - BlockDataListResponse: - properties: - result_set: - $ref: '#/components/schemas/ResultSet' - block_data: - items: - $ref: '#/components/schemas/BlockData' - type: array - title: Block Data - type: object - required: - - result_set - - block_data - title: BlockDataListResponse - BlockDataResponse: - $ref: '#/components/schemas/BlockDataDetail' - title: BlockDataResponse BlockIdentifier: type: string enum: @@ -6692,7 +6306,7 @@ components: CompleteAgreementSet_TokenAddress_: properties: token: - $ref: '#/components/schemas/app__model__schema__dex_order_list__TokenAddress' + $ref: '#/components/schemas/app__model__schema__position__TokenAddress' agreement: $ref: '#/components/schemas/Agreement' sort_id: @@ -7156,34 +6770,6 @@ components: - meta - data title: GenericSuccessResponse[Any] - GenericSuccessResponse_BlockDataListResponse_: - properties: - meta: - $ref: '#/components/schemas/Success200MetaModel' - examples: - - code: 200 - message: OK - data: - $ref: '#/components/schemas/BlockDataListResponse' - type: object - required: - - meta - - data - title: GenericSuccessResponse[BlockDataListResponse] - GenericSuccessResponse_BlockDataResponse_: - properties: - meta: - $ref: '#/components/schemas/Success200MetaModel' - examples: - - code: 200 - message: OK - data: - $ref: '#/components/schemas/BlockDataResponse' - type: object - required: - - meta - - data - title: GenericSuccessResponse[BlockDataResponse] GenericSuccessResponse_CouponPositionWithDetail_: properties: meta: @@ -8078,34 +7664,6 @@ components: - meta - data title: GenericSuccessResponse[TransferHistoriesResponse] - GenericSuccessResponse_TxDataListResponse_: - properties: - meta: - $ref: '#/components/schemas/Success200MetaModel' - examples: - - code: 200 - message: OK - data: - $ref: '#/components/schemas/TxDataListResponse' - type: object - required: - - meta - - data - title: GenericSuccessResponse[TxDataListResponse] - GenericSuccessResponse_TxDataResponse_: - properties: - meta: - $ref: '#/components/schemas/Success200MetaModel' - examples: - - code: 200 - message: OK - data: - $ref: '#/components/schemas/TxDataResponse' - type: object - required: - - meta - - data - title: GenericSuccessResponse[TxDataResponse] GenericSuccessResponse_WaitForTransactionReceiptResponse_: properties: meta: @@ -9710,7 +9268,7 @@ components: OrderSet_TokenAddress_: properties: token: - $ref: '#/components/schemas/app__model__schema__dex_order_list__TokenAddress' + $ref: '#/components/schemas/app__model__schema__position__TokenAddress' order: $ref: '#/components/schemas/Order' sort_id: @@ -9904,46 +9462,6 @@ components: required: - meta title: RequestValidationErrorResponse - ResponseLimitExceededErrorMetainfo: - properties: - message: - type: string - const: Response Limit Exceeded - title: Message - examples: - - Response Limit Exceeded - code: - type: integer - const: 30 - title: Code - examples: - - 30 - description: - anyOf: - - type: string - - type: object - - type: 'null' - title: Description - type: object - required: - - message - - code - title: ResponseLimitExceededErrorMetainfo - ResponseLimitExceededErrorResponse: - properties: - meta: - $ref: '#/components/schemas/ResponseLimitExceededErrorMetainfo' - details: - anyOf: - - type: object - - type: 'null' - title: Details - examples: - - null - type: object - required: - - meta - title: ResponseLimitExceededErrorResponse ResultSet: properties: count: @@ -10553,7 +10071,7 @@ components: token: anyOf: - $ref: '#/components/schemas/RetrieveShareTokenResponse' - - $ref: '#/components/schemas/app__model__schema__dex_order_list__TokenAddress' + - $ref: '#/components/schemas/app__model__schema__position__TokenAddress' title: Token description: set when include_token_details=false or null type: object @@ -10587,7 +10105,7 @@ components: token: anyOf: - $ref: '#/components/schemas/RetrieveStraightBondTokenResponse' - - $ref: '#/components/schemas/app__model__schema__dex_order_list__TokenAddress' + - $ref: '#/components/schemas/app__model__schema__position__TokenAddress' title: Token description: set when include_token_details=false or null type: object @@ -11639,129 +11157,6 @@ components: - corporate_number - corporate_address title: Trustee - TxData: - properties: - hash: - type: string - title: Hash - description: Transaction hash - block_hash: - type: string - title: Block Hash - block_number: - type: integer - minimum: 0.0 - title: Block Number - transaction_index: - type: integer - minimum: 0.0 - title: Transaction Index - from_address: - type: string - title: From Address - to_address: - anyOf: - - type: string - - type: 'null' - title: To Address - type: object - required: - - hash - - block_hash - - block_number - - transaction_index - - from_address - - to_address - title: TxData - TxDataDetail: - properties: - hash: - type: string - title: Hash - description: Transaction hash - block_hash: - type: string - title: Block Hash - block_number: - type: integer - minimum: 0.0 - title: Block Number - transaction_index: - type: integer - minimum: 0.0 - title: Transaction Index - from_address: - type: string - title: From Address - to_address: - anyOf: - - type: string - - type: 'null' - title: To Address - contract_name: - anyOf: - - type: string - - type: 'null' - title: Contract Name - contract_function: - anyOf: - - type: string - - type: 'null' - title: Contract Function - contract_parameters: - anyOf: - - type: object - - type: 'null' - title: Contract Parameters - gas: - type: integer - minimum: 0.0 - title: Gas - gas_price: - type: integer - minimum: 0.0 - title: Gas Price - value: - type: integer - minimum: 0.0 - title: Value - nonce: - type: integer - minimum: 0.0 - title: Nonce - type: object - required: - - hash - - block_hash - - block_number - - transaction_index - - from_address - - to_address - - contract_name - - contract_function - - contract_parameters - - gas - - gas_price - - value - - nonce - title: TxDataDetail - TxDataListResponse: - properties: - result_set: - $ref: '#/components/schemas/ResultSet' - tx_data: - items: - $ref: '#/components/schemas/TxData' - type: array - title: Tx Data - type: object - required: - - result_set - - tx_data - title: TxDataListResponse - TxDataResponse: - $ref: '#/components/schemas/TxDataDetail' - title: TxDataResponse UpdateAdminTokenRequest: properties: is_public: @@ -11866,7 +11261,7 @@ components: required: - status title: WaitForTransactionReceiptResponse - app__model__schema__dex_order_list__TokenAddress: + app__model__schema__position__TokenAddress: properties: token_address: type: string diff --git a/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py b/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py new file mode 100644 index 0000000000..f770bbe01e --- /dev/null +++ b/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py @@ -0,0 +1,180 @@ +"""v26_3_0_feature_1748 + +Revision ID: 1cd2ee459858 +Revises: 835dd5b51e23 +Create Date: 2026-01-30 16:10:08.786675 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +from app.database import get_db_schema + +# revision identifiers, used by Alembic. +revision = "1cd2ee459858" +down_revision = "835dd5b51e23" +branch_labels = None +depends_on = None + + +def _schema_name() -> str: + return get_db_schema() or "public" + + +def upgrade(): + schema = _schema_name() + + op.execute(f'DROP INDEX IF EXISTS {schema}."ix_tx_data_block_number"') + op.execute(f'DROP INDEX IF EXISTS {schema}."ix_tx_data_from_address"') + op.execute(f'DROP INDEX IF EXISTS {schema}."ix_tx_data_to_address"') + op.execute(f"DROP TABLE IF EXISTS {schema}.tx_data") + + op.execute(f'DROP INDEX IF EXISTS {schema}."ix_block_data_hash"') + op.execute(f'DROP INDEX IF EXISTS {schema}."ix_block_data_timestamp"') + op.execute(f"DROP TABLE IF EXISTS {schema}.block_data") + + op.execute(f"DROP TABLE IF EXISTS {schema}.idx_block_data_block_number") + + +def downgrade(): + connection = op.get_bind() + + op.create_table( + "idx_block_data_block_number", + sa.Column( + "created", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "modified", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "chain_id", sa.VARCHAR(length=10), autoincrement=False, nullable=False + ), + sa.Column( + "latest_block_number", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.PrimaryKeyConstraint( + "chain_id", name=op.f("idx_block_data_block_number_pkey") + ), + schema=get_db_schema(), + ) + op.create_table( + "block_data", + sa.Column( + "created", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "modified", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("number", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "parent_hash", sa.VARCHAR(length=66), autoincrement=False, nullable=False + ), + sa.Column( + "sha3_uncles", sa.VARCHAR(length=66), autoincrement=False, nullable=True + ), + sa.Column("miner", sa.VARCHAR(length=42), autoincrement=False, nullable=True), + sa.Column( + "state_root", sa.VARCHAR(length=66), autoincrement=False, nullable=True + ), + sa.Column( + "transactions_root", + sa.VARCHAR(length=66), + autoincrement=False, + nullable=True, + ), + sa.Column( + "receipts_root", sa.VARCHAR(length=66), autoincrement=False, nullable=True + ), + sa.Column( + "logs_bloom", sa.VARCHAR(length=514), autoincrement=False, nullable=True + ), + sa.Column("difficulty", sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column("gas_limit", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("gas_used", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("timestamp", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column( + "proof_of_authority_data", sa.TEXT(), autoincrement=False, nullable=True + ), + sa.Column( + "mix_hash", sa.VARCHAR(length=66), autoincrement=False, nullable=True + ), + sa.Column("nonce", sa.VARCHAR(length=18), autoincrement=False, nullable=True), + sa.Column("hash", sa.VARCHAR(length=66), autoincrement=False, nullable=False), + sa.Column("size", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "transactions", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + sa.PrimaryKeyConstraint("number", name=op.f("block_data_pkey")), + schema=get_db_schema(), + ) + op.create_index( + op.f("ix_block_data_timestamp"), + "block_data", + ["timestamp"], + unique=False, + schema=get_db_schema(), + ) + op.create_index( + op.f("ix_block_data_hash"), + "block_data", + ["hash"], + unique=False, + schema=get_db_schema(), + ) + op.create_table( + "tx_data", + sa.Column( + "created", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "modified", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("hash", sa.VARCHAR(length=66), autoincrement=False, nullable=False), + sa.Column( + "block_hash", sa.VARCHAR(length=66), autoincrement=False, nullable=True + ), + sa.Column("block_number", sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column( + "transaction_index", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "from_address", sa.VARCHAR(length=42), autoincrement=False, nullable=True + ), + sa.Column( + "to_address", sa.VARCHAR(length=42), autoincrement=False, nullable=True + ), + sa.Column("input", sa.TEXT(), autoincrement=False, nullable=True), + sa.Column("gas", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("gas_price", sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column("value", sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column("nonce", sa.INTEGER(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint("hash", name=op.f("tx_data_pkey")), + schema=get_db_schema(), + ) + op.create_index( + op.f("ix_tx_data_to_address"), + "tx_data", + ["to_address"], + unique=False, + schema=get_db_schema(), + ) + op.create_index( + op.f("ix_tx_data_from_address"), + "tx_data", + ["from_address"], + unique=False, + schema=get_db_schema(), + ) + op.create_index( + op.f("ix_tx_data_block_number"), + "tx_data", + ["block_number"], + unique=False, + schema=get_db_schema(), + ) diff --git a/pyproject.toml b/pyproject.toml index 3085fbbc9c..bcd83a2592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,21 +61,9 @@ dev = [ "pyright>=1.1.407", "types-botocore>=1.0.2", "boto3-stubs[all]>=1.42.29", -] - -[project.optional-dependencies] -ibet-explorer = [ - "ibet-wallet-api-explorer", - "textual~=0.44.1", - "async-cache~=1.1.1", "typer~=0.12.3", - "aiohttp~=3.13.3", - "async-cache==1.1.1", ] -[tool.uv.sources] -ibet-wallet-api-explorer = { path = "cmd/explorer", editable = true } - [tool.ruff] line-length = 88 indent-width = 4 diff --git a/tests/Dockerfile_unittest b/tests/Dockerfile_unittest index 55e7abeff9..6cb1d57bc9 100644 --- a/tests/Dockerfile_unittest +++ b/tests/Dockerfile_unittest @@ -56,7 +56,7 @@ RUN echo '. $HOME/.venv/bin/activate' >> ~apl/.bashrc COPY --chown=apl:apl . /app/ibet-Wallet-API RUN cd /app/ibet-Wallet-API \ && uv venv $UV_PROJECT_ENVIRONMENT \ - && uv sync --frozen --no-install-project --extra ibet-explorer + && uv sync --frozen --no-install-project FROM ubuntu:24.04 AS runner diff --git a/tests/app/node_info_GetBlockData_test.py b/tests/app/node_info_GetBlockData_test.py deleted file mode 100644 index 653d1aa9c4..0000000000 --- a/tests/app/node_info_GetBlockData_test.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Any - -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session - -from app import config -from app.model.db import IDXBlockData - - -class TestGetBlockData: - # API to be tested - apiurl = "/NodeInfo/BlockData/{}" - - # Test data - block_0: dict[str, Any] = { - "number": 0, - "difficulty": 1, - "proof_of_authority_data": "0x0000000000000000000000000000000000000000000000000000000000000000f89af8549447a847fbdf801154253593851ac9a2e7753235349403ee8c85944b16dfa517cb0ddefe123c7341a5349435d56a7515e824be4122f033d60063d035573a0c94c25d04978fd86ee604feb88f3c635d555eb6d42db8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", - "gas_limit": 800000000, - "gas_used": 0, - "hash": "0x307166a396b99259ed2072f4b99d850e332db4ef4c72656870680728944ad445", - "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner": "0x0000000000000000000000000000000000000000", - "mix_hash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", - "nonce": "0x0000000000000000", - "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sha3_uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "size": 698, - "state_root": "0x260aa025c613224aaa46e2134b6469f3d956c07949a6ca23143936c574838995", - "timestamp": 1524130695, - "transactions": [], - "transactions_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - } - block_1: dict[str, Any] = { - "number": 1, - "difficulty": 1, - "proof_of_authority_data": "0xd983010907846765746889676f312e31332e3135856c696e7578000000000000f90164f8549403ee8c85944b16dfa517cb0ddefe123c7341a5349435d56a7515e824be4122f033d60063d035573a0c9447a847fbdf801154253593851ac9a2e77532353494c25d04978fd86ee604feb88f3c635d555eb6d42db841d6b123b02f015f4f0b0d47648e22c043dca3f803e6498084522c07ccbea58f8c39e498f6c8c604abb406dc323246c30525cc0dd01c39bcadbb608733bdf3f08300f8c9b84186503892a4b314f2b8aba73b1a7434c69bd95695ac285d5db6d15f879b5dbbf83471502e13eb1d0fce4d2e7ea41be91e8ecf744f0eeb8c808fb2e4a7833377f901b84141f6e74ea7f1365fa01ab90318c066f05b65fe82a2fd856203407653aa242c5875bd016f68d169951aec61958f4ad9654ecc1c8edc686e55c601e3db45f4cc8a01b8414e3259ec82771a0c83d0b5db72c0199549efa80736e6e1f892c50504d187eb505f784cb8623ce475195fe1287ceb85bccf703fca919da23940ce29e4f9f1a7ba01", - "gas_limit": 800000000, - "gas_used": 0, - "hash": "0xa4852f7e1b8ce036b057087e54492524796e0a68bba07a1c110605cf2cb8c01d", - "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner": "0x0000000000000000000000000000000000000000", - "mix_hash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", - "nonce": "0x0000000000000000", - "parent_hash": "0x307166a396b99259ed2072f4b99d850e332db4ef4c72656870680728944ad445", - "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sha3_uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "size": 902, - "state_root": "0x260aa025c613224aaa46e2134b6469f3d956c07949a6ca23143936c574838995", - "timestamp": 1638960161, - "transactions": [], - "transactions_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - } - block_2: dict[str, Any] = { - "number": 2, - "difficulty": 1, - "proof_of_authority_data": "0xd983010907846765746889676f312e31332e3135856c696e7578000000000000f90164f8549403ee8c85944b16dfa517cb0ddefe123c7341a5349435d56a7515e824be4122f033d60063d035573a0c9447a847fbdf801154253593851ac9a2e77532353494c25d04978fd86ee604feb88f3c635d555eb6d42db8416ac645ecda42d11985f8d00d25664e336e8588d3bf595a88eb78ef200e5aa0f732a4822141420fba7ccf33248ed5a45038334c5e50561779e427a4fe4b04b08a00f8c9b8415568d4c4a3edcafaed146a43dc7046d9fcd31d5f6d64f8dcf581ba473b868f5145129066188ffef9b8a9e9065d13fa96a81f1196bad98393a70f6461245c097a01b841ea249f9f16bc3c5a13de6df4be7b6933b4908395d1517f7099cc7718ec438a8744aaa356584a3929d1dec2d596268ddbd29d476954d2080b94f82b07b223492300b841804f0f78afa930c7249a81cc3a7a2810098803c6f6cb4c2ed3778c1a0c35962e57c8a1328f670362cfbe3f30e58cac7d78f9059e379bd9c195afecb27b907faf01", - "gas_limit": 800000000, - "gas_used": 0, - "hash": "0x8aade0ef6b0c8a2854b7d71503fe74b8a1edc13e95f7f117467ec2a327ea6f99", - "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner": "0x0000000000000000000000000000000000000000", - "mix_hash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", - "nonce": "0x0000000000000000", - "parent_hash": "0xa4852f7e1b8ce036b057087e54492524796e0a68bba07a1c110605cf2cb8c01d", - "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sha3_uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "size": 902, - "state_root": "0x260aa025c613224aaa46e2134b6469f3d956c07949a6ca23143936c574838995", - "timestamp": 1638960172, - "transactions": [], - "transactions_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - } - - @staticmethod - def insert_block_data(session: Session, block_data: dict[str, Any]) -> None: - block_model = IDXBlockData() - block_model.number = block_data["number"] - block_model.parent_hash = block_data["parent_hash"] - block_model.sha3_uncles = block_data.get("sha3_uncles") - block_model.miner = block_data.get("miner") - block_model.state_root = block_data.get("state_root") - block_model.transactions_root = block_data.get("transactions_root") - block_model.receipts_root = block_data.get("receipts_root") - block_model.logs_bloom = block_data.get("logs_bloom") - block_model.difficulty = block_data.get("difficulty") - block_model.gas_limit = block_data.get("gas_limit") - block_model.gas_used = block_data.get("gas_used") - block_model.timestamp = block_data["timestamp"] - block_model.proof_of_authority_data = block_data.get("proof_of_authority_data") - block_model.mix_hash = block_data.get("mix_hash") - block_model.nonce = block_data.get("nonce") - block_model.hash = block_data["hash"] - block_model.size = block_data.get("size") - block_model.transactions = block_data.get("transactions") - session.add(block_model) - session.commit() - - ########################################################################### - # Normal - ########################################################################### - - # Normal_1 - def test_normal_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - # Request target API - resp = client.get(self.apiurl.format(1)) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - assert resp.json()["data"] == self.block_1 - - ########################################################################### - # Error - ########################################################################### - - # Error_1 - # BC_EXPLORER_ENABLED = False (default) - def test_error_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = False - - # Request target API - resp = client.get(self.apiurl.format(0)) - - # Assertion - assert resp.status_code == 404 - assert resp.json()["meta"] == { - "code": 10, - "message": "Not Supported", - "description": "method: GET, url: /NodeInfo/BlockData/0", - } - - # Error_2 - # DataNotExistsError - def test_error_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - resp = client.get(self.apiurl.format(0)) - - # Assertion - assert resp.status_code == 404 - assert resp.json()["meta"] == {"code": 30, "message": "Data Not Exists"} - - # Error_3 - # Invalid Parameter - def test_error_3(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - resp = client.get(self.apiurl.format(-1)) - - # Assertion - assert resp.status_code == 400 - assert resp.json()["meta"] == { - "code": 88, - "message": "Invalid Parameter", - "description": [ - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["path", "block_number"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - } - ], - } diff --git a/tests/app/node_info_GetTxData_test.py b/tests/app/node_info_GetTxData_test.py deleted file mode 100644 index 4ca50e996a..0000000000 --- a/tests/app/node_info_GetTxData_test.py +++ /dev/null @@ -1,188 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Any - -from eth_utils.address import to_checksum_address -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session - -from app import config -from app.model.db import IDXTokenListRegister, IDXTxData - - -class TestGetTxData: - # API to be tested - apiurl = "/NodeInfo/TxData/{}" - - # Test data - tx_data = { - "block_hash": "0x6698ebc4866223855dbea153eec7a9455682fd6d2f8451746102afb320412a6b", - "block_number": 10407084, - "from_address": "0x8456ac4FEc4869A16ad5C3584306181Af6410682", - "to_address": "0xECeB9FdBd2CF677Be5fA8B1ceEb53e53D582f0Eb", - "gas": 6000000, - "gas_price": 0, - "hash": "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644", - "input": "0x5ccef3e7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011313635323937363632372e363337373738000000000000000000000000000000", - "nonce": 199601, - "transaction_index": 0, - "value": 0, - } - - @staticmethod - def insert_tx_data(session: Session, tx_data: dict[str, Any]) -> None: - tx_model = IDXTxData() - tx_model.hash = tx_data["hash"] - tx_model.block_hash = tx_data.get("block_hash") - tx_model.block_number = tx_data.get("block_number") - tx_model.transaction_index = tx_data.get("transaction_index") - tx_model.from_address = tx_data.get("from_address") - tx_model.to_address = tx_data.get("to_address") - tx_model.input = tx_data.get("input") - tx_model.gas = tx_data.get("gas") - tx_model.gas_price = tx_data.get("gas_price") - tx_model.value = tx_data.get("value") - tx_model.nonce = tx_data.get("nonce") - session.add(tx_model) - session.commit() - - @staticmethod - def insert_token_list(session: Session, token_info: dict[str, Any]) -> None: - token = IDXTokenListRegister() - token.token_address = token_info["token_address"] - token.token_template = token_info.get("token_template") - session.add(token) - session.commit() - - ########################################################################### - # Normal - ########################################################################### - - # Normal_1 - # Contract information is not set - def test_normal_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.tx_data) - - # Request target API - resp = client.get( - self.apiurl.format( - "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644" - ) - ) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - assert resp.json()["data"] == { - "hash": "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644", - "block_hash": "0x6698ebc4866223855dbea153eec7a9455682fd6d2f8451746102afb320412a6b", - "block_number": 10407084, - "transaction_index": 0, - "from_address": "0x8456ac4FEc4869A16ad5C3584306181Af6410682", - "to_address": "0xECeB9FdBd2CF677Be5fA8B1ceEb53e53D582f0Eb", - "contract_name": None, - "contract_function": None, - "contract_parameters": None, - "gas": 6000000, - "gas_price": 0, - "value": 0, - "nonce": 199601, - } - - # Normal_2 - # Contract information is set - def test_normal_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.tx_data) - - token_info = { - "token_address": to_checksum_address(str(self.tx_data["to_address"])), - "token_template": "IbetShare", - } - self.insert_token_list(session, token_info) - - # Request target API - resp = client.get( - self.apiurl.format( - "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644" - ) - ) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - assert resp.json()["data"] == { - "hash": "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644", - "block_hash": "0x6698ebc4866223855dbea153eec7a9455682fd6d2f8451746102afb320412a6b", - "block_number": 10407084, - "transaction_index": 0, - "from_address": "0x8456ac4FEc4869A16ad5C3584306181Af6410682", - "to_address": "0xECeB9FdBd2CF677Be5fA8B1ceEb53e53D582f0Eb", - "contract_name": "IbetShare", - "contract_function": "approveTransfer", - "contract_parameters": {"_index": 1, "_data": "1652976627.637778"}, - "gas": 6000000, - "gas_price": 0, - "value": 0, - "nonce": 199601, - } - - ########################################################################### - # Error - ########################################################################### - - # Error_1 - # BC_EXPLORER_ENABLED = False (default) - def test_error_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = False - - # Request target API - resp = client.get( - self.apiurl.format( - "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644" - ) - ) - - # Assertion - assert resp.status_code == 404 - assert resp.json()["meta"] == { - "code": 10, - "message": "Not Supported", - "description": "method: GET, url: /NodeInfo/TxData/0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644", - } - - # Error_2 - # DataNotExistsError - def test_error_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - resp = client.get( - self.apiurl.format( - "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644" - ) - ) - - # Assertion - assert resp.status_code == 404 - assert resp.json()["meta"] == {"code": 30, "message": "Data Not Exists"} diff --git a/tests/app/node_info_ListBlockData_test.py b/tests/app/node_info_ListBlockData_test.py deleted file mode 100644 index 0c77e820e1..0000000000 --- a/tests/app/node_info_ListBlockData_test.py +++ /dev/null @@ -1,435 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Any -from unittest import mock - -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session - -from app import config -from app.model.db import IDXBlockData, IDXBlockDataBlockNumber - - -class TestListBlockData: - # API to be tested - apiurl = "/NodeInfo/BlockData" - - # Test data - block_0: dict[str, Any] = { - "number": 0, - "difficulty": 1, - "proof_of_authority_data": "0x0000000000000000000000000000000000000000000000000000000000000000f89af8549447a847fbdf801154253593851ac9a2e7753235349403ee8c85944b16dfa517cb0ddefe123c7341a5349435d56a7515e824be4122f033d60063d035573a0c94c25d04978fd86ee604feb88f3c635d555eb6d42db8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", - "gas_limit": 800000000, - "gas_used": 0, - "hash": "0x307166a396b99259ed2072f4b99d850e332db4ef4c72656870680728944ad445", - "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner": "0x0000000000000000000000000000000000000000", - "mix_hash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", - "nonce": "0x0000000000000000", - "parent_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sha3_uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "size": 698, - "state_root": "0x260aa025c613224aaa46e2134b6469f3d956c07949a6ca23143936c574838995", - "timestamp": 1524130695, - "transactions": [], - "transactions_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - } - block_1: dict[str, Any] = { - "number": 1, - "difficulty": 1, - "proof_of_authority_data": "0xd983010907846765746889676f312e31332e3135856c696e7578000000000000f90164f8549403ee8c85944b16dfa517cb0ddefe123c7341a5349435d56a7515e824be4122f033d60063d035573a0c9447a847fbdf801154253593851ac9a2e77532353494c25d04978fd86ee604feb88f3c635d555eb6d42db841d6b123b02f015f4f0b0d47648e22c043dca3f803e6498084522c07ccbea58f8c39e498f6c8c604abb406dc323246c30525cc0dd01c39bcadbb608733bdf3f08300f8c9b84186503892a4b314f2b8aba73b1a7434c69bd95695ac285d5db6d15f879b5dbbf83471502e13eb1d0fce4d2e7ea41be91e8ecf744f0eeb8c808fb2e4a7833377f901b84141f6e74ea7f1365fa01ab90318c066f05b65fe82a2fd856203407653aa242c5875bd016f68d169951aec61958f4ad9654ecc1c8edc686e55c601e3db45f4cc8a01b8414e3259ec82771a0c83d0b5db72c0199549efa80736e6e1f892c50504d187eb505f784cb8623ce475195fe1287ceb85bccf703fca919da23940ce29e4f9f1a7ba01", - "gas_limit": 800000000, - "gas_used": 0, - "hash": "0xa4852f7e1b8ce036b057087e54492524796e0a68bba07a1c110605cf2cb8c01d", - "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner": "0x0000000000000000000000000000000000000000", - "mix_hash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", - "nonce": "0x0000000000000000", - "parent_hash": "0x307166a396b99259ed2072f4b99d850e332db4ef4c72656870680728944ad445", - "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sha3_uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "size": 902, - "state_root": "0x260aa025c613224aaa46e2134b6469f3d956c07949a6ca23143936c574838995", - "timestamp": 1638960161, - "transactions": [], - "transactions_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - } - block_2: dict[str, Any] = { - "number": 2, - "difficulty": 1, - "proof_of_authority_data": "0xd983010907846765746889676f312e31332e3135856c696e7578000000000000f90164f8549403ee8c85944b16dfa517cb0ddefe123c7341a5349435d56a7515e824be4122f033d60063d035573a0c9447a847fbdf801154253593851ac9a2e77532353494c25d04978fd86ee604feb88f3c635d555eb6d42db8416ac645ecda42d11985f8d00d25664e336e8588d3bf595a88eb78ef200e5aa0f732a4822141420fba7ccf33248ed5a45038334c5e50561779e427a4fe4b04b08a00f8c9b8415568d4c4a3edcafaed146a43dc7046d9fcd31d5f6d64f8dcf581ba473b868f5145129066188ffef9b8a9e9065d13fa96a81f1196bad98393a70f6461245c097a01b841ea249f9f16bc3c5a13de6df4be7b6933b4908395d1517f7099cc7718ec438a8744aaa356584a3929d1dec2d596268ddbd29d476954d2080b94f82b07b223492300b841804f0f78afa930c7249a81cc3a7a2810098803c6f6cb4c2ed3778c1a0c35962e57c8a1328f670362cfbe3f30e58cac7d78f9059e379bd9c195afecb27b907faf01", - "gas_limit": 800000000, - "gas_used": 0, - "hash": "0x8aade0ef6b0c8a2854b7d71503fe74b8a1edc13e95f7f117467ec2a327ea6f99", - "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner": "0x0000000000000000000000000000000000000000", - "mix_hash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", - "nonce": "0x0000000000000000", - "parent_hash": "0xa4852f7e1b8ce036b057087e54492524796e0a68bba07a1c110605cf2cb8c01d", - "receipts_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sha3_uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "size": 902, - "state_root": "0x260aa025c613224aaa46e2134b6469f3d956c07949a6ca23143936c574838995", - "timestamp": 1638960172, - "transactions": [], - "transactions_root": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - } - - @staticmethod - def filter_response_item(block_data: dict[str, Any]) -> dict[str, Any]: - return { - "number": block_data.get("number"), - "hash": block_data.get("hash"), - "transactions": block_data.get("transactions"), - "timestamp": block_data.get("timestamp"), - "gas_limit": block_data.get("gas_limit"), - "gas_used": block_data.get("gas_used"), - "size": block_data.get("size"), - } - - @staticmethod - def insert_block_data(session: Session, block_data: dict[str, Any]) -> None: - block_model = IDXBlockData() - block_model.number = block_data["number"] - block_model.parent_hash = block_data["parent_hash"] - block_model.sha3_uncles = block_data.get("sha3_uncles") - block_model.miner = block_data.get("miner") - block_model.state_root = block_data.get("state_root") - block_model.transactions_root = block_data.get("transactions_root") - block_model.receipts_root = block_data.get("receipts_root") - block_model.logs_bloom = block_data.get("logs_bloom") - block_model.difficulty = block_data.get("difficulty") - block_model.gas_limit = block_data.get("gas_limit") - block_model.gas_used = block_data.get("gas_used") - block_model.timestamp = block_data["timestamp"] - block_model.proof_of_authority_data = block_data.get("proof_of_authority_data") - block_model.mix_hash = block_data.get("mix_hash") - block_model.nonce = block_data.get("nonce") - block_model.hash = block_data["hash"] - block_model.size = block_data.get("size") - block_model.transactions = block_data.get("transactions") - session.add(block_model) - session.commit() - - @staticmethod - def insert_block_data_block_number(session: Session, latest_block_number: int): - idx_block_data_block_number = IDXBlockDataBlockNumber() - idx_block_data_block_number.chain_id = config.WEB3_CHAINID - idx_block_data_block_number.latest_block_number = latest_block_number - session.add(idx_block_data_block_number) - session.commit() - - ########################################################################### - # Normal - ########################################################################### - - # Normal_1_1 - # IDXBlockDataBlockNumber is None - def test_normal_1_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - resp = client.get(self.apiurl, params={}) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - assert resp.json()["data"] == { - "result_set": {"count": 0, "offset": None, "limit": None, "total": 0}, - "block_data": [], - } - - # Normal_1_2 - def test_normal_1_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - resp = client.get(self.apiurl) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["block_data"] == [ - self.filter_response_item(self.block_0), - self.filter_response_item(self.block_1), - self.filter_response_item(self.block_2), - ] - - # Normal_2_1 - # Query parameter: from_block_number - def test_normal_2_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - params = {"from_block_number": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 2, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["block_data"] == [ - self.filter_response_item(self.block_1), - self.filter_response_item(self.block_2), - ] - - # Normal_2_2 - # Query parameter: to_block_number - def test_normal_2_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - params = {"to_block_number": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 2, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["block_data"] == [ - self.filter_response_item(self.block_0), - self.filter_response_item(self.block_1), - ] - - # Normal_3_1 - # Pagination: offset - def test_normal_3_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - params = {"offset": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": 1, - "limit": None, - "total": 3, - } - assert response_data["block_data"] == [ - self.filter_response_item(self.block_1), - self.filter_response_item(self.block_2), - ] - - # Normal_3_2 - # Pagination: limit - def test_normal_3_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - params = {"limit": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": None, - "limit": 1, - "total": 3, - } - assert response_data["block_data"] == [self.filter_response_item(self.block_0)] - - # Normal_4 - # Pagination: sort_order - def test_normal_4(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - params = {"sort_order": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["block_data"] == [ - self.filter_response_item(self.block_2), - self.filter_response_item(self.block_1), - self.filter_response_item(self.block_0), - ] - - ########################################################################### - # Error - ########################################################################### - - # Error_1 - # BC_EXPLORER_ENABLED = False (default) - def test_error_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = False - - # Request target API - resp = client.get(self.apiurl) - - # Assertion - assert resp.status_code == 404 - assert resp.json()["meta"] == { - "code": 10, - "message": "Not Supported", - "description": "method: GET, url: /NodeInfo/BlockData", - } - - # Error_2 - # Invalid Parameter - def test_error_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - params = { - "offset": -1, - "limit": -1, - "from_block_number": -1, - "to_block_number": -1, - } - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 400 - assert resp.json()["meta"] == { - "code": 88, - "message": "Invalid Parameter", - "description": [ - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "limit"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "from_block_number"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "to_block_number"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - ], - } - - # Error_3 - # ResponseLimitExceededError - def test_error_3(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_block_data(session, self.block_0) - self.insert_block_data(session, self.block_1) - self.insert_block_data(session, self.block_2) - - self.insert_block_data_block_number(session, latest_block_number=2) - - # Request target API - with mock.patch("app.api.routers.bc_explorer.BLOCK_RESPONSE_LIMIT", 2): - resp = client.get(self.apiurl) - - # Assertion - assert resp.status_code == 400 - assert resp.json()["meta"] == { - "code": 30, - "message": "Response Limit Exceeded", - "description": "Search results exceed the limit", - } diff --git a/tests/app/node_info_ListTxData_test.py b/tests/app/node_info_ListTxData_test.py deleted file mode 100644 index 0f830eb6d3..0000000000 --- a/tests/app/node_info_ListTxData_test.py +++ /dev/null @@ -1,381 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -from typing import Any -from unittest import mock - -from fastapi.testclient import TestClient -from sqlalchemy.orm import Session - -from app import config -from app.model.db import IDXTxData - - -class TestListTxData: - # API to be tested - apiurl = "/NodeInfo/TxData" - - # Test data - A_tx_1 = { - "block_hash": "0x94670853c83f3c444d8515cbb9902c9e88b3619c27b9577714baaa07d35874ff", - "block_number": 6791869, - "from_address": "0x30406Cd5f18DD87367B782b9D63b4d79F7f5eBb8", - "to_address": "0x1FBb27d6682aB47654f0150457B64F9A96C926d4", - "gas": 6000000, - "gas_price": 0, - "hash": "0x560f6761de57832d2a1adcd10434f85762c9f833b45717b1cae778f8a23fc6cb", - "input": "0x5ccef3e7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000010313634393237303331302e383734303400000000000000000000000000000000", - "nonce": 86942, - "transaction_index": 0, - "value": 0, - } - A_tx_2 = { - "block_hash": "0x077e42cfe8bc9577b85a6347136c2d38a2b165e7b31bb340c33d302565b900b6", - "block_number": 6791871, - "from_address": "0x30406Cd5f18DD87367B782b9D63b4d79F7f5eBb8", - "to_address": "0x1FBb27d6682aB47654f0150457B64F9A96C926d4", - "gas": 6000000, - "gas_price": 0, - "hash": "0x4ad0b5e395f8c7cc843ba9ffc8b86e6a8b0c71cb724c68b5842839954410892c", - "input": "0x5ccef3e7000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011313634393237303331322e383235303432000000000000000000000000000000", - "nonce": 86943, - "transaction_index": 0, - "value": 0, - } - B_tx_1 = { - "block_hash": "0x6698ebc4866223855dbea153eec7a9455682fd6d2f8451746102afb320412a6b", - "block_number": 10407084, - "from_address": "0x8456ac4FEc4869A16ad5C3584306181Af6410682", - "to_address": "0xECeB9FdBd2CF677Be5fA8B1ceEb53e53D582f0Eb", - "gas": 6000000, - "gas_price": 0, - "hash": "0x403f9cea4db07aecf71a440c45ae415569cb218bb1a7f4d3a2d83004e29d1644", - "input": "0x5ccef3e7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000011313635323937363632372e363337373738000000000000000000000000000000", - "nonce": 199601, - "transaction_index": 0, - "value": 0, - } - - @staticmethod - def filter_response_item(tx_data: dict[str, Any]) -> dict[str, Any]: - return { - "hash": tx_data.get("hash"), - "block_hash": tx_data.get("block_hash"), - "block_number": tx_data.get("block_number"), - "transaction_index": tx_data.get("transaction_index"), - "from_address": tx_data.get("from_address"), - "to_address": tx_data.get("to_address"), - } - - @staticmethod - def insert_tx_data(session: Session, tx_data: dict[str, Any]) -> None: - tx_model = IDXTxData() - tx_model.hash = tx_data["hash"] - tx_model.block_hash = tx_data.get("block_hash") - tx_model.block_number = tx_data.get("block_number") - tx_model.transaction_index = tx_data.get("transaction_index") - tx_model.from_address = tx_data.get("from_address") - tx_model.to_address = tx_data.get("to_address") - tx_model.input = tx_data.get("input") - tx_model.gas = tx_data.get("gas") - tx_model.gas_price = tx_data.get("gas_price") - tx_model.value = tx_data.get("value") - tx_model.nonce = tx_data.get("nonce") - session.add(tx_model) - session.commit() - - ########################################################################### - # Normal - ########################################################################### - - # Normal_1 - def test_normal_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - resp = client.get(self.apiurl) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["tx_data"] == [ - self.filter_response_item(self.B_tx_1), - self.filter_response_item(self.A_tx_2), - self.filter_response_item(self.A_tx_1), - ] - - # Normal_2_1 - # Query parameter: block_number - def test_normal_2_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - params = {"block_number": 6791871} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 1, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["tx_data"] == [self.filter_response_item(self.A_tx_2)] - - # Normal_2_2 - # Query parameter: from_address - def test_normal_2_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - params = {"from_address": "0x30406cd5f18dd87367b782b9d63b4d79f7f5ebb8"} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 2, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["tx_data"] == [ - self.filter_response_item(self.A_tx_2), - self.filter_response_item(self.A_tx_1), - ] - - # Normal_2_3 - # Query parameter: to_address - def test_normal_2_3(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - params = {"to_address": "0xeceb9fdbd2cf677be5fa8b1ceeb53e53d582f0eb"} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 1, - "offset": None, - "limit": None, - "total": 3, - } - assert response_data["tx_data"] == [self.filter_response_item(self.B_tx_1)] - - # Normal_3_1 - # Pagination: offset - def test_normal_3_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - params = {"offset": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": 1, - "limit": None, - "total": 3, - } - assert response_data["tx_data"] == [ - self.filter_response_item(self.A_tx_2), - self.filter_response_item(self.A_tx_1), - ] - - # Normal_3_2 - # Pagination: limit - def test_normal_3_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - params = {"limit": 1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 200 - assert resp.json()["meta"] == {"code": 200, "message": "OK"} - - response_data = resp.json()["data"] - assert response_data["result_set"] == { - "count": 3, - "offset": None, - "limit": 1, - "total": 3, - } - assert response_data["tx_data"] == [self.filter_response_item(self.B_tx_1)] - - ########################################################################### - # Error - ########################################################################### - - # Error_1 - # BC_EXPLORER_ENABLED = False (default) - def test_error_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = False - - # Request target API - resp = client.get(self.apiurl) - - # Assertion - assert resp.status_code == 404 - assert resp.json()["meta"] == { - "code": 10, - "message": "Not Supported", - "description": "method: GET, url: /NodeInfo/TxData", - } - - # Error_2_1 - # Invalid Parameter - def test_error_2_1(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - params = {"offset": -1, "limit": -1, "block_number": -1} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 400 - assert resp.json()["meta"] == { - "code": 88, - "message": "Invalid Parameter", - "description": [ - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "offset"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "limit"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - { - "ctx": {"ge": 0}, - "input": "-1", - "loc": ["query", "block_number"], - "msg": "Input should be greater than or equal to 0", - "type": "greater_than_equal", - }, - ], - } - - # Error_2_2 - # Invalid Parameter - def test_error_2_2(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - # Request target API - params = {"from_address": "abcd", "to_address": "abcd"} - resp = client.get(self.apiurl, params=params) - - # Assertion - assert resp.status_code == 400 - assert resp.json()["meta"] == { - "code": 88, - "message": "Invalid Parameter", - "description": [ - { - "type": "value_error", - "loc": ["query", "from_address"], - "msg": "Value error, Invalid ethereum address", - "input": "abcd", - "ctx": {"error": {}}, - }, - { - "type": "value_error", - "loc": ["query", "to_address"], - "msg": "Value error, Invalid ethereum address", - "input": "abcd", - "ctx": {"error": {}}, - }, - ], - } - - # Error_3 - # ResponseLimitExceededError - def test_error_3(self, client: TestClient, session: Session): - config.BC_EXPLORER_ENABLED = True - - self.insert_tx_data(session, self.A_tx_1) - self.insert_tx_data(session, self.A_tx_2) - self.insert_tx_data(session, self.B_tx_1) - - # Request target API - with mock.patch("app.api.routers.bc_explorer.TX_RESPONSE_LIMIT", 2): - resp = client.get(self.apiurl) - - # Assertion - assert resp.status_code == 400 - assert resp.json()["meta"] == { - "code": 30, - "message": "Response Limit Exceeded", - "description": "Search results exceed the limit", - } diff --git a/tests/batch/indexer_Block_Tx_Data_test.py b/tests/batch/indexer_Block_Tx_Data_test.py deleted file mode 100644 index 277f4f8646..0000000000 --- a/tests/batch/indexer_Block_Tx_Data_test.py +++ /dev/null @@ -1,356 +0,0 @@ -""" -Copyright BOOSTRY Co., Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - -See the License for the specific language governing permissions and -limitations under the License. - -SPDX-License-Identifier: Apache-2.0 -""" - -import logging -from typing import Sequence -from unittest import mock -from unittest.mock import MagicMock - -import pytest -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session -from web3 import Web3 -from web3.middleware import ExtraDataToPOAMiddleware -from web3.types import RPCEndpoint - -from app import config -from app.errors import ServiceUnavailable -from app.model.db import IDXBlockData, IDXBlockDataBlockNumber, IDXTxData -from batch.indexer_Block_Tx_Data import LOG, Processor -from tests.account_config import eth_account -from tests.utils import IbetStandardTokenUtils - -web3 = Web3(Web3.HTTPProvider(config.WEB3_HTTP_PROVIDER)) -web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) - - -def _transactions_len(block_data: IDXBlockData) -> int: - transactions = block_data.transactions - assert transactions is not None - return len(transactions) - - -@pytest.fixture(scope="function") -def caplog(caplog: pytest.LogCaptureFixture): - LOG = logging.getLogger("ibet_wallet_batch") - default_log_level = LOG.level - LOG.setLevel(logging.DEBUG) - LOG.propagate = True - yield caplog - LOG.propagate = False - LOG.setLevel(default_log_level) - - -@pytest.fixture(scope="function") -def processor() -> Processor: - processor = Processor() - return processor - - -@pytest.mark.asyncio -class TestProcessor: - @staticmethod - def set_block_number(session: Session, block_number: int): - indexed_block_number = IDXBlockDataBlockNumber() - indexed_block_number.chain_id = config.WEB3_CHAINID - indexed_block_number.latest_block_number = block_number - session.add(indexed_block_number) - session.commit() - - ########################################################################### - # Normal - ########################################################################### - - # Normal_1 - # Skip process: from_block > latest_block - async def test_normal_1( - self, processor: Processor, session: Session, caplog: pytest.LogCaptureFixture - ): - before_block_number = web3.eth.block_number - self.set_block_number(session, before_block_number) - - # Execute batch processing - await processor.process() - - # Assertion - indexed_block = session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ).first() - assert indexed_block is not None - assert indexed_block.latest_block_number == before_block_number - - block_data: Sequence[IDXBlockData] = session.scalars( - select(IDXBlockData).order_by(IDXBlockData.number) - ).all() - assert len(block_data) == 0 - - tx_data: Sequence[IDXTxData] = session.scalars( - select(IDXTxData).order_by(IDXTxData.block_number) - ).all() - assert len(tx_data) == 0 - - assert 1 == caplog.record_tuples.count( - (LOG.name, logging.INFO, "Skip process: from_block > latest_block") - ) - - # Normal_2 - # BlockData: Empty block is generated - async def test_normal_2( - self, processor: Processor, session: Session, caplog: pytest.LogCaptureFixture - ): - before_block_number = web3.eth.block_number - self.set_block_number(session, before_block_number) - - # Generate empty block - web3.provider.make_request(RPCEndpoint("evm_mine"), []) - web3.provider.make_request(RPCEndpoint("evm_mine"), []) - - # Execute batch processing - await processor.process() - after_block_number = web3.eth.block_number - - # Assertion: Data - indexed_block = session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ).first() - assert indexed_block is not None - assert indexed_block.latest_block_number == after_block_number - - block_data: Sequence[IDXBlockData] = session.scalars( - select(IDXBlockData).order_by(IDXBlockData.number) - ).all() - assert len(block_data) == 2 - assert block_data[0].number == before_block_number + 1 - assert block_data[1].number == before_block_number + 2 - - tx_data: Sequence[IDXTxData] = session.scalars( - select(IDXTxData).order_by(IDXTxData.block_number) - ).all() - assert len(tx_data) == 0 - - # Assertion: Log - assert 1 == caplog.record_tuples.count( - ( - LOG.name, - logging.INFO, - f"Syncing from={before_block_number + 1}, to={after_block_number}", - ) - ) - assert 1 == caplog.record_tuples.count( - (LOG.name, logging.INFO, "Sync job has been completed") - ) - - # Normal_3_1 - # TxData: Contract deployment - async def test_normal_3_1( - self, processor: Processor, session: Session, caplog: pytest.LogCaptureFixture - ): - deployer = eth_account["issuer"]["account_address"] - - before_block_number = web3.eth.block_number - self.set_block_number(session, before_block_number) - - # Deploy contract - IbetStandardTokenUtils.issue( - tx_from=deployer, - args={ - "name": "test_token", - "symbol": "TEST", - "totalSupply": 1000, - "tradableExchange": config.ZERO_ADDRESS, - "contactInformation": "test_contact_info", - "privacyPolicy": "test_privacy_policy", - }, - ) - - # Execute batch processing - await processor.process() - after_block_number = web3.eth.block_number - - # Assertion - indexed_block = session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ).first() - assert indexed_block is not None - assert indexed_block.latest_block_number == after_block_number - - block_data: Sequence[IDXBlockData] = session.scalars( - select(IDXBlockData).order_by(IDXBlockData.number) - ).all() - assert len(block_data) == 1 - assert block_data[0].number == before_block_number + 1 - assert _transactions_len(block_data[0]) == 1 - - tx_data: Sequence[IDXTxData] = session.scalars( - select(IDXTxData).order_by(IDXTxData.block_number) - ).all() - assert len(tx_data) == 1 - assert tx_data[0].block_hash == block_data[0].hash - assert tx_data[0].block_number == before_block_number + 1 - assert tx_data[0].transaction_index == 0 - assert tx_data[0].from_address == deployer - assert tx_data[0].to_address is None - - # Normal_3_2 - # TxData: Transaction - async def test_normal_3_2( - self, processor: Processor, session: Session, caplog: pytest.LogCaptureFixture - ): - deployer = eth_account["issuer"]["account_address"] - to_address = eth_account["user1"]["account_address"] - - before_block_number = web3.eth.block_number - self.set_block_number(session, before_block_number) - - # Deploy contract -> Transfer - token_contract = IbetStandardTokenUtils.issue( - tx_from=deployer, - args={ - "name": "test_token", - "symbol": "TEST", - "totalSupply": 1000, - "tradableExchange": config.ZERO_ADDRESS, - "contactInformation": "test_contact_info", - "privacyPolicy": "test_privacy_policy", - }, - ) - tx_hash = token_contract.functions.transfer(to_address, 1).transact( - {"from": deployer} - ) - - # Execute batch processing - await processor.process() - after_block_number = web3.eth.block_number - - # Assertion - indexed_block = session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ).first() - assert indexed_block is not None - assert indexed_block.latest_block_number == after_block_number - - block_data: Sequence[IDXBlockData] = session.scalars( - select(IDXBlockData).order_by(IDXBlockData.number) - ).all() - assert len(block_data) == 2 - - assert block_data[0].number == before_block_number + 1 - assert _transactions_len(block_data[0]) == 1 - - assert block_data[1].number == before_block_number + 2 - assert _transactions_len(block_data[1]) == 1 - - tx_data: Sequence[IDXTxData] = session.scalars( - select(IDXTxData).order_by(IDXTxData.block_number) - ).all() - assert len(tx_data) == 2 - - assert tx_data[0].block_hash == block_data[0].hash - assert tx_data[0].block_number == before_block_number + 1 - assert tx_data[0].transaction_index == 0 - assert tx_data[0].from_address == deployer - assert tx_data[0].to_address is None - - assert tx_data[1].hash == tx_hash.to_0x_hex() - assert tx_data[1].block_hash == block_data[1].hash - assert tx_data[1].block_number == before_block_number + 2 - assert tx_data[1].transaction_index == 0 - assert tx_data[1].from_address == deployer - assert tx_data[1].to_address == token_contract.address - - ########################################################################### - # Error - ########################################################################### - - # Error_1: ServiceUnavailable - async def test_error_1(self, processor: Processor, session: Session): - before_block_number = web3.eth.block_number - self.set_block_number(session, before_block_number) - - # Execute batch processing - with ( - mock.patch( - "web3.AsyncWeb3.AsyncHTTPProvider.make_request", - MagicMock(side_effect=ServiceUnavailable()), - ), - pytest.raises(ServiceUnavailable), - ): - await processor.process() - - # Assertion - indexed_block = session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ).first() - assert indexed_block is not None - assert indexed_block.latest_block_number == before_block_number - - block_data: Sequence[IDXBlockData] = session.scalars( - select(IDXBlockData).order_by(IDXBlockData.number) - ).all() - assert len(block_data) == 0 - - tx_data: Sequence[IDXTxData] = session.scalars( - select(IDXTxData).order_by(IDXTxData.block_number) - ).all() - assert len(tx_data) == 0 - - # Error_2: SQLAlchemyError - async def test_error_2(self, processor: Processor, session: Session): - before_block_number = web3.eth.block_number - self.set_block_number(session, before_block_number) - - # Generate empty block - web3.provider.make_request(RPCEndpoint("evm_mine"), []) - - # Execute batch processing - with ( - mock.patch.object(Session, "commit", side_effect=SQLAlchemyError()), - pytest.raises(SQLAlchemyError), - ): - await processor.process() - - # Assertion - indexed_block = session.scalars( - select(IDXBlockDataBlockNumber) - .where(IDXBlockDataBlockNumber.chain_id == config.WEB3_CHAINID) - .limit(1) - ).first() - assert indexed_block is not None - assert indexed_block.latest_block_number == before_block_number - - block_data: Sequence[IDXBlockData] = session.scalars( - select(IDXBlockData).order_by(IDXBlockData.number) - ).all() - assert len(block_data) == 0 - - tx_data: Sequence[IDXTxData] = session.scalars( - select(IDXTxData).order_by(IDXTxData.block_number) - ).all() - assert len(tx_data) == 0 diff --git a/uv.lock b/uv.lock index 75cde18d5a..7c2b2936ff 100644 --- a/uv.lock +++ b/uv.lock @@ -123,12 +123,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" }, ] -[[package]] -name = "async-cache" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/6e/908402652bf7d8ab02fa204399d9517d247fb3a0926045a8c8a28708cd40/async-cache-1.1.1.tar.gz", hash = "sha256:81aa9ccd19fb06784aaf30bd5f2043dc0a23fc3e998b93d0c2c17d1af9803393", size = 3848, upload-time = "2021-07-03T18:24:36.769Z" } - [[package]] name = "asyncpg" version = "0.30.0" @@ -1301,15 +1295,6 @@ dependencies = [ { name = "web3" }, ] -[package.optional-dependencies] -ibet-explorer = [ - { name = "aiohttp" }, - { name = "async-cache" }, - { name = "ibet-wallet-api-explorer" }, - { name = "textual" }, - { name = "typer" }, -] - [package.dev-dependencies] dev = [ { name = "boto3-stubs", extra = ["all"] }, @@ -1327,16 +1312,14 @@ dev = [ { name = "ruamel-yaml" }, { name = "ruff" }, { name = "textual-dev" }, + { name = "typer" }, { name = "types-botocore" }, ] [package.metadata] requires-dist = [ - { name = "aiohttp", marker = "extra == 'ibet-explorer'", specifier = "~=3.13.3" }, { name = "aiomysql", specifier = "==0.3.2" }, { name = "alembic", specifier = ">=1.14.0,<2.0.0" }, - { name = "async-cache", marker = "extra == 'ibet-explorer'", specifier = "==1.1.1" }, - { name = "async-cache", marker = "extra == 'ibet-explorer'", specifier = "~=1.1.1" }, { name = "asyncpg", specifier = "~=0.30.0" }, { name = "boto3", specifier = "~=1.42.18" }, { name = "coincurve", specifier = "~=21.0.0" }, @@ -1347,7 +1330,6 @@ requires-dist = [ { name = "gunicorn", specifier = "~=23.0.0" }, { name = "hexbytes", specifier = "~=1.3.0" }, { name = "httpx", specifier = "~=0.28.0" }, - { name = "ibet-wallet-api-explorer", marker = "extra == 'ibet-explorer'", editable = "cmd/explorer" }, { name = "memray", specifier = ">=1.14.0,<2.0.0" }, { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.33.1,<2.0.0" }, { name = "opentelemetry-instrumentation-asyncio", specifier = ">=0.54b0,<1.0.0" }, @@ -1368,13 +1350,10 @@ requires-dist = [ { name = "python-dotenv", specifier = "~=1.2.1" }, { name = "requests", specifier = ">=2.32.3" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.33,<3.0.0" }, - { name = "textual", marker = "extra == 'ibet-explorer'", specifier = "~=0.44.1" }, - { name = "typer", marker = "extra == 'ibet-explorer'", specifier = "~=0.12.3" }, { name = "tzdata", specifier = ">=2025.1,<2026.0" }, { name = "uvicorn", extras = ["standard"], specifier = "~=0.34.0" }, { name = "web3", specifier = "==7.5.0" }, ] -provides-extras = ["ibet-explorer"] [package.metadata.requires-dev] dev = [ @@ -1393,14 +1372,10 @@ dev = [ { name = "ruamel-yaml", specifier = ">=0.18.6,<1.0.0" }, { name = "ruff", specifier = ">=0.5.4,<1.0.0" }, { name = "textual-dev", specifier = ">=1.2.1,<2.0.0" }, + { name = "typer", specifier = "~=0.12.3" }, { name = "types-botocore", specifier = ">=1.0.2" }, ] -[[package]] -name = "ibet-wallet-api-explorer" -version = "0.1.0" -source = { editable = "cmd/explorer" } - [[package]] name = "identify" version = "2.6.12" From 80603ee025571b5648606f5ed08540fa91268952 Mon Sep 17 00:00:00 2001 From: Yoshihito Aso Date: Fri, 30 Jan 2026 17:38:02 +0900 Subject: [PATCH 2/2] feat: Add MySQL compatibility for dropping indexes and tables in upgrade function --- .../1cd2ee459858_v26_3_0_feature_1748.py | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py b/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py index f770bbe01e..698f8aac5a 100644 --- a/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py +++ b/migrations/versions/1cd2ee459858_v26_3_0_feature_1748.py @@ -10,7 +10,7 @@ import sqlalchemy as sa from sqlalchemy.dialects import postgresql -from app.database import get_db_schema +from app.database import get_db_schema, engine # revision identifiers, used by Alembic. revision = "1cd2ee459858" @@ -19,23 +19,53 @@ depends_on = None -def _schema_name() -> str: - return get_db_schema() or "public" +def _mysql_drop_index_if_exists(conn, table_name: str, index_name: str) -> None: + """MySQL互換: インデックスが存在する場合のみDROPする + + MySQLはバージョンにより `DROP INDEX IF EXISTS` が使えないため、 + INFORMATION_SCHEMAから存在確認してから `DROP INDEX idx ON tbl` を実行する。 + """ + + exists_sql = sa.text( + """ + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = :table_name + AND INDEX_NAME = :index_name + LIMIT 1 + """ + ) + exists = conn.execute( + exists_sql, {"table_name": table_name, "index_name": index_name} + ).scalar() + if exists: + op.execute(f"DROP INDEX {index_name} ON {table_name}") def upgrade(): - schema = _schema_name() + connection = op.get_bind() - op.execute(f'DROP INDEX IF EXISTS {schema}."ix_tx_data_block_number"') - op.execute(f'DROP INDEX IF EXISTS {schema}."ix_tx_data_from_address"') - op.execute(f'DROP INDEX IF EXISTS {schema}."ix_tx_data_to_address"') - op.execute(f"DROP TABLE IF EXISTS {schema}.tx_data") + if engine.name == "mysql": + _mysql_drop_index_if_exists(connection, "tx_data", "ix_tx_data_block_number") + _mysql_drop_index_if_exists(connection, "tx_data", "ix_tx_data_from_address") + _mysql_drop_index_if_exists(connection, "tx_data", "ix_tx_data_to_address") + op.execute("DROP TABLE IF EXISTS tx_data") - op.execute(f'DROP INDEX IF EXISTS {schema}."ix_block_data_hash"') - op.execute(f'DROP INDEX IF EXISTS {schema}."ix_block_data_timestamp"') - op.execute(f"DROP TABLE IF EXISTS {schema}.block_data") + _mysql_drop_index_if_exists(connection, "block_data", "ix_block_data_hash") + _mysql_drop_index_if_exists(connection, "block_data", "ix_block_data_timestamp") + op.execute("DROP TABLE IF EXISTS block_data") - op.execute(f"DROP TABLE IF EXISTS {schema}.idx_block_data_block_number") + op.execute("DROP TABLE IF EXISTS idx_block_data_block_number") + else: + op.execute(f'DROP INDEX IF EXISTS "ix_tx_data_block_number"') + op.execute(f'DROP INDEX IF EXISTS "ix_tx_data_from_address"') + op.execute(f'DROP INDEX IF EXISTS "ix_tx_data_to_address"') + op.execute(f"DROP TABLE IF EXISTS tx_data") + op.execute(f'DROP INDEX IF EXISTS "ix_block_data_hash"') + op.execute(f'DROP INDEX IF EXISTS "ix_block_data_timestamp"') + op.execute(f"DROP TABLE IF EXISTS block_data") + op.execute(f"DROP TABLE IF EXISTS idx_block_data_block_number") def downgrade():