import asyncio from app.core.models.content.user_content import UserContent from app.core.models.wallet_connection import WalletConnection from app.core._blockchain.ton.toncenter import toncenter from tonsdk.utils import Address from datetime import datetime, timedelta from app.core.logger import make_log from httpx import AsyncClient from app.core.models.content.indexation_mixins import unpack_item_indexator_data, MIN_ONCHAIN_INDEX def _platform_address_str() -> str: from app.core._blockchain.ton.platform import platform return platform.address.to_string(1, 1, 1) class WalletMixin: async def wallet_connection_async(self, db_session): from sqlalchemy import select, and_, desc result = await db_session.execute( select(WalletConnection) .where(and_(WalletConnection.user_id == self.id, WalletConnection.invalidated == False)) .order_by(WalletConnection.created.desc()) ) return result.scalars().first() async def wallet_address_async(self, db_session): wc = await self.wallet_connection_async(db_session) return wc.wallet_address if wc else None async def scan_owned_user_content(self, db_session): user_wallet_address = await self.wallet_address_async(db_session) async def get_nft_items_list(): try: # TODO: support more than 1000 items async with AsyncClient() as client: response = await client.get(f"https://tonapi.io/v2/accounts/{user_wallet_address}/nfts?limit=1000&offset=0&indirect_ownership=false") return response.json()['nft_items'] except BaseException as e: make_log(self, f"Error while fetching NFTs: {e}", level='error') await asyncio.sleep(6) return await get_nft_items_list() nfts_list = await get_nft_items_list() make_log(self, f"Found {len(nfts_list)} NFTs", level='info') for nft_item in nfts_list: item_address = Address(nft_item['address']).to_string(1, 1, 1) owner_address = Address(nft_item['owner']['address']).to_string(1, 1, 1) platform_address = _platform_address_str() collection_address = None if isinstance(nft_item, dict): collection_data = nft_item.get('collection') if isinstance(collection_data, dict): collection_address = collection_data.get('address') collection_address = collection_address or nft_item.get('collection_address') if collection_address: try: collection_address = Address(collection_address).to_string(1, 1, 1) except Exception: pass item_index = None license_type = None # Prefer index from tonapi payload if available raw_index = nft_item.get('index') if isinstance(nft_item, dict) else None if isinstance(raw_index, int): item_index = raw_index need_chain_probe = item_index is None or item_index < MIN_ONCHAIN_INDEX platform_address_onchain = None if need_chain_probe: try: indexator_raw = await toncenter.run_get_method(item_address, 'indexator_data') if indexator_raw.get('exit_code', -1) == 0: indexator_data = unpack_item_indexator_data(indexator_raw) item_index = indexator_data['index'] license_type = indexator_data.get('license_type') platform_address_onchain = indexator_data.get('platform_address') except BaseException as err: make_log(self, f"Failed to fetch indexator data for {item_address}: {err}", level='warning') if item_index is None: make_log(self, f"Skip NFT {item_address}: unable to resolve on-chain index", level='warning') continue if platform_address_onchain and platform_address_onchain != platform_address: make_log( self, f"Skip foreign NFT {item_address}: platform mismatch {platform_address_onchain} != {platform_address}", level='debug' ) continue if item_index < MIN_ONCHAIN_INDEX and (license_type is None or license_type == 0): make_log( self, f"Ignore NFT {item_address} with index {item_index} < MIN_ONCHAIN_INDEX={MIN_ONCHAIN_INDEX} (license_type={license_type})", level='debug' ) continue from sqlalchemy import select user_content = (await db_session.execute(select(UserContent).where(UserContent.onchain_address == item_address))).scalars().first() if user_content: if license_type is not None and license_type != 0 and user_content.type == 'nft/ignored': user_content.type = 'nft/unknown' user_content.meta = {**(user_content.meta or {}), 'license_type': license_type} user_content.owner_address = owner_address user_content.status = 'active' user_content.updated = datetime.fromtimestamp(0) await db_session.commit() continue user_content = UserContent( type='nft/unknown', onchain_address=item_address, owner_address=owner_address, code_hash=None, data_hash=None, updated=datetime.fromtimestamp(0), content_id=None, # not resolved yet created=datetime.now(), meta={'license_type': license_type} if license_type is not None else {}, user_id=self.id, wallet_connection_id=(await self.wallet_connection_async(db_session)).id, status="active" ) db_session.add(user_content) await db_session.commit() make_log(self, f"New onchain NFT found: {item_address}", level='info') async def ____scan_owned_user_content(self, db_session): page_id = -1 page_size = 100 have_next_page = True user_wallet_address = await self.wallet_address_async(db_session) while have_next_page: page_id += 1 nfts_list = await toncenter.get_nft_items(limit=100, offset=page_id * page_size, owner_address=user_wallet_address) if len(nfts_list) >= page_size: have_next_page = True for nft_item in nfts_list: try: # make_log(self, f"Scanning onchain NFT: {nft_item}", level='info') item_address = Address(nft_item['address']).to_string(1, 1, 1) owner_address = Address(nft_item['owner_address']).to_string(1, 1, 1) platform_address = _platform_address_str() collection_address = nft_item.get('collection_address') if isinstance(nft_item, dict) else None if collection_address: try: normalized_collection = Address(collection_address).to_string(1, 1, 1) except Exception: normalized_collection = collection_address if normalized_collection != platform_address: make_log(self, f"Skip foreign NFT {item_address} from collection {normalized_collection}", level='debug') continue item_index = None try: indexator_raw = await toncenter.run_get_method(item_address, 'indexator_data') if indexator_raw.get('exit_code', -1) == 0: item_index = unpack_item_indexator_data(indexator_raw)['index'] except BaseException as err: make_log(self, f"Failed to fetch indexator data for {item_address}: {err}", level='warning') if item_index is None: make_log(self, f"Skip NFT {item_address}: unable to resolve on-chain index", level='warning') continue if item_index is not None and item_index < MIN_ONCHAIN_INDEX: make_log(self, f"Ignore NFT {item_address} with index {item_index} < MIN_ONCHAIN_INDEX={MIN_ONCHAIN_INDEX}", level='debug') continue from sqlalchemy import select user_content = (await db_session.execute(select(UserContent).where(UserContent.onchain_address == item_address))).scalars().first() if user_content: continue try: nft_content = nft_item['content']['uri'] except KeyError: nft_content = None user_content = UserContent( type='nft/unknown', onchain_address=item_address, owner_address=owner_address, code_hash=nft_item['code_hash'], data_hash=nft_item['data_hash'], updated=datetime.fromtimestamp(0), content_id=None, # not resolved yet created=datetime.now(), meta={ 'metadata_uri': nft_content, }, user_id=self.id, wallet_connection_id=(await self.wallet_connection_async(db_session)).id, status="active" ) db_session.add(user_content) await db_session.commit() make_log(self, f"New onchain NFT found: {item_address}", level='info') except BaseException as e: make_log(self, f"Error while scanning onchain NFT: {e}", level='error') continue async def get_user_content(self, db_session, limit=100, offset=0): try: await self.scan_owned_user_content(db_session) except BaseException as e: make_log(self, f"Error while scanning user content: {e}", level='error') from sqlalchemy import select result = await db_session.execute(select(UserContent).where(UserContent.user_id == self.id).offset(offset).limit(limit)) return result.scalars().all()