From 61417722d375419bfb40a352c512ff3b54a59c89 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:09:03 +0800 Subject: [PATCH 01/15] feat: add claim rewards functionality to Group and GroupingModuleClient --- .../GroupingModule/GroupingModule_client.py | 8 ++ .../resources/Group.py | 88 ++++++++++++++++++- .../types/resource/Group.py | 22 +++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 src/story_protocol_python_sdk/types/resource/Group.py diff --git a/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py b/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py index ed8ffe16..1d647ed9 100644 --- a/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py +++ b/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py @@ -43,6 +43,14 @@ def build_addIp_transaction( groupIpId, ipIds, maxAllowedRewardShare ).build_transaction(tx_params) + def claimReward(self, groupId, token, ipIds): + return self.contract.functions.claimReward(groupId, token, ipIds).transact() + + def build_claimReward_transaction(self, groupId, token, ipIds, tx_params): + return self.contract.functions.claimReward( + groupId, token, ipIds + ).build_transaction(tx_params) + def registerGroup(self, groupPool): return self.contract.functions.registerGroup(groupPool).transact() diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 2e86bb66..467b98af 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -1,6 +1,4 @@ -# src/story_protocol_python_sdk/resources/Group.py - -from ens.ens import HexStr +from ens.ens import Address, HexStr from web3 import Web3 from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import ( @@ -28,6 +26,10 @@ PILicenseTemplateClient, ) from story_protocol_python_sdk.types.common import RevShareType +from story_protocol_python_sdk.types.resource.Group import ( + ClaimReward, + ClaimRewardsResponse, +) from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.sign import Sign @@ -531,6 +533,58 @@ def collect_and_distribute_group_royalties( f"Failed to collect and distribute group royalties: {str(e)}" ) + def claim_rewards( + self, + group_ip_id: Address, + currency_token: Address, + member_ip_ids: list[Address], + tx_options: dict | None = None, + ) -> ClaimRewardsResponse: + """ + Claim rewards for the entire group. + + :param group_ip_id str: The ID of the group IP. + :param currency_token str: The address of the currency (revenue) token to claim.. + :param member_ip_ids list: The IDs of the member IPs to distribute the rewards to. + :param tx_options dict: [Optional] The transaction options. + :return ClaimRewardsResponse: A response object with the transaction hash and claimed rewards. + """ + try: + if not self.web3.is_address(group_ip_id): + raise ValueError(f"Invalid group IP ID: {group_ip_id}") + if not self.web3.is_address(currency_token): + raise ValueError(f"Invalid currency token: {currency_token}") + for ip_id in member_ip_ids: + if not self.web3.is_address(ip_id): + print("ip_id", ip_id) + raise ValueError(f"Invalid member IP ID: {ip_id}") + + claim_reward_param = { + "groupIpId": group_ip_id, + "token": currency_token, + "memberIpIds": member_ip_ids, + } + + response = build_and_send_transaction( + self.web3, + self.account, + self.grouping_module_client.build_claimReward_transaction, + *claim_reward_param.values(), + tx_options=tx_options, + ) + + claimed_rewards = self._parse_tx_claimed_reward_event( + response["tx_receipt"] + ) + + return ClaimRewardsResponse( + tx_hash=response["tx_hash"], + claimed_rewards=claimed_rewards, + ) + + except Exception as e: + raise ValueError(f"Failed to claim rewards: {str(e)}") + def _get_license_data(self, license_data: list) -> list: """ Process license data into the format expected by the contracts. @@ -695,3 +749,31 @@ def _parse_tx_royalty_paid_event(self, tx_receipt: dict) -> list: ) return royalties_distributed + + def _parse_tx_claimed_reward_event(self, tx_receipt: dict) -> list[ClaimReward]: + """ + Parse the ClaimedReward event from a transaction receipt. + + :param tx_receipt dict: The transaction receipt. + :return list: List of claimed rewards. + """ + event_signature = self.web3.keccak( + text="ClaimedReward(address,address,address,uint256)" + ).hex() + claimed_rewards = [] + + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + ip_id = "0x" + log["topics"][0].hex()[24:] + amount = int(log["data"][:66].hex(), 16) + token = "0x" + log["topics"][2].hex()[24:] + + claimed_rewards.append( + ClaimReward( + ip_id=ip_id, + amount=amount, + token=token, + ) + ) + + return claimed_rewards diff --git a/src/story_protocol_python_sdk/types/resource/Group.py b/src/story_protocol_python_sdk/types/resource/Group.py new file mode 100644 index 00000000..62d0b2ec --- /dev/null +++ b/src/story_protocol_python_sdk/types/resource/Group.py @@ -0,0 +1,22 @@ +from typing import TypedDict + +from ens.ens import Address, HexStr + + +class ClaimReward(TypedDict): + """ + Structure for a claimed reward. + """ + + ip_id: Address + amount: int + token: Address + + +class ClaimRewardsResponse(TypedDict): + """ + Response structure for Group.claim_rewards method. + """ + + tx_hash: HexStr + claimed_rewards: list[ClaimReward] From 68666a20c1c40d3db3e6f568c21a92b76afc159e Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:09:18 +0800 Subject: [PATCH 02/15] feat: add integration test for claiming rewards in Group functionality --- tests/integration/test_integration_group.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index e5d645e5..3042ceff 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -4,6 +4,8 @@ import pytest +from story_protocol_python_sdk.story_client import StoryClient + from .setup_for_integration import ( EVEN_SPLIT_GROUP_POOL, PIL_LICENSE_TEMPLATE, @@ -538,3 +540,34 @@ def test_collect_and_distribute_group_royalties( assert len(response["royalties_distributed"]) == 2 assert response["royalties_distributed"][0]["amount"] == 10 assert response["royalties_distributed"][1]["amount"] == 10 + + def test_claim_rewards(self, story_client: StoryClient, setup_royalty_collection): + """Test claiming rewards for group members.""" + group_ip_id = setup_royalty_collection["group_ip_id"] + ip_ids = setup_royalty_collection["ip_ids"] + print("group_ip_id", group_ip_id) + print("ip_ids", ip_ids) + # First, collect and distribute royalties to set up rewards for claiming + story_client.Group.collect_and_distribute_group_royalties( + group_ip_id=group_ip_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids + ) + + # Test claiming rewards for specific members + response = story_client.Group.claim_rewards( + group_ip_id=group_ip_id, + currency_token=MockERC20, + member_ip_ids=ip_ids, + ) + print("response", response) + # Verify response structure + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + + # Verify claimed rewards details if any are present + if response["claimed_rewards"]: + for reward in response["claimed_rewards"]: + assert "amount" in reward + assert isinstance(reward["amount"], int) + assert "token" in reward + assert story_client.web3.is_address(reward["token"]) From 13346e31a20994c8fbd186c4bfab0a39920a4d90 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:09:46 +0800 Subject: [PATCH 03/15] feat: add unit tests for Group.claim_rewards method with various scenarios --- tests/unit/conftest.py | 8 ++ tests/unit/resources/test_group.py | 164 +++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 tests/unit/resources/test_group.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 88c3b587..484fbfc6 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -149,3 +149,11 @@ def __exit__(self, exc_type, exc_val, exc_tb): return MockContext() return _mock + + +@pytest.fixture(scope="module") +def mock_web3_is_address(mock_web3): + def _mock(is_address: bool = True): + return patch.object(mock_web3, "is_address", return_value=is_address) + + return _mock diff --git a/tests/unit/resources/test_group.py b/tests/unit/resources/test_group.py new file mode 100644 index 00000000..3bee6893 --- /dev/null +++ b/tests/unit/resources/test_group.py @@ -0,0 +1,164 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from story_protocol_python_sdk.resources.Group import Group +from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH + + +@pytest.fixture(scope="class") +def group(mock_web3, mock_account): + return Group(mock_web3, mock_account, CHAIN_ID) + + +@pytest.fixture(scope="class") +def mock_grouping_module_client(group): + def _mock(): + return patch.object( + group.grouping_module_client, + "build_claimReward_transaction", + return_value=MagicMock(), + ) + + return _mock + + +@pytest.fixture(scope="class") +def mock_build_and_send_transaction(): + def _mock(): + return patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": {"status": 1, "logs": []}, + }, + ) + + return _mock + + +@pytest.fixture(scope="class") +def mock_parse_tx_claimed_reward_event(group): + def _mock(): + return patch.object( + group, + "_parse_tx_claimed_reward_event", + return_value=[{"amount": 100, "token": ADDRESS}], + ) + + return _mock + + +class TestGroupClaimRewards: + """Test class for Group.claim_rewards method""" + + def test_claim_rewards_invalid_group_ip_id( + self, group: Group, mock_web3_is_address + ): + """Test claim_rewards with invalid group IP ID.""" + invalid_group_ip_id = "invalid_group_ip_id" + with mock_web3_is_address(False): + with pytest.raises( + ValueError, + match=f"Failed to claim rewards: Invalid group IP ID: {invalid_group_ip_id}", + ): + group.claim_rewards( + group_ip_id=invalid_group_ip_id, + currency_token=ADDRESS, + member_ip_ids=[IP_ID], + ) + + def test_claim_rewards_invalid_currency_token(self, group: Group, mock_web3): + """Test claim_rewards with invalid currency token.""" + invalid_currency_token = "invalid_currency_token" + with patch.object(mock_web3, "is_address") as mock_is_address: + # group_ip_id=True, currency_token=False + mock_is_address.side_effect = [True, False] + with pytest.raises( + ValueError, + match=f"Failed to claim rewards: Invalid currency token: {invalid_currency_token}", + ): + group.claim_rewards( + group_ip_id=IP_ID, + currency_token=invalid_currency_token, + member_ip_ids=[ADDRESS], + ) + + def test_claim_rewards_invalid_member_ip_ids(self, group: Group, mock_web3): + """Test claim_rewards with invalid member IP IDs.""" + invalid_member_ip_id = "invalid_member_ip" + with patch.object(mock_web3, "is_address") as mock_is_address: + # group_ip_id=True, currency_token=True, first member_ip_id=False + mock_is_address.side_effect = [True, True, False] + with pytest.raises( + ValueError, + match=f"Failed to claim rewards: Invalid member IP ID: {invalid_member_ip_id}", + ): + group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[invalid_member_ip_id], + ) + + def test_claim_rewards_mixed_valid_invalid_members(self, group: Group, mock_web3): + """Test claim_rewards with mix of valid and invalid member IP IDs.""" + invalid_member_ip_id = "invalid_member_ip" + with patch.object(mock_web3, "is_address") as mock_is_address: + # group_ip_id=True, currency_token=True, first_member=True, second_member=False + mock_is_address.side_effect = [True, True, True, False] + with pytest.raises( + ValueError, + match=f"Failed to claim rewards: Invalid member IP ID: {invalid_member_ip_id}", + ): + group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[ADDRESS, invalid_member_ip_id], + ) + + def test_claim_rewards_success( + self, + group: Group, + mock_grouping_module_client, + mock_build_and_send_transaction, + mock_parse_tx_claimed_reward_event, + mock_web3_is_address, + ): + """Test successful claim_rewards operation.""" + + with ( + mock_grouping_module_client(), + mock_build_and_send_transaction(), + mock_parse_tx_claimed_reward_event(), + mock_web3_is_address(), + ): + result = group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[IP_ID, ADDRESS], + ) + + # Verify response structure + assert "tx_hash" in result + assert result["tx_hash"] == TX_HASH + assert "claimed_rewards" in result + assert result["claimed_rewards"] == [{"amount": 100, "token": ADDRESS}] + + def test_claim_rewards_transaction_build_failure( + self, group: Group, mock_web3_is_address + ): + """Test claim_rewards when transaction building fails.""" + with mock_web3_is_address(True): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + side_effect=Exception("Transaction build failed"), + ): + with pytest.raises( + ValueError, + match="Failed to claim rewards: Transaction build failed", + ): + group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[IP_ID], + ) From 8278136f0cdc577d1d06e2d5729f8f517706a152 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:09:54 +0800 Subject: [PATCH 04/15] feat: add claimReward function to GroupingModule in config.json --- src/story_protocol_python_sdk/scripts/config.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index 58573fa3..b1d9252c 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -179,7 +179,12 @@ { "contract_name": "GroupingModule", "contract_address": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", - "functions": ["registerGroup", "addIp", "IPGroupRegistered"] + "functions": [ + "registerGroup", + "addIp", + "IPGroupRegistered", + "claimReward" + ] }, { "contract_name": "LicenseRegistry", From 4c2b698087a2f2d22ff8cf0c44a7193d4a964069 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:10:10 +0800 Subject: [PATCH 05/15] feat: update imports in __init__.py to include ClaimReward and RegisterPILTermsAndAttachResponse --- src/story_protocol_python_sdk/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 6107fee3..6a095b39 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -8,7 +8,11 @@ from .resources.WIP import WIP from .story_client import StoryClient from .types.common import AccessPermission -from .types.resource.IPAsset import RegistrationResponse +from .types.resource.Group import ClaimReward, ClaimRewardsResponse +from .types.resource.IPAsset import ( + RegisterPILTermsAndAttachResponse, + RegistrationResponse, +) from .utils.constants import ( DEFAULT_FUNCTION_SELECTOR, MAX_ROYALTY_TOKEN, @@ -34,6 +38,9 @@ "DerivativeDataInput", "IPMetadataInput", "RegistrationResponse", + "ClaimRewardsResponse", + "ClaimReward", + "RegisterPILTermsAndAttachResponse", # Constants "ZERO_ADDRESS", "ZERO_HASH", From 97378e640a5fad6fee73633ee2476682403967ec Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:11:21 +0800 Subject: [PATCH 06/15] refactor: remove debug print statements from Group and integration tests --- src/story_protocol_python_sdk/resources/Group.py | 1 - tests/integration/test_integration_group.py | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 467b98af..ce7f4204 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -556,7 +556,6 @@ def claim_rewards( raise ValueError(f"Invalid currency token: {currency_token}") for ip_id in member_ip_ids: if not self.web3.is_address(ip_id): - print("ip_id", ip_id) raise ValueError(f"Invalid member IP ID: {ip_id}") claim_reward_param = { diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index 3042ceff..58b30797 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -545,9 +545,7 @@ def test_claim_rewards(self, story_client: StoryClient, setup_royalty_collection """Test claiming rewards for group members.""" group_ip_id = setup_royalty_collection["group_ip_id"] ip_ids = setup_royalty_collection["ip_ids"] - print("group_ip_id", group_ip_id) - print("ip_ids", ip_ids) - # First, collect and distribute royalties to set up rewards for claiming + # Collect and distribute royalties to set up rewards for claiming story_client.Group.collect_and_distribute_group_royalties( group_ip_id=group_ip_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids ) @@ -558,7 +556,6 @@ def test_claim_rewards(self, story_client: StoryClient, setup_royalty_collection currency_token=MockERC20, member_ip_ids=ip_ids, ) - print("response", response) # Verify response structure assert "tx_hash" in response assert isinstance(response["tx_hash"], str) From 695e9d28af003d916e5dde0d8437a7b5ddf918d0 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 1 Sep 2025 13:44:08 +0800 Subject: [PATCH 07/15] refactor: update claim_rewards method in Group to enhance response structure and remove unused parsing function --- .../resources/Group.py | 51 ++++------- .../types/resource/Group.py | 11 +-- tests/integration/test_integration_group.py | 16 ++-- tests/unit/resources/test_group.py | 90 +++++++------------ 4 files changed, 58 insertions(+), 110 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index ce7f4204..30986adf 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -543,9 +543,9 @@ def claim_rewards( """ Claim rewards for the entire group. - :param group_ip_id str: The ID of the group IP. - :param currency_token str: The address of the currency (revenue) token to claim.. - :param member_ip_ids list: The IDs of the member IPs to distribute the rewards to. + :param group_ip_id Address: The ID of the group IP. + :param currency_token Address: The address of the currency (revenue) token to claim. + :param member_ip_ids list[Address]: The IDs of the member IPs to distribute the rewards to. :param tx_options dict: [Optional] The transaction options. :return ClaimRewardsResponse: A response object with the transaction hash and claimed rewards. """ @@ -571,14 +571,21 @@ def claim_rewards( *claim_reward_param.values(), tx_options=tx_options, ) - - claimed_rewards = self._parse_tx_claimed_reward_event( + claimed_rewards = self.grouping_module_client.contract.events.ClaimedReward.process_receipt( response["tx_receipt"] - ) - + )[ + 0 + ][ + "args" + ] return ClaimRewardsResponse( tx_hash=response["tx_hash"], - claimed_rewards=claimed_rewards, + claimed_rewards=ClaimReward( + ip_ids=claimed_rewards["ipId"], + amounts=claimed_rewards["amount"], + token=claimed_rewards["token"], + group_id=claimed_rewards["groupId"], + ), ) except Exception as e: @@ -748,31 +755,3 @@ def _parse_tx_royalty_paid_event(self, tx_receipt: dict) -> list: ) return royalties_distributed - - def _parse_tx_claimed_reward_event(self, tx_receipt: dict) -> list[ClaimReward]: - """ - Parse the ClaimedReward event from a transaction receipt. - - :param tx_receipt dict: The transaction receipt. - :return list: List of claimed rewards. - """ - event_signature = self.web3.keccak( - text="ClaimedReward(address,address,address,uint256)" - ).hex() - claimed_rewards = [] - - for log in tx_receipt["logs"]: - if log["topics"][0].hex() == event_signature: - ip_id = "0x" + log["topics"][0].hex()[24:] - amount = int(log["data"][:66].hex(), 16) - token = "0x" + log["topics"][2].hex()[24:] - - claimed_rewards.append( - ClaimReward( - ip_id=ip_id, - amount=amount, - token=token, - ) - ) - - return claimed_rewards diff --git a/src/story_protocol_python_sdk/types/resource/Group.py b/src/story_protocol_python_sdk/types/resource/Group.py index 62d0b2ec..e6c225e2 100644 --- a/src/story_protocol_python_sdk/types/resource/Group.py +++ b/src/story_protocol_python_sdk/types/resource/Group.py @@ -1,6 +1,6 @@ from typing import TypedDict -from ens.ens import Address, HexStr +from ens.ens import Address, HexBytes class ClaimReward(TypedDict): @@ -8,9 +8,10 @@ class ClaimReward(TypedDict): Structure for a claimed reward. """ - ip_id: Address - amount: int + ip_ids: list[Address] + amounts: list[int] token: Address + group_id: Address class ClaimRewardsResponse(TypedDict): @@ -18,5 +19,5 @@ class ClaimRewardsResponse(TypedDict): Response structure for Group.claim_rewards method. """ - tx_hash: HexStr - claimed_rewards: list[ClaimReward] + tx_hash: HexBytes + claimed_rewards: ClaimReward diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index 58b30797..e9327814 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -549,22 +549,16 @@ def test_claim_rewards(self, story_client: StoryClient, setup_royalty_collection story_client.Group.collect_and_distribute_group_royalties( group_ip_id=group_ip_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids ) - # Test claiming rewards for specific members response = story_client.Group.claim_rewards( group_ip_id=group_ip_id, currency_token=MockERC20, member_ip_ids=ip_ids, ) - # Verify response structure assert "tx_hash" in response assert isinstance(response["tx_hash"], str) - assert len(response["tx_hash"]) > 0 - - # Verify claimed rewards details if any are present - if response["claimed_rewards"]: - for reward in response["claimed_rewards"]: - assert "amount" in reward - assert isinstance(reward["amount"], int) - assert "token" in reward - assert story_client.web3.is_address(reward["token"]) + assert "claimed_rewards" in response + assert len(response["claimed_rewards"]["ip_ids"]) == 2 + assert len(response["claimed_rewards"]["amounts"]) == 2 + assert response["claimed_rewards"]["token"] == MockERC20 + assert response["claimed_rewards"]["group_id"] == group_ip_id diff --git a/tests/unit/resources/test_group.py b/tests/unit/resources/test_group.py index 3bee6893..0bf7595d 100644 --- a/tests/unit/resources/test_group.py +++ b/tests/unit/resources/test_group.py @@ -1,8 +1,9 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from story_protocol_python_sdk.resources.Group import Group +from story_protocol_python_sdk.types.resource.Group import ClaimReward from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH @@ -11,44 +12,6 @@ def group(mock_web3, mock_account): return Group(mock_web3, mock_account, CHAIN_ID) -@pytest.fixture(scope="class") -def mock_grouping_module_client(group): - def _mock(): - return patch.object( - group.grouping_module_client, - "build_claimReward_transaction", - return_value=MagicMock(), - ) - - return _mock - - -@pytest.fixture(scope="class") -def mock_build_and_send_transaction(): - def _mock(): - return patch( - "story_protocol_python_sdk.resources.Group.build_and_send_transaction", - return_value={ - "tx_hash": TX_HASH, - "tx_receipt": {"status": 1, "logs": []}, - }, - ) - - return _mock - - -@pytest.fixture(scope="class") -def mock_parse_tx_claimed_reward_event(group): - def _mock(): - return patch.object( - group, - "_parse_tx_claimed_reward_event", - return_value=[{"amount": 100, "token": ADDRESS}], - ) - - return _mock - - class TestGroupClaimRewards: """Test class for Group.claim_rewards method""" @@ -119,30 +82,41 @@ def test_claim_rewards_mixed_valid_invalid_members(self, group: Group, mock_web3 def test_claim_rewards_success( self, group: Group, - mock_grouping_module_client, - mock_build_and_send_transaction, - mock_parse_tx_claimed_reward_event, mock_web3_is_address, ): """Test successful claim_rewards operation.""" - with ( - mock_grouping_module_client(), - mock_build_and_send_transaction(), - mock_parse_tx_claimed_reward_event(), - mock_web3_is_address(), - ): - result = group.claim_rewards( - group_ip_id=IP_ID, - currency_token=ADDRESS, - member_ip_ids=[IP_ID, ADDRESS], - ) + with mock_web3_is_address(): + with patch.object( + group.grouping_module_client.contract.events.ClaimedReward, + "process_receipt", + return_value=[ + { + "args": { + "ipId": [IP_ID, ADDRESS], + "amount": [100, 200], + "token": ADDRESS, + "groupId": IP_ID, + }, + } + ], + ): + result = group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[IP_ID, ADDRESS], + ) - # Verify response structure - assert "tx_hash" in result - assert result["tx_hash"] == TX_HASH - assert "claimed_rewards" in result - assert result["claimed_rewards"] == [{"amount": 100, "token": ADDRESS}] + # Verify response structure + assert "tx_hash" in result + assert result["tx_hash"] == TX_HASH.hex() + assert "claimed_rewards" in result + assert result["claimed_rewards"] == ClaimReward( + ip_ids=[IP_ID, ADDRESS], + amounts=[100, 200], + token=ADDRESS, + group_id=IP_ID, + ) def test_claim_rewards_transaction_build_failure( self, group: Group, mock_web3_is_address From ce27ca209e25070262466394a7cc91ed39461707 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 15:55:34 +0800 Subject: [PATCH 08/15] refactor: improve claim_rewards method in Group to process logs directly and enhance error handling --- .../resources/Group.py | 20 +- tests/unit/resources/test_group.py | 195 ++++++++++++++++-- 2 files changed, 194 insertions(+), 21 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 30986adf..1819234c 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -571,13 +571,19 @@ def claim_rewards( *claim_reward_param.values(), tx_options=tx_options, ) - claimed_rewards = self.grouping_module_client.contract.events.ClaimedReward.process_receipt( - response["tx_receipt"] - )[ - 0 - ][ - "args" - ] + event_signature = self.web3.keccak( + text="ClaimedReward(address,address,address[],uint256[])" + ).hex() + claimed_rewards = None + for log in response["tx_receipt"]["logs"]: + if log["topics"][0].hex() == event_signature: + event_result = self.grouping_module_client.contract.events.ClaimedReward.process_log( + log + ) + claimed_rewards = event_result["args"] + break + if not claimed_rewards: + raise ValueError("Not found ClaimedReward event in transaction logs.") return ClaimRewardsResponse( tx_hash=response["tx_hash"], claimed_rewards=ClaimReward( diff --git a/tests/unit/resources/test_group.py b/tests/unit/resources/test_group.py index 0bf7595d..a2d3ad6e 100644 --- a/tests/unit/resources/test_group.py +++ b/tests/unit/resources/test_group.py @@ -3,7 +3,10 @@ import pytest from story_protocol_python_sdk.resources.Group import Group -from story_protocol_python_sdk.types.resource.Group import ClaimReward +from story_protocol_python_sdk.types.resource.Group import ( + ClaimReward, + ClaimRewardsResponse, +) from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH @@ -85,22 +88,36 @@ def test_claim_rewards_success( mock_web3_is_address, ): """Test successful claim_rewards operation.""" - with mock_web3_is_address(): - with patch.object( + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": { + "logs": [ + { + "topics": [ + group.web3.keccak( + text="ClaimedReward(address,address,address[],uint256[])" + ) + ] + } + ] + }, + }, + ), patch.object( group.grouping_module_client.contract.events.ClaimedReward, - "process_receipt", - return_value=[ - { - "args": { - "ipId": [IP_ID, ADDRESS], - "amount": [100, 200], - "token": ADDRESS, - "groupId": IP_ID, - }, + "process_log", + return_value={ + "args": { + "ipId": [IP_ID, ADDRESS], + "amount": [100, 200], + "token": ADDRESS, + "groupId": IP_ID, } - ], + }, ): + # Test without tx_options result = group.claim_rewards( group_ip_id=IP_ID, currency_token=ADDRESS, @@ -109,7 +126,7 @@ def test_claim_rewards_success( # Verify response structure assert "tx_hash" in result - assert result["tx_hash"] == TX_HASH.hex() + assert result["tx_hash"] == TX_HASH assert "claimed_rewards" in result assert result["claimed_rewards"] == ClaimReward( ip_ids=[IP_ID, ADDRESS], @@ -117,6 +134,156 @@ def test_claim_rewards_success( token=ADDRESS, group_id=IP_ID, ) + assert result == ClaimRewardsResponse( + tx_hash=TX_HASH, + claimed_rewards=ClaimReward( + ip_ids=[IP_ID, ADDRESS], + amounts=[100, 200], + token=ADDRESS, + group_id=IP_ID, + ), + ) + + def test_claim_rewards_with_tx_options( + self, + group: Group, + mock_web3_is_address, + ): + """Test claim_rewards with transaction options.""" + with mock_web3_is_address(): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": { + "logs": [ + { + "topics": [ + group.web3.keccak( + text="ClaimedReward(address,address,address[],uint256[])" + ) + ] + } + ] + }, + }, + ) as mock_build_and_send, patch.object( + group.grouping_module_client.contract.events.ClaimedReward, + "process_log", + return_value={ + "args": { + "ipId": [IP_ID, ADDRESS], + "amount": [100, 200], + "token": ADDRESS, + "groupId": IP_ID, + } + }, + ): + tx_options = {"gas": 200000, "gasPrice": 20000000000} + result = group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[IP_ID, ADDRESS], + tx_options=tx_options, + ) + + # Verify tx_options were passed to build_and_send_transaction + mock_build_and_send.assert_called_once() + call_args = mock_build_and_send.call_args + assert call_args[1]["tx_options"] == tx_options + + # Verify response with tx_options + assert result["tx_hash"] == TX_HASH + assert result["claimed_rewards"] == ClaimReward( + ip_ids=[IP_ID, ADDRESS], + amounts=[100, 200], + token=ADDRESS, + group_id=IP_ID, + ) + + def test_claim_rewards_no_event_found( + self, + group: Group, + mock_web3_is_address, + ): + """Test claim_rewards when no ClaimedReward event is found.""" + with mock_web3_is_address(): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": { + "logs": [ + {"topics": [group.web3.keccak(text="DifferentEvent()")]} + ] + }, + }, + ), patch.object( + group.grouping_module_client.contract.events.ClaimedReward, + "process_log", + return_value={"args": {}}, + ): + with pytest.raises( + ValueError, + match="Failed to claim rewards: Not found ClaimedReward event in transaction logs.", + ): + group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[IP_ID], + ) + + def test_claim_rewards_empty_member_ip_ids( + self, + group: Group, + mock_web3_is_address, + ): + """Test claim_rewards with empty member IP IDs list.""" + with mock_web3_is_address(): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": { + "logs": [ + { + "topics": [ + group.web3.keccak( + text="ClaimedReward(address,address,address[],uint256[])" + ) + ] + } + ] + }, + }, + ), patch.object( + group.grouping_module_client.contract.events.ClaimedReward, + "process_log", + return_value={ + "args": { + "ipId": [], + "amount": [], + "token": ADDRESS, + "groupId": IP_ID, + } + }, + ): + result = group.claim_rewards( + group_ip_id=IP_ID, + currency_token=ADDRESS, + member_ip_ids=[], + ) + + # Verify response structure + assert "tx_hash" in result + assert result["tx_hash"] == TX_HASH + assert "claimed_rewards" in result + assert result["claimed_rewards"] == ClaimReward( + ip_ids=[], + amounts=[], + token=ADDRESS, + group_id=IP_ID, + ) def test_claim_rewards_transaction_build_failure( self, group: Group, mock_web3_is_address From 2911569d744a4477a0ce937145196ad6cb887664 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 16:00:50 +0800 Subject: [PATCH 09/15] refactor: simplify test assertions in TestGroupClaimRewards for claim_rewards method --- tests/unit/resources/test_group.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/unit/resources/test_group.py b/tests/unit/resources/test_group.py index a2d3ad6e..7ef683bb 100644 --- a/tests/unit/resources/test_group.py +++ b/tests/unit/resources/test_group.py @@ -117,23 +117,12 @@ def test_claim_rewards_success( } }, ): - # Test without tx_options result = group.claim_rewards( group_ip_id=IP_ID, currency_token=ADDRESS, member_ip_ids=[IP_ID, ADDRESS], ) - # Verify response structure - assert "tx_hash" in result - assert result["tx_hash"] == TX_HASH - assert "claimed_rewards" in result - assert result["claimed_rewards"] == ClaimReward( - ip_ids=[IP_ID, ADDRESS], - amounts=[100, 200], - token=ADDRESS, - group_id=IP_ID, - ) assert result == ClaimRewardsResponse( tx_hash=TX_HASH, claimed_rewards=ClaimReward( From fafe899e704eedcd1eec5f34a8b9550b8c40d75b Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 1 Sep 2025 17:23:53 +0800 Subject: [PATCH 10/15] feat: add collect_royalties functionality to Group and GroupingModuleClient --- src/story_protocol_python_sdk/__init__.py | 7 ++- .../GroupingModule/GroupingModule_client.py | 8 ++++ .../resources/Group.py | 43 +++++++++++++++++++ .../scripts/config.json | 3 +- .../types/resource/Group.py | 9 ++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 6a095b39..7222ed6c 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -8,7 +8,11 @@ from .resources.WIP import WIP from .story_client import StoryClient from .types.common import AccessPermission -from .types.resource.Group import ClaimReward, ClaimRewardsResponse +from .types.resource.Group import ( + ClaimReward, + ClaimRewardsResponse, + CollectRoyaltiesResponse, +) from .types.resource.IPAsset import ( RegisterPILTermsAndAttachResponse, RegistrationResponse, @@ -40,6 +44,7 @@ "RegistrationResponse", "ClaimRewardsResponse", "ClaimReward", + "CollectRoyaltiesResponse", "RegisterPILTermsAndAttachResponse", # Constants "ZERO_ADDRESS", diff --git a/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py b/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py index 1d647ed9..e207d8df 100644 --- a/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py +++ b/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py @@ -51,6 +51,14 @@ def build_claimReward_transaction(self, groupId, token, ipIds, tx_params): groupId, token, ipIds ).build_transaction(tx_params) + def collectRoyalties(self, groupId, token): + return self.contract.functions.collectRoyalties(groupId, token).transact() + + def build_collectRoyalties_transaction(self, groupId, token, tx_params): + return self.contract.functions.collectRoyalties( + groupId, token + ).build_transaction(tx_params) + def registerGroup(self, groupPool): return self.contract.functions.registerGroup(groupPool).transact() diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 1819234c..ab5d4c12 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -29,6 +29,7 @@ from story_protocol_python_sdk.types.resource.Group import ( ClaimReward, ClaimRewardsResponse, + CollectRoyaltiesResponse, ) from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH from story_protocol_python_sdk.utils.license_terms import LicenseTerms @@ -597,6 +598,48 @@ def claim_rewards( except Exception as e: raise ValueError(f"Failed to claim rewards: {str(e)}") + def collect_royalties( + self, + group_ip_id: Address, + currency_token: Address, + tx_options: dict | None = None, + ) -> CollectRoyaltiesResponse: + """ + Collects royalties into the pool, making them claimable by group member IPs. + + :param group_ip_id Address: The ID of the group IP. + :param currency_token Address: The address of the currency (revenue) token to collect. + :param tx_options dict: [Optional] The transaction options. + :return CollectRoyaltiesResponse: A response object with the transaction hash and collected royalties. + """ + try: + if not self.web3.is_address(group_ip_id): + raise ValueError(f"Invalid group IP ID: {group_ip_id}") + if not self.web3.is_address(currency_token): + raise ValueError(f"Invalid currency token: {currency_token}") + + response = build_and_send_transaction( + self.web3, + self.account, + self.grouping_module_client.build_collectRoyalties_transaction, + group_ip_id, + currency_token, + tx_options=tx_options, + ) + collected_royalties = self.grouping_module_client.contract.events.CollectedRoyaltiesToGroupPool.process_receipt( + response["tx_receipt"] + )[ + 0 + ][ + "args" + ] + return CollectRoyaltiesResponse( + tx_hash=response["tx_hash"], + collected_royalties=collected_royalties["amount"], + ) + except Exception as e: + raise ValueError(f"Failed to collect royalties: {str(e)}") + def _get_license_data(self, license_data: list) -> list: """ Process license data into the format expected by the contracts. diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index b1d9252c..bf797ad8 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -183,7 +183,8 @@ "registerGroup", "addIp", "IPGroupRegistered", - "claimReward" + "claimReward", + "collectRoyalties" ] }, { diff --git a/src/story_protocol_python_sdk/types/resource/Group.py b/src/story_protocol_python_sdk/types/resource/Group.py index e6c225e2..4d262d1c 100644 --- a/src/story_protocol_python_sdk/types/resource/Group.py +++ b/src/story_protocol_python_sdk/types/resource/Group.py @@ -21,3 +21,12 @@ class ClaimRewardsResponse(TypedDict): tx_hash: HexBytes claimed_rewards: ClaimReward + + +class CollectRoyaltiesResponse(TypedDict): + """ + Response structure for Group.collect_royalties method. + """ + + tx_hash: HexBytes + collected_royalties: int From 63f3f1faea1983feeb0d8912ded12ad7033212dc Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 11:02:22 +0800 Subject: [PATCH 11/15] chore: update .gitignore to include .cursor directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 71ae301e..87163b5f 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,4 @@ web3py.md # AI Assistant Configuration CLAUDE.md +.cursor/ From dde228459c6c2a0b0d45c91bce0be1655c3f0a0c Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 11:25:13 +0800 Subject: [PATCH 12/15] fix: improve royalty collection logic in Group by processing logs directly for event signatures --- .../resources/Group.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index ab5d4c12..18cb4d54 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -626,16 +626,23 @@ def collect_royalties( currency_token, tx_options=tx_options, ) - collected_royalties = self.grouping_module_client.contract.events.CollectedRoyaltiesToGroupPool.process_receipt( - response["tx_receipt"] - )[ - 0 - ][ - "args" - ] + + event_signature = self.web3.keccak( + text="CollectedRoyaltiesToGroupPool(address,address,address,uint256)" + ).hex() + + collected_royalties = 0 + for log in response["tx_receipt"]["logs"]: + if log["topics"][0].hex() == event_signature: + event_results = self.grouping_module_client.contract.events.CollectedRoyaltiesToGroupPool.process_log( + log + ) + collected_royalties = event_results["args"]["amount"] + break + return CollectRoyaltiesResponse( tx_hash=response["tx_hash"], - collected_royalties=collected_royalties["amount"], + collected_royalties=collected_royalties, ) except Exception as e: raise ValueError(f"Failed to collect royalties: {str(e)}") From 0f00bbe3de1bac8b207ecef9b77b2cfabfef5dad Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 11:34:14 +0800 Subject: [PATCH 13/15] refactor: restructure integration tests for Group functionality by introducing a helper class and enhancing test methods for royalty collection and reward claiming --- tests/integration/test_integration_group.py | 644 ++++++-------------- 1 file changed, 194 insertions(+), 450 deletions(-) diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index e9327814..f0e360db 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -1,8 +1,6 @@ -# tests/integration/test_integration_group.py +import time -import copy - -import pytest +from ens.ens import Address from story_protocol_python_sdk.story_client import StoryClient @@ -12,337 +10,18 @@ ROYALTY_POLICY_LRP, ZERO_ADDRESS, MockERC20, - account, web3, ) -# class TestGroupBasicOperations: -# def test_register_basic_group(self, story_client): -# response = story_client.Group.register_group( -# group_pool=EVEN_SPLIT_GROUP_POOL -# ) - -# assert 'tx_hash' in response -# assert isinstance(response['tx_hash'], str) -# assert len(response['tx_hash']) > 0 - -# assert 'group_id' in response -# assert isinstance(response['group_id'], str) -# assert response['group_id'].startswith("0x") - -# class TestGroupWithLicenseOperations: -# @pytest.fixture(scope="module") -# def nft_collection(self, story_client): -# tx_data = story_client.NFTClient.create_nft_collection( -# name="test-collection", -# symbol="TEST", -# max_supply=100, -# is_public_minting=True, -# mint_open=True, -# contract_uri="test-uri", -# mint_fee_recipient=account.address, -# ) -# return tx_data['nft_contract'] - -# @pytest.fixture(scope="module") -# def ip_with_license(self, story_client, nft_collection): -# # Create initial IP with license terms -# response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( -# spg_nft_contract=nft_collection, -# terms=[{ -# 'terms': { -# 'transferable': True, -# 'royalty_policy': ROYALTY_POLICY_LRP, -# 'default_minting_fee': 0, -# 'expiration': 1000, -# 'commercial_use': True, -# 'commercial_attribution': False, -# 'commercializer_checker': ZERO_ADDRESS, -# 'commercializer_checker_data': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'commercial_rev_ceiling': 0, -# 'derivatives_allowed': True, -# 'derivatives_attribution': True, -# 'derivatives_approval': False, -# 'derivatives_reciprocal': True, -# 'derivative_rev_ceiling': 0, -# 'currency': MockERC20, -# 'uri': "test case" -# }, -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': EVEN_SPLIT_GROUP_POOL -# } -# }] -# ) - -# ip_id = response['ip_id'] -# license_terms_id = response['license_terms_ids'][0] - -# licensing_config = { -# 'isSet': True, -# 'mintingFee': 0, -# 'licensingHook': ZERO_ADDRESS, -# 'hookData': ZERO_ADDRESS, -# 'commercialRevShare': 0, -# 'disabled': False, -# 'expectMinimumGroupRewardShare': 0, -# 'expectGroupRewardPool': EVEN_SPLIT_GROUP_POOL -# } - -# # Set licensing config -# story_client.License.set_licensing_config( -# ip_id=ip_id, -# license_terms_id=license_terms_id, -# license_template=PIL_LICENSE_TEMPLATE, -# licensing_config=licensing_config -# ) - -# return { -# 'ip_id': ip_id, -# 'license_terms_id': license_terms_id -# } - -# @pytest.fixture(scope="module") -# def group_with_license(self, story_client, ip_with_license): -# response = story_client.Group.register_group_and_attach_license( -# group_pool=EVEN_SPLIT_GROUP_POOL, -# license_data={ -# 'license_terms_id': ip_with_license['license_terms_id'], -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': ZERO_ADDRESS -# } -# } -# ) - -# assert 'tx_hash' in response -# assert isinstance(response['tx_hash'], str) - -# assert response is not None -# assert 'group_id' in response -# assert response['group_id'] is not None -# return response['group_id'] - -# def test_register_group_and_attach_license(self, group_with_license): -# assert group_with_license is not None - -# def test_mint_register_ip_attach_license_add_to_group(self, story_client, group_with_license, ip_with_license, nft_collection): -# response = story_client.Group.mint_and_register_ip_and_attach_license_and_add_to_group( -# group_id=group_with_license, -# spg_nft_contract=nft_collection, -# license_data=[{ -# 'license_terms_id': ip_with_license['license_terms_id'], -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': EVEN_SPLIT_GROUP_POOL -# } -# }], -# max_allowed_reward_share=5 -# ) - -# assert 'tx_hash' in response -# assert isinstance(response['tx_hash'], str) -# assert len(response['tx_hash']) > 0 - -# assert 'ip_id' in response -# assert isinstance(response['ip_id'], str) -# assert response['ip_id'].startswith("0x") - -# class TestAdvancedGroupOperations: -# @pytest.fixture(scope="module") -# def nft_collection(self, story_client): -# tx_data = story_client.NFTClient.create_nft_collection( -# name="test-collection", -# symbol="TEST", -# max_supply=100, -# is_public_minting=True, -# mint_open=True, -# contract_uri="test-uri", -# mint_fee_recipient=account.address, -# ) -# return tx_data['nft_contract'] - -# @pytest.fixture(scope="module") -# def ip_with_license(self, story_client, nft_collection): -# response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( -# spg_nft_contract=nft_collection, -# terms=[{ -# 'terms': { -# 'transferable': True, -# 'royalty_policy': ROYALTY_POLICY_LRP, -# 'default_minting_fee': 0, -# 'expiration': 1000, -# 'commercial_use': True, -# 'commercial_attribution': False, -# 'commercializer_checker': ZERO_ADDRESS, -# 'commercializer_checker_data': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'commercial_rev_ceiling': 0, -# 'derivatives_allowed': True, -# 'derivatives_attribution': True, -# 'derivatives_approval': False, -# 'derivatives_reciprocal': True, -# 'derivative_rev_ceiling': 0, -# 'currency': MockERC20, -# 'uri': "test case" -# }, -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': EVEN_SPLIT_GROUP_POOL -# } -# }] -# ) - -# return { -# 'ip_id': response['ip_id'], -# 'license_terms_id': response['license_terms_ids'][0] -# } - -# @pytest.fixture(scope="module") -# def group_id(self, story_client, ip_with_license): -# # Create a group with license attached -# response = story_client.Group.register_group_and_attach_license( -# group_pool=EVEN_SPLIT_GROUP_POOL, -# license_data={ -# 'license_terms_id': ip_with_license['license_terms_id'], -# 'license_template': PIL_LICENSE_TEMPLATE, -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': ZERO_ADDRESS -# } -# } -# ) -# return response['group_id'] - -# def test_register_ip_and_attach_license_and_add_to_group(self, story_client, group_id, ip_with_license): -# token_id = get_token_id(MockERC721, story_client.web3, story_client.account) - -# response = story_client.Group.register_ip_and_attach_license_and_add_to_group( -# group_id=group_id, -# nft_contract=MockERC721, -# token_id=token_id, -# max_allowed_reward_share=5, -# license_data=[{ -# 'license_terms_id': ip_with_license['license_terms_id'], -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': EVEN_SPLIT_GROUP_POOL -# } -# }] -# ) - -# assert 'tx_hash' in response -# assert isinstance(response['tx_hash'], str) -# assert len(response['tx_hash']) > 0 - -# assert 'ip_id' in response -# assert isinstance(response['ip_id'], str) -# assert response['ip_id'].startswith("0x") - -# def test_register_group_and_attach_license_and_add_ips(self, story_client, ip_with_license): -# response = story_client.Group.register_group_and_attach_license_and_add_ips( -# group_pool=EVEN_SPLIT_GROUP_POOL, -# max_allowed_reward_share=5, -# ip_ids=[ip_with_license['ip_id']], -# license_data={ -# 'license_terms_id': ip_with_license['license_terms_id'], -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': ZERO_ADDRESS -# } -# } -# ) - -# assert 'tx_hash' in response -# assert isinstance(response['tx_hash'], str) -# assert len(response['tx_hash']) > 0 - -# assert 'group_id' in response -# assert isinstance(response['group_id'], str) -# assert response['group_id'].startswith("0x") - -# def test_fail_add_unregistered_ip_to_group(self, story_client, ip_with_license): -# with pytest.raises(ValueError, match="Failed to register group and attach license and add IPs"): -# story_client.Group.register_group_and_attach_license_and_add_ips( -# group_pool=EVEN_SPLIT_GROUP_POOL, -# max_allowed_reward_share=5, -# ip_ids=[ZERO_ADDRESS], # Invalid IP address -# license_data={ -# 'license_terms_id': ip_with_license['license_terms_id'], -# 'licensing_config': { -# 'is_set': True, -# 'minting_fee': 0, -# 'hook_data': ZERO_ADDRESS, -# 'licensing_hook': ZERO_ADDRESS, -# 'commercial_rev_share': 0, -# 'disabled': False, -# 'expect_minimum_group_reward_share': 0, -# 'expect_group_reward_pool': ZERO_ADDRESS -# } -# } -# ) - -class TestCollectRoyaltyAndClaimReward: - @pytest.fixture(scope="module") - def nft_collection(self, story_client): - tx_data = story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - max_supply=100, - is_public_minting=True, - mint_open=True, - contract_uri="test-uri", - mint_fee_recipient=account.address, - ) - return tx_data["nft_contract"] +class GroupTestHelper: + """Helper class for Group integration tests.""" - @pytest.fixture(scope="module") - def setup_royalty_collection(self, story_client, nft_collection): - # Create license terms data + @staticmethod + def mint_and_register_ip_asset_with_pil_terms( + story_client: StoryClient, nft_collection: Address + ) -> dict[str, any]: + """Helper to mint and register an IP asset with PIL terms.""" license_terms_data = [ { "terms": { @@ -371,52 +50,111 @@ def setup_royalty_collection(self, story_client, nft_collection): "licensing_hook": ZERO_ADDRESS, "commercial_rev_share": 10, "disabled": False, - "expect_minimum_group_reward_share": 10, + "expect_minimum_group_reward_share": 0, "expect_group_reward_pool": EVEN_SPLIT_GROUP_POOL, }, } ] - # Create unique metadata for each IP - metadata_1 = { - "ip_metadata_uri": "test-uri-1", - "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-1")), - "nft_metadata_uri": "test-nft-uri-1", + + # Create unique metadata + metadata = { + "ip_metadata_uri": f"test-uri-{int(time.time())}", + "ip_metadata_hash": web3.to_hex( + web3.keccak(text=f"test-metadata-hash-{int(time.time())}") + ), + "nft_metadata_uri": f"test-nft-uri-{int(time.time())}", "nft_metadata_hash": web3.to_hex( - web3.keccak(text="test-nft-metadata-hash-a") + web3.keccak(text=f"test-nft-metadata-hash-{int(time.time())}") ), } - metadata_2 = { - "ip_metadata_uri": "test-uri-2", - "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-2")), - "nft_metadata_uri": "test-nft-uri-2", + result = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( + spg_nft_contract=nft_collection, + terms=license_terms_data, + ip_metadata=metadata, + ) + + return { + "ip_id": result["ip_id"], + "license_terms_id": result["license_terms_ids"][0], + } + + @staticmethod + def mint_and_register_ip_and_make_derivative( + story_client: StoryClient, + nft_collection: Address, + group_id: Address, + license_id: int, + ) -> Address: + """Helper to mint and register an IP and make it a derivative of another IP.""" + # Step 1: Mint and register IP + metadata = { + "ip_metadata_uri": f"test-derivative-uri-{int(time.time())}", + "ip_metadata_hash": web3.to_hex( + web3.keccak(text=f"test-derivative-metadata-hash-{int(time.time())}") + ), + "nft_metadata_uri": f"test-derivative-nft-uri-{int(time.time())}", "nft_metadata_hash": web3.to_hex( - web3.keccak(text="test-nft-metadata-hash-2") + web3.keccak( + text=f"test-derivative-nft-metadata-hash-{int(time.time())}" + ) ), } - # Create two IPs - result1 = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( + result = story_client.IPAsset.mint_and_register_ip( spg_nft_contract=nft_collection, - terms=copy.deepcopy(license_terms_data), - ip_metadata=metadata_1, + ip_metadata=metadata, ) - result2 = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( - spg_nft_contract=nft_collection, - terms=copy.deepcopy(license_terms_data), - ip_metadata=metadata_2, + child_ip_id = result["ip_id"] + + # Step 2: Register as derivative + story_client.IPAsset.register_derivative( + child_ip_id=child_ip_id, + parent_ip_ids=[group_id], + license_terms_ids=[license_id], + max_minting_fee=0, + max_rts=10, + max_revenue_share=0, ) - ip_ids = [result1["ip_id"], result2["ip_id"]] - license_terms_id = result1["license_terms_ids"][0] + return child_ip_id + + @staticmethod + def pay_royalty_and_transfer_to_vault( + story_client: StoryClient, + child_ip_id: Address, + group_id: Address, + token: Address, + amount: int, + ) -> None: + """Helper to pay royalty on behalf and transfer to vault.""" + # Pay royalties from group IP id to child IP id + story_client.Royalty.pay_royalty_on_behalf( + receiver_ip_id=child_ip_id, + payer_ip_id=group_id, + token=token, + amount=amount, + ) - # Register group and add IPs - result3 = story_client.Group.register_group_and_attach_license_and_add_ips( + # Transfer to vault + story_client.Royalty.transfer_to_vault( + royalty_policy="LRP", + ip_id=child_ip_id, + ancestor_ip_id=group_id, + token=token, + ) + + @staticmethod + def register_group_and_attach_license( + story_client: StoryClient, license_id: int, ip_ids: list[Address] + ) -> Address: + """Helper to register a group and attach license.""" + result = story_client.Group.register_group_and_attach_license_and_add_ips( group_pool=EVEN_SPLIT_GROUP_POOL, max_allowed_reward_share=100, ip_ids=ip_ids, license_data={ - "license_terms_id": license_terms_id, + "license_terms_id": license_id, "license_template": PIL_LICENSE_TEMPLATE, "licensing_config": { "is_set": True, @@ -431,100 +169,129 @@ def setup_royalty_collection(self, story_client, nft_collection): }, ) - group_ip_id = result3["group_id"] + return result["group_id"] - # Create derivative IPs - Step 1: Mint and register - result4 = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=nft_collection, - ip_metadata={ - "ip_metadata_uri": "test-derivative-uri-4", - "ip_metadata_hash": web3.to_hex( - web3.keccak(text="test-derivative-metadata-hash-1") - ), - "nft_metadata_uri": "test-derivative-nft-uri-4", - "nft_metadata_hash": web3.to_hex( - web3.keccak(text="test-derivative-nft-metadata-hash-1") - ), - }, + +class TestCollectRoyaltyAndClaimReward: + """Test class for collecting royalties and claiming rewards functionality.""" + + def test_collect_royalties( + self, story_client: StoryClient, nft_collection: Address + ): + """Test collecting royalties into the pool.""" + # Register IP id + result1 = GroupTestHelper.mint_and_register_ip_asset_with_pil_terms( + story_client, nft_collection ) - child_ip_id1 = result4["ip_id"] + ip_id = result1["ip_id"] + license_terms_id = result1["license_terms_id"] - # Step 2: Register as derivative - story_client.IPAsset.register_derivative( - child_ip_id=child_ip_id1, - parent_ip_ids=[group_ip_id], - license_terms_ids=[license_terms_id], - max_minting_fee=0, - max_rts=10, - max_revenue_share=0, + # Register group id + group_ip_id = GroupTestHelper.register_group_and_attach_license( + story_client, license_terms_id, [ip_id] ) - # Create second derivative IP - Step 1: Mint and register - result5 = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=nft_collection, - ip_metadata={ - "ip_metadata_uri": "test-derivative-uri-5", - "ip_metadata_hash": web3.to_hex( - web3.keccak(text="test-derivative-metadata-hash-2") - ), - "nft_metadata_uri": "test-derivative-nft-uri-5", - "nft_metadata_hash": web3.to_hex( - web3.keccak(text="test-derivative-nft-metadata-hash-2") - ), - }, + # Mint and register child IP id + child_ip_id = GroupTestHelper.mint_and_register_ip_and_make_derivative( + story_client, nft_collection, group_ip_id, license_terms_id ) - child_ip_id2 = result5["ip_id"] - # Step 2: Register as derivative - story_client.IPAsset.register_derivative( - child_ip_id=child_ip_id2, - parent_ip_ids=[group_ip_id], - license_terms_ids=[license_terms_id], - max_minting_fee=0, - max_rts=10, - max_revenue_share=0, + # Pay royalties from group IP id to child IP id and transfer to vault + GroupTestHelper.pay_royalty_and_transfer_to_vault( + story_client, child_ip_id, group_ip_id, MockERC20, 100 ) - # Pay royalties from child IPs to group IP - story_client.Royalty.pay_royalty_on_behalf( - receiver_ip_id=child_ip_id1, - payer_ip_id=group_ip_id, - token=MockERC20, - amount=100, + # Collect royalties + result = story_client.Group.collect_royalties( + group_ip_id=group_ip_id, currency_token=MockERC20 ) - story_client.Royalty.pay_royalty_on_behalf( - receiver_ip_id=child_ip_id2, - payer_ip_id=group_ip_id, - token=MockERC20, - amount=100, + assert "tx_hash" in result + assert isinstance(result["tx_hash"], str) + assert len(result["tx_hash"]) > 0 + assert result["collected_royalties"] == 10 # 10% of 100 = 10 + + def test_claim_reward(self, story_client: StoryClient, nft_collection: Address): + """Test claiming rewards for group members.""" + # Register IP id + result1 = GroupTestHelper.mint_and_register_ip_asset_with_pil_terms( + story_client, nft_collection ) + ip_id = result1["ip_id"] + license_terms_id = result1["license_terms_id"] - # Transfer to vault - story_client.Royalty.transfer_to_vault( - royalty_policy="LRP", - ip_id=child_ip_id1, - ancestor_ip_id=group_ip_id, - token=MockERC20, + # Register group id + group_ip_id = GroupTestHelper.register_group_and_attach_license( + story_client, license_terms_id, [ip_id] ) - story_client.Royalty.transfer_to_vault( - royalty_policy="LRP", - ip_id=child_ip_id2, - ancestor_ip_id=group_ip_id, - token=MockERC20, + # Mint license tokens to the IP id which doesn't have a royalty vault + story_client.License.mint_license_tokens( + licensor_ip_id=ip_id, + license_template=PIL_LICENSE_TEMPLATE, + license_terms_id=license_terms_id, + amount=100, + receiver=ip_id, + max_minting_fee=1, + max_revenue_share=100, ) - return {"group_ip_id": group_ip_id, "ip_ids": ip_ids} + # Claim reward + result = story_client.Group.claim_rewards( + group_ip_id=group_ip_id, + currency_token=MockERC20, + member_ip_ids=[ip_id], + ) + + assert "tx_hash" in result + assert isinstance(result["tx_hash"], str) + assert "claimed_rewards" in result + assert len(result["claimed_rewards"]["ip_ids"]) == 1 + assert len(result["claimed_rewards"]["amounts"]) == 1 + assert result["claimed_rewards"]["token"] == MockERC20 + assert result["claimed_rewards"]["group_id"] == group_ip_id def test_collect_and_distribute_group_royalties( - self, story_client, setup_royalty_collection + self, story_client: StoryClient, nft_collection: Address ): - group_ip_id = setup_royalty_collection["group_ip_id"] - ip_ids = setup_royalty_collection["ip_ids"] + """Test collecting and distributing group royalties in one transaction.""" + ip_ids = [] + # Create two IPs + result1 = GroupTestHelper.mint_and_register_ip_asset_with_pil_terms( + story_client, nft_collection + ) + result2 = GroupTestHelper.mint_and_register_ip_asset_with_pil_terms( + story_client, nft_collection + ) + ip_ids.append(result1["ip_id"]) + ip_ids.append(result2["ip_id"]) + license_terms_id = result1["license_terms_id"] + + # Register group + group_id = GroupTestHelper.register_group_and_attach_license( + story_client, license_terms_id, ip_ids + ) + + # Create derivative IPs + child_ip_id1 = GroupTestHelper.mint_and_register_ip_and_make_derivative( + story_client, nft_collection, group_id, license_terms_id + ) + child_ip_id2 = GroupTestHelper.mint_and_register_ip_and_make_derivative( + story_client, nft_collection, group_id, license_terms_id + ) + + # Pay royalties + GroupTestHelper.pay_royalty_and_transfer_to_vault( + story_client, child_ip_id1, group_id, MockERC20, 100 + ) + GroupTestHelper.pay_royalty_and_transfer_to_vault( + story_client, child_ip_id2, group_id, MockERC20, 100 + ) + + # Collect and distribute royalties response = story_client.Group.collect_and_distribute_group_royalties( - group_ip_id=group_ip_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids + group_ip_id=group_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids ) assert "tx_hash" in response @@ -532,7 +299,6 @@ def test_collect_and_distribute_group_royalties( assert len(response["tx_hash"]) > 0 assert "collected_royalties" in response - assert len(response["collected_royalties"]) > 0 assert response["collected_royalties"][0]["amount"] == 20 @@ -540,25 +306,3 @@ def test_collect_and_distribute_group_royalties( assert len(response["royalties_distributed"]) == 2 assert response["royalties_distributed"][0]["amount"] == 10 assert response["royalties_distributed"][1]["amount"] == 10 - - def test_claim_rewards(self, story_client: StoryClient, setup_royalty_collection): - """Test claiming rewards for group members.""" - group_ip_id = setup_royalty_collection["group_ip_id"] - ip_ids = setup_royalty_collection["ip_ids"] - # Collect and distribute royalties to set up rewards for claiming - story_client.Group.collect_and_distribute_group_royalties( - group_ip_id=group_ip_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids - ) - # Test claiming rewards for specific members - response = story_client.Group.claim_rewards( - group_ip_id=group_ip_id, - currency_token=MockERC20, - member_ip_ids=ip_ids, - ) - assert "tx_hash" in response - assert isinstance(response["tx_hash"], str) - assert "claimed_rewards" in response - assert len(response["claimed_rewards"]["ip_ids"]) == 2 - assert len(response["claimed_rewards"]["amounts"]) == 2 - assert response["claimed_rewards"]["token"] == MockERC20 - assert response["claimed_rewards"]["group_id"] == group_ip_id From d634f53349e2fa6cb81438e291fcb96284f8b2f1 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 14:10:33 +0800 Subject: [PATCH 14/15] test: add unit tests for Group.collect_royalties method covering various scenarios including invalid inputs and successful transactions --- tests/unit/resources/test_group.py | 130 +++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tests/unit/resources/test_group.py b/tests/unit/resources/test_group.py index 7ef683bb..bfbcf45d 100644 --- a/tests/unit/resources/test_group.py +++ b/tests/unit/resources/test_group.py @@ -6,6 +6,7 @@ from story_protocol_python_sdk.types.resource.Group import ( ClaimReward, ClaimRewardsResponse, + CollectRoyaltiesResponse, ) from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH @@ -15,6 +16,135 @@ def group(mock_web3, mock_account): return Group(mock_web3, mock_account, CHAIN_ID) +class TestGroupCollectRoyalties: + """Test class for Group.collect_royalties method""" + + def test_collect_royalties_invalid_group_ip_id( + self, group: Group, mock_web3_is_address + ): + """Test collect_royalties with invalid group IP ID.""" + invalid_group_ip_id = "invalid_group_ip_id" + with mock_web3_is_address(False): + with pytest.raises( + ValueError, + match=f"Failed to collect royalties: Invalid group IP ID: {invalid_group_ip_id}", + ): + group.collect_royalties( + group_ip_id=invalid_group_ip_id, + currency_token=ADDRESS, + ) + + def test_collect_royalties_invalid_currency_token(self, group: Group, mock_web3): + """Test collect_royalties with invalid currency token.""" + invalid_currency_token = "invalid_currency_token" + with patch.object(mock_web3, "is_address") as mock_is_address: + # group_ip_id=True, currency_token=False + mock_is_address.side_effect = [True, False] + with pytest.raises( + ValueError, + match=f"Failed to collect royalties: Invalid currency token: {invalid_currency_token}", + ): + group.collect_royalties( + group_ip_id=IP_ID, + currency_token=invalid_currency_token, + ) + + def test_collect_royalties_success( + self, + group: Group, + mock_web3_is_address, + ): + """Test successful collect_royalties operation.""" + collected_amount = 100 + + with mock_web3_is_address(): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": { + "logs": [ + { + "topics": [ + group.web3.keccak( + text="CollectedRoyaltiesToGroupPool(address,address,address,uint256)" + ) + ] + } + ] + }, + }, + ), patch.object( + group.grouping_module_client.contract.events.CollectedRoyaltiesToGroupPool, + "process_log", + return_value={"args": {"amount": collected_amount}}, + ): + result = group.collect_royalties( + group_ip_id=IP_ID, + currency_token=ADDRESS, + ) + + assert "tx_hash" in result + assert result["tx_hash"] == TX_HASH + assert "collected_royalties" in result + assert result["collected_royalties"] == collected_amount + assert result == CollectRoyaltiesResponse( + tx_hash=TX_HASH, + collected_royalties=collected_amount, + ) + + def test_collect_royalties_no_event_found( + self, + group: Group, + mock_web3_is_address, + ): + """Test collect_royalties when no CollectedRoyaltiesToGroupPool event is found.""" + with mock_web3_is_address(): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + return_value={ + "tx_hash": TX_HASH, + "tx_receipt": { + "logs": [ + {"topics": [group.web3.keccak(text="DifferentEvent()")]} + ] + }, + }, + ), patch.object( + group.grouping_module_client.contract.events.CollectedRoyaltiesToGroupPool, + "process_log", + return_value={"args": {"amount": 0}}, + ): + result = group.collect_royalties( + group_ip_id=IP_ID, + currency_token=ADDRESS, + ) + + # Should return 0 collected royalties when no event is found + assert "tx_hash" in result + assert result["tx_hash"] == TX_HASH + assert "collected_royalties" in result + assert result["collected_royalties"] == 0 + + def test_collect_royalties_transaction_build_failure( + self, group: Group, mock_web3_is_address + ): + """Test collect_royalties when transaction building fails.""" + with mock_web3_is_address(True): + with patch( + "story_protocol_python_sdk.resources.Group.build_and_send_transaction", + side_effect=Exception("Transaction build failed"), + ): + with pytest.raises( + ValueError, + match="Failed to collect royalties: Transaction build failed", + ): + group.collect_royalties( + group_ip_id=IP_ID, + currency_token=ADDRESS, + ) + + class TestGroupClaimRewards: """Test class for Group.claim_rewards method""" From 2fb7f21c94ac5deea90b008cb212a51e20b95b6a Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Sep 2025 14:25:23 +0800 Subject: [PATCH 15/15] refactor: update type hint for mint_and_register_ip_asset_with_pil_terms method in GroupTestHelper to use 'Any' for improved clarity --- tests/integration/test_integration_group.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index f0e360db..2f81928e 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -1,4 +1,5 @@ import time +from typing import Any from ens.ens import Address @@ -20,7 +21,7 @@ class GroupTestHelper: @staticmethod def mint_and_register_ip_asset_with_pil_terms( story_client: StoryClient, nft_collection: Address - ) -> dict[str, any]: + ) -> dict[str, Any]: """Helper to mint and register an IP asset with PIL terms.""" license_terms_data = [ {