uploader-bot/app/core/models/user/wallet_mixin.py

221 lines
11 KiB
Python

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()