From 5053d7df1c69154f57139f63e1542e38d57b2ead Mon Sep 17 00:00:00 2001 From: Yosuke Otosu Date: Thu, 31 Jul 2025 13:29:49 +0900 Subject: [PATCH 1/4] Add notification category to Notification model and implement attribute change tracking watcher --- app/api/routers/notification.py | 3 + app/model/db/__init__.py | 7 +- app/model/db/notification.py | 45 +- app/model/schema/notification.py | 8 +- ...processor_Notifications_Coupon_Exchange.py | 9 + ...essor_Notifications_Membership_Exchange.py | 9 + batch/processor_Notifications_Token.py | 292 ++++++++++++- .../a5e395bf46a9_v25_9_0_feature_1662.py | 64 +++ .../notification_NotificationsCount_test.py | 5 + ...otification_NotificationsId_DELETE_test.py | 5 + .../notification_NotificationsId_POST_test.py | 5 + .../notification_NotificationsRead_test.py | 5 + .../notification_Notifications_GET_test.py | 23 +- ...ssor_Notifications_Coupon_Exchange_test.py | 27 ++ ..._Notifications_Membership_Exchange_test.py | 27 ++ .../processor_Notifications_Token_test.py | 393 +++++++++++++++++- 16 files changed, 902 insertions(+), 25 deletions(-) create mode 100644 migrations/versions/a5e395bf46a9_v25_9_0_feature_1662.py diff --git a/app/api/routers/notification.py b/app/api/routers/notification.py index 345add283..72af79302 100644 --- a/app/api/routers/notification.py +++ b/app/api/routers/notification.py @@ -61,6 +61,7 @@ async def list_all_notifications( """ Returns notifications filtered by given query. """ + notification_category = request_query.notification_category address = request_query.address notification_type = request_query.notification_type priority = request_query.priority @@ -75,6 +76,8 @@ async def list_all_notifications( ) # Search Filter + if notification_category is not None: + stmt = stmt.where(Notification.notification_category == notification_category) if address is not None: stmt = stmt.where(Notification.address == to_checksum_address(address)) if notification_type is not None: diff --git a/app/model/db/__init__.py b/app/model/db/__init__.py index fdc11c4ea..5a2421441 100644 --- a/app/model/db/__init__.py +++ b/app/model/db/__init__.py @@ -52,7 +52,12 @@ from .listing import Listing from .messaging import ChatWebhook, Mail from .node import Node -from .notification import Notification, NotificationBlockNumber, NotificationType +from .notification import ( + Notification, + NotificationAttributeValue, + NotificationBlockNumber, + NotificationType, +) from .public_info import PublicAccountList, TokenList from .tokenholders import TokenHolder, TokenHolderBatchStatus, TokenHoldersList from .user_info import AccountTag diff --git a/app/model/db/notification.py b/app/model/db/notification.py index 98f0ae81f..b80b5011b 100644 --- a/app/model/db/notification.py +++ b/app/model/db/notification.py @@ -20,6 +20,7 @@ import sys from datetime import datetime from enum import StrEnum +from typing import Literal from sqlalchemy import ( JSON, @@ -63,13 +64,29 @@ class Notification(Base): autoincrement=True, ) + # 通知分類 + notification_category: Mapped[Literal["event_log", "attribute_change"]] = ( + mapped_column( + String(20), + primary_key=True, + ) + ) + # 通知ID - # Spec: 0x | | | | + # Spec: 0x | | | | # ( | は文字列連結 ) - # : blockNumberをhexstringで表現したもの。12桁 - # : transactionIndex(block内でのトランザクションの採番)をhexstringで表現したもの。6桁 - # : logIndex(transaction内でのログの採番)をhexstringで表現したもの。6桁 - # : blockNumber, transactionIndex, logIndexが等しいが、通知としては複数にしたい場合に使用する識別子。2桁(デフォルトは00) + # + # notification_category = "event_log" の場合 + # = : blockNumberをhexstringで表現したもの。12桁 + # = : transactionIndex(block内でのトランザクションの採番)をhexstringで表現したもの。6桁 + # = : logIndex(transaction内でのログの採番)をhexstringで表現したもの。6桁 + # : blockNumber, transactionIndex, logIndexが等しいが、通知としては複数にしたい場合に使用する識別子。2桁(デフォルトは00) + # + # notification_category = "attribute_change" の場合 + # = : ミリ秒タイムスタンプをhexstringで表現したもの。12桁 + # = : コントラクトアドレス。40桁 + # = : 属性名のkeccak256ハッシュ先頭8文字。8桁 + # : 同一のコントラクト・属性で複数通知にする場合の識別子。2桁(デフォルトは00) notification_id: Mapped[str] = mapped_column(String(256), primary_key=True) # 通知タイプ(例:BuySettlementOK, BuyAgreementなど) @@ -122,6 +139,7 @@ def __repr__(self): def json(self): return { + "notification_category": self.notification_category, "notification_type": self.notification_type, "id": self.notification_id, "priority": self.priority, @@ -185,6 +203,7 @@ def json(self): class NotificationType(StrEnum): + # Event Log Notification NEW_ORDER = "NewOrder" NEW_ORDER_COUNTERPART = "NewOrderCounterpart" CANCEL_ORDER = "CancelOrder" @@ -206,6 +225,9 @@ class NotificationType(StrEnum): CHANGE_TO_REDEEMED = "ChangeToRedeemed" CHANGE_TO_CANCELED = "ChangeToCanceled" + # Attribute Change Notification + TRANSFERABLE_CHANGED = "TransferableChanged" + class NotificationBlockNumber(Base): """Synchronized blockNumber of Notification""" @@ -226,3 +248,16 @@ class NotificationBlockNumber(Base): } FIELDS.update(Base.FIELDS) + + +class NotificationAttributeValue(Base): + """Synchronized attribute value for Notification""" + + __tablename__ = "notification_attribute_value" + + # contract address + contract_address = mapped_column(String(42), primary_key=True) + # attribute key + attribute_key = mapped_column(String(256), primary_key=True) + # attribute + attribute = mapped_column(JSON, nullable=False) diff --git a/app/model/schema/notification.py b/app/model/schema/notification.py index 333a78efe..64957ebcd 100644 --- a/app/model/schema/notification.py +++ b/app/model/schema/notification.py @@ -18,7 +18,7 @@ """ from enum import StrEnum -from typing import Optional +from typing import Literal, Optional from pydantic import BaseModel, Field @@ -47,6 +47,9 @@ class NotificationsSortItem(StrEnum): class NotificationsQuery(BasePaginationQuery): + notification_category: Optional[Literal["event_log", "attribute_change"]] = Field( + None + ) address: Optional[EthereumAddress] = Field(None, description="account address") notification_type: Optional[NotificationType] = Field(None) priority: Optional[int] = Field(None, ge=0, le=2) @@ -85,8 +88,9 @@ class NotificationMetainfo(BaseModel): class Notification(BaseModel): - notification_type: NotificationType = Field(examples=[NotificationType.NEW_ORDER]) + notification_category: Literal["event_log", "attribute_change"] id: str = Field(examples=["0x00000373ca8600000000000000"]) + notification_type: NotificationType = Field(examples=[NotificationType.NEW_ORDER]) priority: int block_timestamp: str = Field(description="block timestamp") is_read: bool diff --git a/batch/processor_Notifications_Coupon_Exchange.py b/batch/processor_Notifications_Coupon_Exchange.py index a9e214aa8..0c9d21e20 100644 --- a/batch/processor_Notifications_Coupon_Exchange.py +++ b/batch/processor_Notifications_Coupon_Exchange.py @@ -236,6 +236,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -276,6 +277,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -319,6 +321,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 2 @@ -359,6 +362,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 1) notification.notification_type = self.notification_type notification.priority = 1 @@ -399,6 +403,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 2) notification.notification_type = self.notification_type notification.priority = 2 @@ -439,6 +444,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 1) notification.notification_type = self.notification_type notification.priority = 1 @@ -482,6 +488,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 2) notification.notification_type = self.notification_type notification.priority = 1 @@ -522,6 +529,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 1) notification.notification_type = self.notification_type notification.priority = 2 @@ -565,6 +573,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 2) notification.notification_type = self.notification_type notification.priority = 2 diff --git a/batch/processor_Notifications_Membership_Exchange.py b/batch/processor_Notifications_Membership_Exchange.py index 28de790f7..7c0997ddb 100644 --- a/batch/processor_Notifications_Membership_Exchange.py +++ b/batch/processor_Notifications_Membership_Exchange.py @@ -236,6 +236,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -279,6 +280,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -322,6 +324,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 2 @@ -362,6 +365,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 1) notification.notification_type = self.notification_type notification.priority = 1 @@ -402,6 +406,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 2) notification.notification_type = self.notification_type notification.priority = 2 @@ -445,6 +450,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 1) notification.notification_type = self.notification_type notification.priority = 1 @@ -488,6 +494,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 2) notification.notification_type = self.notification_type notification.priority = 1 @@ -531,6 +538,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 1) notification.notification_type = self.notification_type notification.priority = 2 @@ -574,6 +582,7 @@ async def watch(self, db_session: AsyncSession, entries): } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry, 2) notification.notification_type = self.notification_type notification.priority = 2 diff --git a/batch/processor_Notifications_Token.py b/batch/processor_Notifications_Token.py index 1d3f74ad1..ae2166e10 100644 --- a/batch/processor_Notifications_Token.py +++ b/batch/processor_Notifications_Token.py @@ -26,8 +26,11 @@ from sqlalchemy import and_, select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession +from web3 import Web3 from web3.contract import AsyncContract as Web3AsyncContract -from web3.exceptions import ABIEventNotFound +from web3.exceptions import ( + ABIEventNotFound, +) from web3.types import EventData from app.config import ( @@ -38,10 +41,12 @@ from app.contracts import AsyncContract from app.database import BatchAsyncSessionLocal from app.errors import ServiceUnavailable +from app.model.blockchain import BondToken, CouponToken, MembershipToken, ShareToken from app.model.db import ( IDXTokenListRegister, Listing, Notification, + NotificationAttributeValue, NotificationBlockNumber, NotificationType, ) @@ -67,8 +72,8 @@ token_list = TokenList(list_contract) -# Watcher -class Watcher: +# EventWatcher +class EventWatcher: contract_cache: dict[str, Web3AsyncContract] = {} def __init__( @@ -276,7 +281,7 @@ async def __set_synchronized_block_number( await db_session.merge(notification_block_number) -class WatchTransfer(Watcher): +class WatchTransfer(EventWatcher): """Watch Token Receive Event - Process for registering a notification when a token is received @@ -314,6 +319,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -324,7 +330,7 @@ async def db_merge( await db_session.merge(notification) -class WatchApplyForTransfer(Watcher): +class WatchApplyForTransfer(EventWatcher): """Watch Token ApplyForTransfer Event - Process for registering a notification when application for transfer is submitted @@ -359,6 +365,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -369,7 +376,7 @@ async def db_merge( await db_session.merge(notification) -class WatchApproveTransfer(Watcher): +class WatchApproveTransfer(EventWatcher): """Watch Token ApproveTransfer Event - Process for registering a notification when application for transfer is approved @@ -404,6 +411,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -414,7 +422,7 @@ async def db_merge( await db_session.merge(notification) -class WatchCancelTransfer(Watcher): +class WatchCancelTransfer(EventWatcher): """Watch Token CancelTransfer Event - Process for registering a notification when application for transfer is canceled @@ -449,6 +457,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -459,7 +468,7 @@ async def db_merge( await db_session.merge(notification) -class WatchForceLock(Watcher): +class WatchForceLock(EventWatcher): """Watch ForceLock Event - Process for registering a notification when a token is forcibly locked. @@ -490,6 +499,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -500,7 +510,7 @@ async def db_merge( await db_session.merge(notification) -class WatchForceUnlock(Watcher): +class WatchForceUnlock(EventWatcher): """Watch ForceUnlock Event - Process for registering a notification when a token is forcibly unlocked. @@ -531,6 +541,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -541,7 +552,7 @@ async def db_merge( await db_session.merge(notification) -class WatchChangeToRedeemed(Watcher): +class WatchChangeToRedeemed(EventWatcher): """Watch ChangeToRedeemed Event for Bond Token - Process for registering a notification when a token status is changed to redeemed. @@ -578,6 +589,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -588,7 +600,7 @@ async def db_merge( await db_session.merge(notification) -class WatchChangeToCanceled(Watcher): +class WatchChangeToCanceled(EventWatcher): """Watch ChangeToCanceled Event for Share Token - Process for registering a notification when a token status is changed to canceled. @@ -625,6 +637,7 @@ async def db_merge( "token_type": token_type, } notification = Notification() + notification.notification_category = "event_log" notification.notification_id = self._gen_notification_id(entry) notification.notification_type = self.notification_type notification.priority = 0 @@ -635,6 +648,262 @@ async def db_merge( await db_session.merge(notification) +# AttributeWatcher +class AttributeWatcher: + def __init__( + self, + attribute_key: str, + notification_type: str, + token_type_list: list[TokenType] = None, + ): + if token_type_list is None: + token_type_list = list([]) + self.notification_type = notification_type + self.token_type_list = token_type_list + self.attribute_key = attribute_key + self.attribute_key_hash = Web3.keccak(text=attribute_key).hex()[0:8] + + @staticmethod + def _gen_notification_id( + timestamp_ms: int, contract_address: str, attribute_key_hash: str, option_type=0 + ): + contract_address_without_prefix = contract_address.replace("0x", "").lower() + return f"0x{timestamp_ms:012x}{contract_address_without_prefix}{attribute_key_hash}{option_type:02x}" + + @staticmethod + async def _get_token_all_list( + db_session: AsyncSession, token_type_list: list[TokenType] + ): + _tokens = [] + + stmt = select(IDXTokenListRegister).join( + Listing, + and_(Listing.token_address == IDXTokenListRegister.token_address), + ) + if len(token_type_list) != 0: + stmt = stmt.where(IDXTokenListRegister.token_template.in_(token_type_list)) + registered_tokens: Sequence[IDXTokenListRegister] = ( + await db_session.scalars(stmt) + ).all() + for registered_token in registered_tokens: + _tokens.append( + { + "token": registered_token, + "token_type": registered_token.token_template, + } + ) + return _tokens + + async def db_merge( + self, + db_session: AsyncSession, + token_address: str, + token_type: str, + token_name: str, + token_owner_address: str, + previous_value: str | int | bool, + current_value: str | int | bool, + ): + pass + + async def loop(self): + start_time = time.time() + db_session = BatchAsyncSessionLocal() + + try: + # Get listed tokens + _token_list = await self._get_token_all_list( + db_session, self.token_type_list + ) + + for _token in _token_list: + try: + # Get previous attribute value from DB + previous_attribute_key_value = await self.__get_attribute_value( + db_session, + _token["token"].token_address, + self.attribute_key, + ) + is_initial_sync = ( + True if previous_attribute_key_value is None else False + ) + + # Get token detail by token type + if _token["token_type"] == TokenType.IbetStraightBond: + token_detail = await BondToken.get( + async_session=db_session, + token_address=_token["token"].token_address, + ) + elif _token["token_type"] == TokenType.IbetShare: + token_detail = await ShareToken.get( + async_session=db_session, + token_address=_token["token"].token_address, + ) + elif _token["token_type"] == TokenType.IbetCoupon: + token_detail = await CouponToken.get( + async_session=db_session, + token_address=_token["token"].token_address, + ) + elif _token["token_type"] == TokenType.IbetMembership: + token_detail = await MembershipToken.get( + async_session=db_session, + token_address=_token["token"].token_address, + ) + else: # pragma: no cover + continue + + # Get current attribute value from token detail + current_attribute_value = token_detail.__dict__.get( + self.attribute_key + ) + + if is_initial_sync is False: + # Get previous attribute value from DB record + previous_attribute_value = ( + previous_attribute_key_value.attribute.get( + self.attribute_key, None + ) + ) + # Register notification only if attribute value has changed + if ( + current_attribute_value is not None + and current_attribute_value != previous_attribute_value + ): + token_name = token_detail.name + # Register attribute change notification + await self.db_merge( + db_session=db_session, + token_address=_token["token"].token_address, + token_type=_token["token_type"], + token_owner_address=_token["token"].owner_address, + token_name=token_name, + previous_value=previous_attribute_value, + current_value=current_attribute_value, + ) + + # Save latest attribute value to DB + await self.__set_attribute_value( + db_session, + _token["token"].token_address, + self.attribute_key, + current_attribute_value, + ) + await db_session.commit() + + except Exception: # Continue processing even if an exception occurs + LOG.exception("Failed to watch attribute") + continue + + except SQLAlchemyError as sa_err: + LOG.error(f"A database error has occurred: code={sa_err.code}\n{sa_err}") + finally: + await db_session.close() + elapsed_time = time.time() - start_time + LOG.info( + "<{}> finished in {} secs".format(self.__class__.__name__, elapsed_time) + ) + + @staticmethod + async def __get_attribute_value( + db_session: AsyncSession, contract_address: str, attribute_key: str + ): + """Get latest synchronized attribute value""" + notification_attribute_value: NotificationAttributeValue | None = ( + await db_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == contract_address, + NotificationAttributeValue.attribute_key == attribute_key, + ) + ) + .limit(1) + ) + ).first() + return notification_attribute_value + + @staticmethod + async def __set_attribute_value( + db_session: AsyncSession, + contract_address: str, + attribute_key: str, + attribute_value: bool | int | str, + ): + """Set latest synchronized attribute value""" + notification_attribute_value: NotificationAttributeValue | None = ( + await db_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == contract_address, + NotificationAttributeValue.attribute_key == attribute_key, + ) + ) + .limit(1) + ) + ).first() + if notification_attribute_value is None: + notification_attribute_value = NotificationAttributeValue() + notification_attribute_value.contract_address = contract_address + notification_attribute_value.attribute_key = attribute_key + notification_attribute_value.attribute = {attribute_key: attribute_value} + await db_session.merge(notification_attribute_value) + + +class WatchTransferableAttribute(AttributeWatcher): + """Watch Transferable Attribute + + - Process for registering a notification when a token attribute "transferable" is changed. + """ + + def __init__(self): + super().__init__( + attribute_key="transferable", + notification_type=NotificationType.TRANSFERABLE_CHANGED, + token_type_list=[TokenType.IbetShare, TokenType.IbetStraightBond], + ) + + async def db_merge( + self, + db_session: AsyncSession, + token_address: str, + token_type: str, + token_name: str, + token_owner_address: str, + previous_value: str | int | bool, + current_value: str | int | bool, + ): + company_list = await CompanyList.get() + company = company_list.find(token_owner_address) + metadata = { + "company_name": company.corporate_name, + "token_address": token_address, + "token_name": token_name, + "exchange_address": "", + "token_type": token_type, + } + timestamp_ms = int(time.time() * 1000) + notification = Notification() + notification.notification_category = "attribute_change" + notification.notification_id = self._gen_notification_id( + timestamp_ms=timestamp_ms, + contract_address=token_address, + attribute_key_hash=self.attribute_key_hash, + ) + notification.notification_type = self.notification_type + notification.priority = 0 + notification.address = None + notification.block_timestamp = datetime.fromtimestamp(timestamp_ms / 1000) + notification.args = dict( + { + "previous": previous_value, + "current": current_value, + } + ) + notification.metainfo = metadata + await db_session.merge(notification) + + # メイン処理 async def main(): watchers = [ @@ -646,6 +915,7 @@ async def main(): WatchForceUnlock(), WatchChangeToRedeemed(), WatchChangeToCanceled(), + WatchTransferableAttribute(), ] LOG.info("Service started successfully") diff --git a/migrations/versions/a5e395bf46a9_v25_9_0_feature_1662.py b/migrations/versions/a5e395bf46a9_v25_9_0_feature_1662.py new file mode 100644 index 000000000..07a39c8a8 --- /dev/null +++ b/migrations/versions/a5e395bf46a9_v25_9_0_feature_1662.py @@ -0,0 +1,64 @@ +"""v25_9_0_feature_1662 + +Revision ID: a5e395bf46a9 +Revises: 819325835c3d +Create Date: 2025-07-31 12:02:12.029337 + +""" + +from alembic import op +import sqlalchemy as sa + + +from app.database import get_db_schema +from app.model.db import Notification + +# revision identifiers, used by Alembic. +revision = "a5e395bf46a9" +down_revision = "819325835c3d" +branch_labels = None +depends_on = None + + +def upgrade(): + connection = op.get_bind() + + op.create_table( + "notification_attribute_value", + sa.Column("contract_address", sa.String(length=42), nullable=False), + sa.Column("attribute_key", sa.String(length=256), nullable=False), + sa.Column("attribute", sa.JSON(), nullable=False), + sa.Column("created", sa.DateTime(), nullable=True), + sa.Column("modified", sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint("contract_address", "attribute_key"), + schema=get_db_schema(), + ) + op.add_column( + "notification", + sa.Column("notification_category", sa.String(length=20), nullable=True), + schema=get_db_schema(), + ) + op.get_bind().execute( + sa.update(Notification).values(notification_category="event_log") + ) + op.alter_column( + "notification", + "notification_category", + existing_type=sa.String(length=20), + nullable=False, + schema=get_db_schema(), + ) + op.drop_constraint("notification_pkey", "notification", type_="primary") + op.create_primary_key( + "notification_pkey", + "notification", + ["notification_category", "notification_id"], + schema=get_db_schema(), + ) + + +def downgrade(): + connection = op.get_bind() + + op.drop_column("notification", "notification_category", schema=get_db_schema()) + op.drop_table("notification_attribute_value", schema=get_db_schema()) diff --git a/tests/app/notification_NotificationsCount_test.py b/tests/app/notification_NotificationsCount_test.py index af4e73edd..f16079cbf 100644 --- a/tests/app/notification_NotificationsCount_test.py +++ b/tests/app/notification_NotificationsCount_test.py @@ -33,6 +33,7 @@ class TestNotificationCount: def _insert_test_data(self, session): n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034300000000000000" n.notification_type = "SampleNotification1" n.priority = 1 @@ -51,6 +52,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034000000000000000" n.notification_type = "SampleNotification2" n.priority = 1 @@ -67,6 +69,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011034000000000000000" n.notification_type = "SampleNotification3" n.priority = 2 @@ -83,6 +86,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011032000000000000000" n.notification_type = "SampleNotification4" n.priority = 1 @@ -99,6 +103,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000001034000000000000000" n.notification_type = "SampleNotification5" n.priority = 0 diff --git a/tests/app/notification_NotificationsId_DELETE_test.py b/tests/app/notification_NotificationsId_DELETE_test.py index 2ec072332..eb72a085a 100644 --- a/tests/app/notification_NotificationsId_DELETE_test.py +++ b/tests/app/notification_NotificationsId_DELETE_test.py @@ -36,6 +36,7 @@ class TestNotificationsIdDELETE: def _insert_test_data(self, session): n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034300000000000000" n.notification_type = "NewOrder" n.priority = 1 @@ -55,6 +56,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034000000000000000" n.notification_type = "NewOrderCounterpart" n.priority = 1 @@ -72,6 +74,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011034000000000000000" n.notification_type = "NewOrder" n.priority = 2 @@ -89,6 +92,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011032000000000000000" n.notification_type = "NewOrderCounterpart" n.priority = 1 @@ -106,6 +110,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000001034000000000000000" n.notification_type = "NewOrder" n.priority = 0 diff --git a/tests/app/notification_NotificationsId_POST_test.py b/tests/app/notification_NotificationsId_POST_test.py index 3c3108dae..2b14ca278 100644 --- a/tests/app/notification_NotificationsId_POST_test.py +++ b/tests/app/notification_NotificationsId_POST_test.py @@ -38,6 +38,7 @@ def _insert_test_data(self, session): self.session = session # HACK: updateでcommitされてしまう対策 n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034300000000000000" n.notification_type = NotificationType.NEW_ORDER n.priority = 1 @@ -56,6 +57,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034000000000000000" n.notification_type = NotificationType.APPROVE_TRANSFER n.priority = 1 @@ -72,6 +74,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011034000000000000000" n.notification_type = NotificationType.APPLY_FOR_TRANSFER n.priority = 2 @@ -88,6 +91,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011032000000000000000" n.notification_type = NotificationType.BUY_AGREEMENT n.priority = 1 @@ -104,6 +108,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = NotificationType.BUY_SETTLEMENT_NG n.notification_type = "SampleNotification5" n.priority = 0 diff --git a/tests/app/notification_NotificationsRead_test.py b/tests/app/notification_NotificationsRead_test.py index f5fbb4acc..3ea39c0fa 100644 --- a/tests/app/notification_NotificationsRead_test.py +++ b/tests/app/notification_NotificationsRead_test.py @@ -44,6 +44,7 @@ def _insert_test_data(self, session): self.session = session # HACK: updateでcommitされてしまう対策 n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034300000000000000" n.notification_type = "SampleNotification1" n.priority = 1 @@ -64,6 +65,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034000000000000000" n.notification_type = "SampleNotification2" n.priority = 1 @@ -80,6 +82,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011034000000000000000" n.notification_type = "SampleNotification3" n.priority = 2 @@ -96,6 +99,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011032000000000000000" n.notification_type = "SampleNotification4" n.priority = 1 @@ -112,6 +116,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000001034000000000000000" n.notification_type = "SampleNotification5" n.priority = 0 diff --git a/tests/app/notification_Notifications_GET_test.py b/tests/app/notification_Notifications_GET_test.py index c27d966ba..a40504cf8 100644 --- a/tests/app/notification_Notifications_GET_test.py +++ b/tests/app/notification_Notifications_GET_test.py @@ -35,6 +35,7 @@ class TestNotificationsGet: def _insert_test_data(self, session): n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034300000000000000" n.notification_type = "NewOrder" n.priority = 1 @@ -54,6 +55,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000021034000000000000000" n.notification_type = "NewOrderCounterpart" n.priority = 1 @@ -71,6 +73,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011034000000000000000" n.notification_type = "NewOrder" n.priority = 2 @@ -88,6 +91,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000011032000000000000000" n.notification_type = "NewOrderCounterpart" n.priority = 1 @@ -105,6 +109,7 @@ def _insert_test_data(self, session): session.add(n) n = Notification() + n.notification_category = "event_log" n.notification_id = "0x00000001034000000000000000" n.notification_type = "NewOrder" n.priority = 0 @@ -140,6 +145,7 @@ def test_normal_1(self, client: TestClient, session: Session): "result_set": {"count": 5, "offset": None, "limit": None, "total": 5}, "notifications": [ { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000021034300000000000000", "sort_id": 1, @@ -157,6 +163,7 @@ def test_normal_1(self, client: TestClient, session: Session): "created": "2022/01/01 15:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrderCounterpart", "id": "0x00000021034000000000000000", "sort_id": 2, @@ -174,6 +181,7 @@ def test_normal_1(self, client: TestClient, session: Session): "created": "2022/01/01 16:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000011034000000000000000", "sort_id": 3, @@ -191,6 +199,7 @@ def test_normal_1(self, client: TestClient, session: Session): "created": "2022/01/01 17:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrderCounterpart", "id": "0x00000011032000000000000000", "sort_id": 4, @@ -208,6 +217,7 @@ def test_normal_1(self, client: TestClient, session: Session): "created": "2022/01/01 18:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000001034000000000000000", "sort_id": 5, @@ -252,6 +262,7 @@ def test_normal_2(self, client: TestClient, session: Session): "result_set": {"count": 5, "offset": 1, "limit": 2, "total": 5}, "notifications": [ { + "notification_category": "event_log", "notification_type": "NewOrderCounterpart", "id": "0x00000021034000000000000000", "sort_id": 2, @@ -269,6 +280,7 @@ def test_normal_2(self, client: TestClient, session: Session): "created": "2022/01/01 16:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000011034000000000000000", "sort_id": 3, @@ -332,6 +344,7 @@ def test_normal_4(self, client: TestClient, session: Session): "address": self.address, "notification_type": "NewOrder", "priority": 2, + "notification_category": "event_log", }, ) @@ -339,6 +352,7 @@ def test_normal_4(self, client: TestClient, session: Session): "result_set": {"count": 1, "offset": None, "limit": None, "total": 5}, "notifications": [ { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000011034000000000000000", "sort_id": 1, @@ -410,6 +424,7 @@ def test_normal_6(self, client: TestClient, session: Session): "result_set": {"count": 5, "offset": None, "limit": None, "total": 5}, "notifications": [ { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000011034000000000000000", "sort_id": 1, @@ -427,6 +442,7 @@ def test_normal_6(self, client: TestClient, session: Session): "created": "2022/01/01 17:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000021034300000000000000", "sort_id": 2, @@ -444,6 +460,7 @@ def test_normal_6(self, client: TestClient, session: Session): "created": "2022/01/01 15:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrderCounterpart", "id": "0x00000021034000000000000000", "sort_id": 3, @@ -461,6 +478,7 @@ def test_normal_6(self, client: TestClient, session: Session): "created": "2022/01/01 16:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrderCounterpart", "id": "0x00000011032000000000000000", "sort_id": 4, @@ -478,6 +496,7 @@ def test_normal_6(self, client: TestClient, session: Session): "created": "2022/01/01 18:20:30", }, { + "notification_category": "event_log", "notification_type": "NewOrder", "id": "0x00000001034000000000000000", "sort_id": 5, @@ -544,10 +563,10 @@ def test_error_1(self, client: TestClient, session: Session): { "type": "enum", "loc": ["query", "notification_type"], - "msg": "Input should be 'NewOrder', 'NewOrderCounterpart', 'CancelOrder', 'CancelOrderCounterpart', 'ForceCancelOrder', 'BuyAgreement', 'BuySettlementOK', 'BuySettlementNG', 'SellAgreement', 'SellSettlementOK', 'SellSettlementNG', 'Transfer', 'ApplyForTransfer', 'ApproveTransfer', 'CancelTransfer', 'ForceLock', 'ForceUnlock', 'ChangeToRedeemed' or 'ChangeToCanceled'", + "msg": "Input should be 'NewOrder', 'NewOrderCounterpart', 'CancelOrder', 'CancelOrderCounterpart', 'ForceCancelOrder', 'BuyAgreement', 'BuySettlementOK', 'BuySettlementNG', 'SellAgreement', 'SellSettlementOK', 'SellSettlementNG', 'Transfer', 'ApplyForTransfer', 'ApproveTransfer', 'CancelTransfer', 'ForceLock', 'ForceUnlock', 'ChangeToRedeemed', 'ChangeToCanceled' or 'TransferableChanged'", "input": "hoge", "ctx": { - "expected": "'NewOrder', 'NewOrderCounterpart', 'CancelOrder', 'CancelOrderCounterpart', 'ForceCancelOrder', 'BuyAgreement', 'BuySettlementOK', 'BuySettlementNG', 'SellAgreement', 'SellSettlementOK', 'SellSettlementNG', 'Transfer', 'ApplyForTransfer', 'ApproveTransfer', 'CancelTransfer', 'ForceLock', 'ForceUnlock', 'ChangeToRedeemed' or 'ChangeToCanceled'" + "expected": "'NewOrder', 'NewOrderCounterpart', 'CancelOrder', 'CancelOrderCounterpart', 'ForceCancelOrder', 'BuyAgreement', 'BuySettlementOK', 'BuySettlementNG', 'SellAgreement', 'SellSettlementOK', 'SellSettlementNG', 'Transfer', 'ApplyForTransfer', 'ApproveTransfer', 'CancelTransfer', 'ForceLock', 'ForceUnlock', 'ChangeToRedeemed', 'ChangeToCanceled' or 'TransferableChanged'" }, }, { diff --git a/tests/batch/processor_Notifications_Coupon_Exchange_test.py b/tests/batch/processor_Notifications_Coupon_Exchange_test.py index 3614d3de2..3a0d0fdd5 100644 --- a/tests/batch/processor_Notifications_Coupon_Exchange_test.py +++ b/tests/batch/processor_Notifications_Coupon_Exchange_test.py @@ -133,6 +133,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 0 ) @@ -212,6 +213,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 0 ) @@ -237,6 +239,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 0 ) @@ -383,6 +386,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -461,6 +465,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 0 ) @@ -486,6 +491,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -657,6 +663,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -763,6 +770,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 0 ) @@ -790,6 +798,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -961,6 +970,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -1041,6 +1051,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 1 ) @@ -1067,6 +1078,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -1226,6 +1238,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) @@ -1304,6 +1317,7 @@ async def test_normal_2( ).all() assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 2 ) @@ -1329,6 +1343,7 @@ async def test_normal_2( "token_type": "IbetCoupon", } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) @@ -1493,6 +1508,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 1 ) @@ -1581,6 +1597,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 1 ) @@ -1609,6 +1626,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 1 ) @@ -1780,6 +1798,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 2 ) @@ -1868,6 +1887,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 2 ) @@ -1896,6 +1916,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 2 ) @@ -2067,6 +2088,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -2155,6 +2177,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 1 ) @@ -2183,6 +2206,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -2354,6 +2378,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) @@ -2442,6 +2467,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 2 ) @@ -2470,6 +2496,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) diff --git a/tests/batch/processor_Notifications_Membership_Exchange_test.py b/tests/batch/processor_Notifications_Membership_Exchange_test.py index 4b6e87a40..4e934ac55 100644 --- a/tests/batch/processor_Notifications_Membership_Exchange_test.py +++ b/tests/batch/processor_Notifications_Membership_Exchange_test.py @@ -133,6 +133,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 0 ) @@ -204,6 +205,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 0 ) @@ -229,6 +231,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 0 ) @@ -374,6 +377,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -451,6 +455,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 0 ) @@ -476,6 +481,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -646,6 +652,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -752,6 +759,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 0 ) @@ -779,6 +787,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 0 ) @@ -948,6 +957,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -1027,6 +1037,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 1 ) @@ -1053,6 +1064,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -1210,6 +1222,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) @@ -1289,6 +1302,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 2 ) @@ -1315,6 +1329,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) @@ -1475,6 +1490,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 1 ) @@ -1560,6 +1576,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 1 ) @@ -1588,6 +1605,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 1 ) @@ -1753,6 +1771,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 2 ) @@ -1838,6 +1857,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 1, 2 ) @@ -1866,6 +1886,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 1, 2 ) @@ -2031,6 +2052,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -2116,6 +2138,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 1 ) @@ -2144,6 +2167,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 1 ) @@ -2309,6 +2333,7 @@ async def test_normal_1( select(Notification).order_by(Notification.created).limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) @@ -2394,6 +2419,7 @@ async def test_normal_2( assert len(_notification_list) == 2 _notification = _notification_list[0] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number - 1, 0, 0, 2 ) @@ -2422,6 +2448,7 @@ async def test_normal_2( } _notification = _notification_list[1] + assert _notification.notification_category == "event_log" assert _notification.notification_id == "0x{:012x}{:06x}{:06x}{:02x}".format( block_number, 0, 0, 2 ) diff --git a/tests/batch/processor_Notifications_Token_test.py b/tests/batch/processor_Notifications_Token_test.py index 6293133dc..6f60e9523 100644 --- a/tests/batch/processor_Notifications_Token_test.py +++ b/tests/batch/processor_Notifications_Token_test.py @@ -19,6 +19,7 @@ from __future__ import annotations +from datetime import datetime, timezone from importlib import reload from typing import TYPE_CHECKING, Callable from unittest import mock @@ -33,9 +34,11 @@ from app import config from app.model.db import ( + IDXShareToken, IDXTokenListRegister, Listing, Notification, + NotificationAttributeValue, NotificationBlockNumber, NotificationType, ) @@ -61,10 +64,11 @@ share_set_transfer_approval_required, transfer_coupon_token, transfer_share_token, + untransferable_share_token, ) if TYPE_CHECKING: - from batch.processor_Notifications_Token import Watcher + from batch.processor_Notifications_Token import EventWatcher web3 = Web3(Web3.HTTPProvider(config.WEB3_HTTP_PROVIDER)) web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) @@ -73,7 +77,7 @@ @pytest.fixture(scope="function") def watcher_factory( async_session: AsyncSession, shared_contract: SharedContract -) -> Callable[[str], Watcher]: +) -> Callable[[str], EventWatcher]: def _watcher(cls_name): config.TOKEN_LIST_CONTRACT_ADDRESS = shared_contract["TokenList"]["address"] @@ -274,6 +278,7 @@ async def test_normal_1( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -355,6 +360,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -382,6 +388,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader2["account_address"] @@ -516,6 +523,7 @@ async def test_normal_4( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.TRANSFER assert _notification.priority == 0 assert _notification.address == exchange_contract["address"] @@ -744,6 +752,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.APPLY_FOR_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader2["account_address"] @@ -773,6 +782,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.APPLY_FOR_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader2["account_address"] @@ -980,6 +990,7 @@ async def test_normal_1( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.APPROVE_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1073,6 +1084,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.APPROVE_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1101,6 +1113,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.APPROVE_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1308,6 +1321,7 @@ async def test_normal_1( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1401,6 +1415,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1429,6 +1444,7 @@ async def test_normal_2( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1546,6 +1562,7 @@ async def test_normal_3( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1574,6 +1591,7 @@ async def test_normal_3( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1602,6 +1620,7 @@ async def test_normal_3( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1630,6 +1649,7 @@ async def test_normal_3( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.CANCEL_TRANSFER assert _notification.priority == 0 assert _notification.address == self.trader["account_address"] @@ -1836,6 +1856,7 @@ async def test_normal_1( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.FORCE_LOCK assert _notification.priority == 0 assert _notification.address == self.issuer["account_address"] @@ -2100,6 +2121,7 @@ async def test_normal_1( .limit(1) ) ).first() + assert _notification.notification_category == "event_log" assert _notification.notification_type == NotificationType.FORCE_UNLOCK assert _notification.priority == 0 assert _notification.address == self.issuer["account_address"] @@ -2308,7 +2330,6 @@ async def test_error_1( @pytest.mark.asyncio class TestWatchChangeToRedeemed: issuer = eth_account["issuer"] - lock_account = eth_account["user1"] ########################################################################### # Normal Case @@ -2423,6 +2444,7 @@ async def test_normal_2( _notification_list = (await async_session.scalars(select(Notification))).all() assert len(_notification_list) == 1 + assert _notification_list[0].notification_category == "event_log" assert ( _notification_list[0].notification_type == NotificationType.CHANGE_TO_REDEEMED @@ -2507,6 +2529,7 @@ async def test_normal_3( _notification_list = (await async_session.scalars(select(Notification))).all() assert len(_notification_list) == 2 + assert _notification_list[0].notification_category == "event_log" assert ( _notification_list[0].notification_type == NotificationType.CHANGE_TO_REDEEMED @@ -2521,6 +2544,7 @@ async def test_normal_3( "token_name": "テスト債券", "token_type": "IbetStraightBond", } + assert _notification_list[1].notification_category == "event_log" assert ( _notification_list[1].notification_type == NotificationType.CHANGE_TO_REDEEMED @@ -2703,7 +2727,6 @@ async def test_error_1( @pytest.mark.asyncio class TestWatchChangeToCanceled: issuer = eth_account["issuer"] - lock_account = eth_account["user1"] ########################################################################### # Normal Case @@ -2818,6 +2841,7 @@ async def test_normal_2( _notification_list = (await async_session.scalars(select(Notification))).all() assert len(_notification_list) == 1 + assert _notification_list[0].notification_category == "event_log" assert ( _notification_list[0].notification_type == NotificationType.CHANGE_TO_CANCELED @@ -2902,6 +2926,7 @@ async def test_normal_3( _notification_list = (await async_session.scalars(select(Notification))).all() assert len(_notification_list) == 2 + assert _notification_list[0].notification_category == "event_log" assert ( _notification_list[0].notification_type == NotificationType.CHANGE_TO_CANCELED @@ -2916,6 +2941,7 @@ async def test_normal_3( "token_name": "テスト株式", "token_type": "IbetShare", } + assert _notification_list[1].notification_category == "event_log" assert ( _notification_list[1].notification_type == NotificationType.CHANGE_TO_CANCELED @@ -3093,3 +3119,362 @@ async def test_error_1( await async_session.scalars(select(NotificationBlockNumber).limit(1)) ).first() assert _notification_block_number is None + + +@pytest.mark.asyncio +class TestWatchWatchTransferableAttribute: + issuer = eth_account["issuer"] + + ########################################################################### + # Normal Case + ########################################################################### + + # + # Initial Sync + async def test_normal_1( + self, watcher_factory, async_session, shared_contract, mocked_company_list + ): + watcher = watcher_factory("WatchTransferableAttribute") + exchange_contract = shared_contract["IbetShareExchange"] + token_list_contract = shared_contract["TokenList"] + personal_info_contract = shared_contract["PersonalInfo"] + + # Issue token + token = await prepare_share_token( + self.issuer, + exchange_contract, + token_list_contract, + personal_info_contract, + async_session, + ) + + idx_token_list_item = IDXTokenListRegister() + idx_token_list_item.token_address = token["address"] + idx_token_list_item.owner_address = self.issuer["account_address"] + idx_token_list_item.token_template = "IbetShare" + async_session.add(idx_token_list_item) + await async_session.commit() + + # Run target process + await watcher.loop() + + # Assertion + _notification = ( + await async_session.scalars(select(Notification).limit(1)) + ).first() + assert _notification is None + + _notification_attribute_value: NotificationAttributeValue = ( + await async_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == token["address"], + NotificationAttributeValue.attribute_key == "transferable", + ) + ) + .limit(1) + ) + ).first() + assert _notification_attribute_value.attribute == {"transferable": True} + + # + # Attribute not changed + @pytest.mark.freeze_time(datetime(2025, 7, 31, 1, 35, 0, tzinfo=timezone.utc)) + async def test_normal_2( + self, watcher_factory, async_session, shared_contract, mocked_company_list + ): + watcher = watcher_factory("WatchTransferableAttribute") + exchange_contract = shared_contract["IbetShareExchange"] + token_list_contract = shared_contract["TokenList"] + personal_info_contract = shared_contract["PersonalInfo"] + + # Issue token + token = await prepare_share_token( + self.issuer, + exchange_contract, + token_list_contract, + personal_info_contract, + async_session, + ) + + idx_token = IDXShareToken() + idx_token.token_address = token["address"] + idx_token.token_template = "IbetShare" + idx_token.name = "test_token" + idx_token.transferable = True + idx_token.short_term_cache_created = datetime( + 2025, 7, 31, 1, 35, 0, tzinfo=timezone.utc + ) + async_session.add(idx_token) + + idx_token_list_item = IDXTokenListRegister() + idx_token_list_item.token_address = token["address"] + idx_token_list_item.owner_address = self.issuer["account_address"] + idx_token_list_item.token_template = "IbetShare" + async_session.add(idx_token_list_item) + + notification_attribute_value = NotificationAttributeValue() + notification_attribute_value.contract_address = token["address"] + notification_attribute_value.attribute_key = "transferable" + notification_attribute_value.attribute = {"transferable": True} + async_session.add(notification_attribute_value) + await async_session.commit() + + # Run target process + await watcher.loop() + + # Assertion + async_session.expunge_all() + _notification = ( + await async_session.scalars(select(Notification).limit(1)) + ).first() + assert _notification is None + + _notification_attribute_value: NotificationAttributeValue = ( + await async_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == token["address"], + NotificationAttributeValue.attribute_key == "transferable", + ) + ) + .limit(1) + ) + ).first() + assert _notification_attribute_value.attribute == {"transferable": True} + + # + # Attribute changed + @pytest.mark.freeze_time(datetime(2025, 7, 31, 1, 35, 0, tzinfo=timezone.utc)) + async def test_normal_3( + self, watcher_factory, async_session, shared_contract, mocked_company_list + ): + watcher = watcher_factory("WatchTransferableAttribute") + exchange_contract = shared_contract["IbetShareExchange"] + token_list_contract = shared_contract["TokenList"] + personal_info_contract = shared_contract["PersonalInfo"] + + # Issue token + token = await prepare_share_token( + self.issuer, + exchange_contract, + token_list_contract, + personal_info_contract, + async_session, + ) + + idx_token = IDXShareToken() + idx_token.token_address = token["address"] + idx_token.token_template = "IbetShare" + idx_token.name = "test_token" + idx_token.transferable = False + idx_token.short_term_cache_created = datetime( + 2025, 7, 31, 1, 35, 0, tzinfo=timezone.utc + ) + async_session.add(idx_token) + + idx_token_list_item = IDXTokenListRegister() + idx_token_list_item.token_address = token["address"] + idx_token_list_item.owner_address = self.issuer["account_address"] + idx_token_list_item.token_template = "IbetShare" + async_session.add(idx_token_list_item) + + notification_attribute_value = NotificationAttributeValue() + notification_attribute_value.contract_address = token["address"] + notification_attribute_value.attribute_key = "transferable" + notification_attribute_value.attribute = {"transferable": True} + async_session.add(notification_attribute_value) + await async_session.commit() + + # Run target process + await watcher.loop() + + # Assertion + async_session.expunge_all() + _notification = ( + await async_session.scalars(select(Notification).limit(1)) + ).first() + attribute_key_hash = Web3.keccak(text="transferable").hex()[0:8] + assert _notification.notification_category == "attribute_change" + assert ( + _notification.notification_id[14:] + == f"{token['address'].replace('0x', '').lower()}{attribute_key_hash}00" + ) + assert _notification.notification_type == NotificationType.TRANSFERABLE_CHANGED + assert _notification.priority == 0 + assert _notification.address is None + assert _notification.block_timestamp is not None + assert _notification.args == { + "previous": True, + "current": False, + } + assert _notification.metainfo == { + "company_name": "株式会社DEMO", + "token_address": token["address"], + "token_name": "test_token", + "exchange_address": "", + "token_type": "IbetShare", + } + + _notification_attribute_value: NotificationAttributeValue = ( + await async_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == token["address"], + NotificationAttributeValue.attribute_key == "transferable", + ) + ) + .limit(1) + ) + ).first() + assert _notification_attribute_value.attribute == {"transferable": False} + + # + # Attribute changed (On chain access) + @pytest.mark.freeze_time(datetime(2025, 7, 31, 1, 35, 0, tzinfo=timezone.utc)) + async def test_normal_4( + self, watcher_factory, async_session, shared_contract, mocked_company_list + ): + watcher = watcher_factory("WatchTransferableAttribute") + exchange_contract = shared_contract["IbetShareExchange"] + token_list_contract = shared_contract["TokenList"] + personal_info_contract = shared_contract["PersonalInfo"] + + # Issue token + token = await prepare_share_token( + self.issuer, + exchange_contract, + token_list_contract, + personal_info_contract, + async_session, + ) + + untransferable_share_token(self.issuer, token) + + idx_token = IDXShareToken() + idx_token.token_address = token["address"] + idx_token.token_template = "IbetShare" + idx_token.name = "test_token" + idx_token.transferable = True # Cache expired + idx_token.short_term_cache_created = datetime( + 2025, 7, 30, 1, 35, 0, tzinfo=timezone.utc + ) # Cache expired + async_session.add(idx_token) + + idx_token_list_item = IDXTokenListRegister() + idx_token_list_item.token_address = token["address"] + idx_token_list_item.owner_address = self.issuer["account_address"] + idx_token_list_item.token_template = "IbetShare" + async_session.add(idx_token_list_item) + + notification_attribute_value = NotificationAttributeValue() + notification_attribute_value.contract_address = token["address"] + notification_attribute_value.attribute_key = "transferable" + notification_attribute_value.attribute = {"transferable": True} + async_session.add(notification_attribute_value) + await async_session.commit() + + # Run target process + await watcher.loop() + + # Assertion + async_session.expunge_all() + _notification = ( + await async_session.scalars(select(Notification).limit(1)) + ).first() + attribute_key_hash = Web3.keccak(text="transferable").hex()[0:8] + assert _notification.notification_category == "attribute_change" + assert ( + _notification.notification_id[14:] + == f"{token['address'].replace('0x', '').lower()}{attribute_key_hash}00" + ) + assert _notification.notification_type == NotificationType.TRANSFERABLE_CHANGED + assert _notification.priority == 0 + assert _notification.address is None + assert _notification.block_timestamp is not None + assert _notification.args == { + "previous": True, + "current": False, + } + assert _notification.metainfo == { + "company_name": "株式会社DEMO", + "token_address": token["address"], + "token_name": "test_token", + "exchange_address": "", + "token_type": "IbetShare", + } + + _notification_attribute_value: NotificationAttributeValue = ( + await async_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == token["address"], + NotificationAttributeValue.attribute_key == "transferable", + ) + ) + .limit(1) + ) + ).first() + assert _notification_attribute_value.attribute == {"transferable": False} + + # ########################################################################### + # # Error Case + # ########################################################################### + + # + # Error occur + @mock.patch( + "web3.eth.async_eth.AsyncEth.get_logs", + MagicMock(side_effect=Exception()), + ) + async def test_error_1( + self, watcher_factory, async_session, shared_contract, mocked_company_list + ): + watcher = watcher_factory("WatchChangeToCanceled") + exchange_contract = shared_contract["IbetShareExchange"] + token_list_contract = shared_contract["TokenList"] + personal_info_contract = shared_contract["PersonalInfo"] + + # Issue token + token = await prepare_share_token( + self.issuer, + exchange_contract, + token_list_contract, + personal_info_contract, + async_session, + ) + + idx_token_list_item = IDXTokenListRegister() + idx_token_list_item.token_address = token["address"] + idx_token_list_item.owner_address = self.issuer["account_address"] + idx_token_list_item.token_template = "IbetShare" + async_session.add(idx_token_list_item) + await async_session.commit() + + # Run target process + await watcher.loop() + + # Assertion + async_session.expunge_all() + _notification = ( + await async_session.scalars(select(Notification).limit(1)) + ).first() + assert _notification is None + + _notification_attribute_value: NotificationAttributeValue = ( + await async_session.scalars( + select(NotificationAttributeValue) + .where( + and_( + NotificationAttributeValue.contract_address == token["address"], + NotificationAttributeValue.attribute_key == "transferable", + ) + ) + .limit(1) + ) + ).first() + assert _notification_attribute_value is None From 77a7f7ca5f708b551c6737fe66effb9ce1ce01b4 Mon Sep 17 00:00:00 2001 From: Yosuke Otosu Date: Thu, 31 Jul 2025 17:22:48 +0900 Subject: [PATCH 2/4] Add notification category to API schema for enhanced notification filtering --- docs/ibet_wallet_api.yaml | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/ibet_wallet_api.yaml b/docs/ibet_wallet_api.yaml index ab04a8c69..6002ef2c3 100644 --- a/docs/ibet_wallet_api.yaml +++ b/docs/ibet_wallet_api.yaml @@ -5045,6 +5045,17 @@ paths: description: Limit for pagination title: Limit description: Limit for pagination + - name: notification_category + in: query + required: false + schema: + anyOf: + - enum: + - event_log + - attribute_change + type: string + - type: 'null' + title: Notification Category - name: address in: query required: false @@ -9295,15 +9306,21 @@ components: title: NotSupportedErrorResponse Notification: properties: - notification_type: - $ref: '#/components/schemas/NotificationType' - examples: - - NewOrder + notification_category: + type: string + enum: + - event_log + - attribute_change + title: Notification Category id: type: string title: Id examples: - '0x00000373ca8600000000000000' + notification_type: + $ref: '#/components/schemas/NotificationType' + examples: + - NewOrder priority: type: integer title: Priority @@ -9345,8 +9362,9 @@ components: description: datetime of create type: object required: - - notification_type + - notification_category - id + - notification_type - priority - block_timestamp - is_read @@ -9418,6 +9436,7 @@ components: - ForceUnlock - ChangeToRedeemed - ChangeToCanceled + - TransferableChanged title: NotificationType NotificationUpdateResponse: properties: From c3ca77e4d8c0a155d22c15c7b864a069991e5483 Mon Sep 17 00:00:00 2001 From: Yoshihito Aso Date: Thu, 31 Jul 2025 17:57:31 +0900 Subject: [PATCH 3/4] Update tests/batch/processor_Notifications_Token_test.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/batch/processor_Notifications_Token_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/batch/processor_Notifications_Token_test.py b/tests/batch/processor_Notifications_Token_test.py index 6f60e9523..c47156b46 100644 --- a/tests/batch/processor_Notifications_Token_test.py +++ b/tests/batch/processor_Notifications_Token_test.py @@ -3434,7 +3434,7 @@ async def test_normal_4( async def test_error_1( self, watcher_factory, async_session, shared_contract, mocked_company_list ): - watcher = watcher_factory("WatchChangeToCanceled") + watcher = watcher_factory("WatchTransferableAttribute") exchange_contract = shared_contract["IbetShareExchange"] token_list_contract = shared_contract["TokenList"] personal_info_contract = shared_contract["PersonalInfo"] From c9d6d80184a3a3a5a32c986eef3db1f01dbb8c88 Mon Sep 17 00:00:00 2001 From: Yoshihito Aso Date: Thu, 31 Jul 2025 18:20:17 +0900 Subject: [PATCH 4/4] Update test to mock ShareToken.get for error handling in processor_Notifications_Token_test.py --- tests/batch/processor_Notifications_Token_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/batch/processor_Notifications_Token_test.py b/tests/batch/processor_Notifications_Token_test.py index c47156b46..0342a5cce 100644 --- a/tests/batch/processor_Notifications_Token_test.py +++ b/tests/batch/processor_Notifications_Token_test.py @@ -3428,7 +3428,7 @@ async def test_normal_4( # # Error occur @mock.patch( - "web3.eth.async_eth.AsyncEth.get_logs", + "app.model.blockchain.token.ShareToken.get", MagicMock(side_effect=Exception()), ) async def test_error_1(