Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions backend/core/src/core/services/nft.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,14 @@ def get_all(

def bulk_create_or_update(
self, nft_items: NftItems, whitelist_collection_addresses: list[str]
) -> list[NftItem]:
) -> tuple[list[NftItem], set[str]]:
"""
Returns the persisted NFTs and the set of addresses that lost
ownership in this batch so callers can re-queue eligibility checks
for them.
"""
created_or_updated_nfts = []
previous_owners: set[str] = set()
for nft_item in nft_items.nft_items:
if (
not nft_item.collection
Expand All @@ -161,9 +167,18 @@ def bulk_create_or_update(
):
continue

created_or_updated_nfts.append(self.create_or_update(nft_item))
try:
existing = self.get(address=nft_item.address.to_raw())
if existing.owner_address != nft_item.owner.address.to_raw():
previous_owners.add(existing.owner_address)
created_or_updated_nfts.append(self._update(nft_item, existing))
except NoResultFound:
logger.info(
f"No NFT Item for address {nft_item.address!r} found. Creating new NFT Item."
)
created_or_updated_nfts.append(self._create(nft_item))
self.db_session.flush()
return created_or_updated_nfts
return created_or_updated_nfts, previous_owners

def count(self) -> int:
return self.db_session.query(NftItem).count()
Expand Down
6 changes: 5 additions & 1 deletion backend/indexer_blockchain/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,18 @@ def fetch_wallet_details(address: str) -> None:
jetton_wallet_service.delete_missing(address, active_jetton_wallets)

nft_service = NftItemService(db_session)
nft_service.bulk_create_or_update(nft_items, whitelist_collection_addresses)
_, evicted_owners = nft_service.bulk_create_or_update(
nft_items, whitelist_collection_addresses
)

active_nft_items = [item.address.to_raw() for item in nft_items.nft_items]
nft_service.delete_missing(address, active_nft_items)

logger.info(f"NFT items for {address!r} updated.")
redis_service = RedisService()
redis_service.add_to_set(UPDATED_WALLETS_SET_NAME, address)
for evicted_owner in evicted_owners - {address}:
redis_service.add_to_set(UPDATED_WALLETS_SET_NAME, evicted_owner)


@app.task(
Expand Down
122 changes: 122 additions & 0 deletions backend/tests/unit/core/services/test_nft_item_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import pytest
from pytest_mock import MockerFixture
from sqlalchemy.orm import Session

from core.services.nft import NftItemService
from tests.factories.nft import NFTCollectionFactory, NftItemFactory
from tests.factories.wallet import UserWalletFactory


def _build_tonapi_nft_item(
mocker: MockerFixture,
*,
address: str,
owner_address: str,
collection_address: str,
):
nft_item = mocker.MagicMock()
nft_item.address.to_raw.return_value = address
nft_item.owner.address.to_raw.return_value = owner_address
nft_item.collection.address.to_raw.return_value = collection_address
return nft_item


def _build_tonapi_nft_items(items):
container = type("NftItemsContainer", (), {})()
container.nft_items = items
return container


@pytest.fixture(autouse=True)
def _patch_metadata_dto(mocker: MockerFixture):
mocker.patch(
"core.services.nft.NftItemMetadataDTO.from_nft_item",
return_value=None,
)


def test_bulk_create_or_update_returns_previous_owner_on_ownership_change(
db_session: Session, mocker: MockerFixture
) -> None:
previous_wallet = UserWalletFactory.with_session(db_session).create()
new_wallet = UserWalletFactory.with_session(db_session).create()
nft = NftItemFactory.with_session(db_session).create(
owner_address=previous_wallet.address
)
tonapi_nft = _build_tonapi_nft_item(
mocker,
address=nft.address,
owner_address=new_wallet.address,
collection_address=nft.collection_address,
)

persisted, evicted_owners = NftItemService(db_session).bulk_create_or_update(
_build_tonapi_nft_items([tonapi_nft]),
whitelist_collection_addresses=[nft.collection_address],
)

assert len(persisted) == 1
assert persisted[0].owner_address == new_wallet.address
assert evicted_owners == {previous_wallet.address}


def test_bulk_create_or_update_returns_empty_set_when_owner_unchanged(
db_session: Session, mocker: MockerFixture
) -> None:
wallet = UserWalletFactory.with_session(db_session).create()
nft = NftItemFactory.with_session(db_session).create(owner_address=wallet.address)
tonapi_nft = _build_tonapi_nft_item(
mocker,
address=nft.address,
owner_address=wallet.address,
collection_address=nft.collection_address,
)

_, evicted_owners = NftItemService(db_session).bulk_create_or_update(
_build_tonapi_nft_items([tonapi_nft]),
whitelist_collection_addresses=[nft.collection_address],
)

assert evicted_owners == set()


def test_bulk_create_or_update_returns_empty_set_for_brand_new_nft(
db_session: Session, mocker: MockerFixture
) -> None:
wallet = UserWalletFactory.with_session(db_session).create()
collection = NFTCollectionFactory.with_session(db_session).create()
tonapi_nft = _build_tonapi_nft_item(
mocker,
address="0:brand_new_nft_address_used_only_for_this_test_no_collisions_xx",
owner_address=wallet.address,
collection_address=collection.address,
)

persisted, evicted_owners = NftItemService(db_session).bulk_create_or_update(
_build_tonapi_nft_items([tonapi_nft]),
whitelist_collection_addresses=[collection.address],
)

assert len(persisted) == 1
assert evicted_owners == set()


def test_bulk_create_or_update_skips_non_whitelisted_collections(
db_session: Session, mocker: MockerFixture
) -> None:
wallet = UserWalletFactory.with_session(db_session).create()
collection = NFTCollectionFactory.with_session(db_session).create()
tonapi_nft = _build_tonapi_nft_item(
mocker,
address="0:nonwhitelisted_collection_nft_used_only_for_this_unit_test_xx",
owner_address=wallet.address,
collection_address=collection.address,
)

persisted, evicted_owners = NftItemService(db_session).bulk_create_or_update(
_build_tonapi_nft_items([tonapi_nft]),
whitelist_collection_addresses=["0:some_other_whitelisted_collection_address"],
)

assert persisted == []
assert evicted_owners == set()