From 61417722d375419bfb40a352c512ff3b54a59c89 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 29 Aug 2025 17:09:03 +0800 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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(