From 1607a7789977b95debd74f5817e5c187efab374e Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Sep 2025 11:08:30 +0800 Subject: [PATCH 1/8] feat: add LicensingConfig and LicensingConfigData classes for licensing configuration management --- .../utils/licensing_config_data.py | 121 +++++++++++ .../unit/utils/test_licensing_config_data.py | 189 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 src/story_protocol_python_sdk/utils/licensing_config_data.py create mode 100644 tests/unit/utils/test_licensing_config_data.py diff --git a/src/story_protocol_python_sdk/utils/licensing_config_data.py b/src/story_protocol_python_sdk/utils/licensing_config_data.py new file mode 100644 index 0000000..63ede0c --- /dev/null +++ b/src/story_protocol_python_sdk/utils/licensing_config_data.py @@ -0,0 +1,121 @@ +from dataclasses import dataclass +from typing import TypedDict + +from ens.ens import Address, HexStr + +from story_protocol_python_sdk.types.common import RevShareType +from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH +from story_protocol_python_sdk.utils.validation import ( + get_revenue_share, + validate_address, +) + + +class LicensingConfig(TypedDict): + """ + Structure for licensing configuration. + + Attributes: + is_set: Whether the licensing configuration is active. If false, the configuration is ignored. + minting_fee: The minting fee to be paid when minting license tokens. + licensing_hook: The licensingHook is an address to a smart contract that implements the `ILicensingHook` interface. + This contract's `beforeMintLicenseTokens` function is executed before a user mints a License Token, + allowing for custom validation or business logic to be enforced during the minting process. + For detailed documentation on licensing hook, visit https://docs.story.foundation/concepts/hooks#licensing-hooks + hook_data: The data to be used by the licensing hook. Set to a `zeroHash` if no data is provided. For detailed documentation on hook data, visit https://docs.story.foundation/concepts/hooks#hook-data + commercial_rev_share: Percentage of revenue that must be shared with the licensor. Must be between 0 and 100 (where 100% represents 100_000_000). + disabled: Whether the licensing is disabled or not. If this is true, then no licenses can be minted and no more derivatives can be attached at all. + expect_minimum_group_reward_share: The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100_000_000) that can be allocated to the IP when it is added to the group. + Must be between 0 and 100 (where 100% represents 100_000_000). + expect_group_reward_pool: The address of the expected group reward pool. The IP can only be added to a group with this specified reward pool address, or `zeroAddress` if the IP does not want to be added to any group. + For detailed documentation on group reward pool, visit https://docs.story.foundation/concepts/hooks#group-reward-pool + """ + + is_set: bool + minting_fee: int + licensing_hook: Address + hook_data: HexStr + commercial_rev_share: int + disabled: bool + expect_minimum_group_reward_share: int + expect_group_reward_pool: Address + + +@dataclass +class LicensingConfigData: + """ + Licensing configuration data. + """ + + @classmethod + def from_tuple(cls, tuple_data: tuple) -> LicensingConfig: + """ + Convert tuple data to LicensingConfig. + + Args: + tuple_data: tuple data + + Returns: + LicensingConfig + """ + return LicensingConfig( + is_set=tuple_data[0], + minting_fee=tuple_data[1], + licensing_hook=tuple_data[2], + hook_data=tuple_data[3], + commercial_rev_share=tuple_data[4], + disabled=tuple_data[5], + expect_minimum_group_reward_share=tuple_data[6], + expect_group_reward_pool=tuple_data[7], + ) + + @classmethod + def validate_license_config( + cls, licensing_config: LicensingConfig | None = None + ) -> LicensingConfig: + """ + Validates and normalizes licensing configuration. + + If no licensing_config is provided, returns default values. + + Args: + licensing_config: Optional licensing configuration input + + Returns: + LicensingConfig: Validated and normalized licensing configuration + + Raises: + ValueError: If validation fails for any field + """ + if licensing_config is None: + return LicensingConfig( + is_set=False, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=0, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ) + + if licensing_config["minting_fee"] < 0: + raise ValueError("The minting_fee must be greater than 0.") + + return LicensingConfig( + is_set=licensing_config["is_set"], + minting_fee=licensing_config["minting_fee"], + licensing_hook=validate_address(licensing_config["licensing_hook"]), + hook_data=licensing_config["hook_data"], + commercial_rev_share=get_revenue_share( + licensing_config["commercial_rev_share"] + ), + disabled=licensing_config["disabled"], + expect_minimum_group_reward_share=get_revenue_share( + licensing_config["expect_minimum_group_reward_share"], + RevShareType.EXPECT_MINIMUM_GROUP_REWARD_SHARE, + ), + expect_group_reward_pool=validate_address( + licensing_config["expect_group_reward_pool"] + ), + ) diff --git a/tests/unit/utils/test_licensing_config_data.py b/tests/unit/utils/test_licensing_config_data.py new file mode 100644 index 0000000..f0fdf9f --- /dev/null +++ b/tests/unit/utils/test_licensing_config_data.py @@ -0,0 +1,189 @@ +import pytest + +from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH +from story_protocol_python_sdk.utils.licensing_config_data import ( + LicensingConfig, + LicensingConfigData, +) + + +class TestValidateLicenseConfig: + def test_validate_license_config_default_values(self): + """Test validate_license_config with no input returns default values.""" + result = LicensingConfigData.validate_license_config() + assert result == LicensingConfig( + is_set=False, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=0, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ) + + def test_validate_license_config_valid_input(self): + """Test validate_license_config with valid input.""" + input_config: LicensingConfig = { + "is_set": True, + "minting_fee": 100, + "licensing_hook": ZERO_ADDRESS, + "hook_data": "0xabcdef", + "commercial_rev_share": 50, + "disabled": False, + "expect_minimum_group_reward_share": 25, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + result = LicensingConfigData.validate_license_config(input_config) + + assert result == LicensingConfig( + is_set=True, + minting_fee=100, + licensing_hook=ZERO_ADDRESS, + hook_data="0xabcdef", + commercial_rev_share=50 * 10**6, + disabled=False, + expect_minimum_group_reward_share=25 * 10**6, + expect_group_reward_pool=ZERO_ADDRESS, + ) + + def test_validate_license_config_invalid_commercial_rev_share_negative(self): + """Test validate_license_config raises error for negative commercial_rev_share.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": -1, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises( + ValueError, + match="The commercial_rev_share must be between 0 and 100.", + ): + LicensingConfigData.validate_license_config(input_config) + + def test_validate_license_config_invalid_commercial_rev_share_too_high(self): + """Test validate_license_config raises error for commercial_rev_share > 100.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 101, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises( + ValueError, + match="The commercial_rev_share must be between 0 and 100.", + ): + LicensingConfigData.validate_license_config(input_config) + + def test_validate_license_config_invalid_expect_minimum_group_reward_share_negative( + self, + ): + """Test validate_license_config raises error for negative expect_minimum_group_reward_share.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": -1, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises( + ValueError, + match="The expect_minimum_group_reward_share must be between 0 and 100.", + ): + LicensingConfigData.validate_license_config(input_config) + + def test_validate_license_config_invalid_expect_minimum_group_reward_share_too_high( + self, + ): + """Test validate_license_config raises error for expect_minimum_group_reward_share > 100.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 101, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises( + ValueError, + match="The expect_minimum_group_reward_share must be between 0 and 100.", + ): + LicensingConfigData.validate_license_config(input_config) + + def test_validate_license_config_invalid_minting_fee_negative(self): + """Test validate_license_config raises error for negative minting_fee.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": -1, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises(ValueError, match="The minting_fee must be greater than 0."): + LicensingConfigData.validate_license_config(input_config) + + def test_validate_license_config_invalid_address(self): + """Test validate_license_config raises error for invalid address.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": "invalid_address", + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises(ValueError, match="Invalid address: invalid_address."): + LicensingConfigData.validate_license_config(input_config) + + +class TestLicensingConfigFromTuple: + def test_licensing_config_from_tuple(self): + """Test licensing_config_from_tuple with valid input.""" + input_tuple = ( + True, + 100, + ZERO_ADDRESS, + ZERO_HASH, + 50, + False, + 25, + ZERO_ADDRESS, + ) + + result = LicensingConfigData.from_tuple(input_tuple) + + assert result == LicensingConfig( + is_set=True, + minting_fee=100, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=25, + expect_group_reward_pool=ZERO_ADDRESS, + ) From 72317c4c54d7d96735c97f4856e5ad98571401b9 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Sep 2025 11:19:04 +0800 Subject: [PATCH 2/8] feat: introduce ValidatedLicensingConfig class and update validation method to return validated configuration --- .../utils/licensing_config_data.py | 49 +++++++++++------ .../unit/utils/test_licensing_config_data.py | 53 ++++++++++++------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/story_protocol_python_sdk/utils/licensing_config_data.py b/src/story_protocol_python_sdk/utils/licensing_config_data.py index 63ede0c..0517ebb 100644 --- a/src/story_protocol_python_sdk/utils/licensing_config_data.py +++ b/src/story_protocol_python_sdk/utils/licensing_config_data.py @@ -41,6 +41,21 @@ class LicensingConfig(TypedDict): expect_group_reward_pool: Address +class ValidatedLicensingConfig(TypedDict): + """ + Validated licensing configuration. + """ + + isSet: bool + mintingFee: int + licensingHook: Address + hookData: HexStr + commercialRevShare: int + disabled: bool + expectMinimumGroupRewardShare: int + expectGroupRewardPool: Address + + @dataclass class LicensingConfigData: """ @@ -72,7 +87,7 @@ def from_tuple(cls, tuple_data: tuple) -> LicensingConfig: @classmethod def validate_license_config( cls, licensing_config: LicensingConfig | None = None - ) -> LicensingConfig: + ) -> ValidatedLicensingConfig: """ Validates and normalizes licensing configuration. @@ -88,34 +103,34 @@ def validate_license_config( ValueError: If validation fails for any field """ if licensing_config is None: - return LicensingConfig( - is_set=False, - minting_fee=0, - licensing_hook=ZERO_ADDRESS, - hook_data=ZERO_HASH, - commercial_rev_share=0, + return ValidatedLicensingConfig( + isSet=False, + mintingFee=0, + licensingHook=ZERO_ADDRESS, + hookData=ZERO_HASH, + commercialRevShare=0, disabled=False, - expect_minimum_group_reward_share=0, - expect_group_reward_pool=ZERO_ADDRESS, + expectMinimumGroupRewardShare=0, + expectGroupRewardPool=ZERO_ADDRESS, ) if licensing_config["minting_fee"] < 0: raise ValueError("The minting_fee must be greater than 0.") - return LicensingConfig( - is_set=licensing_config["is_set"], - minting_fee=licensing_config["minting_fee"], - licensing_hook=validate_address(licensing_config["licensing_hook"]), - hook_data=licensing_config["hook_data"], - commercial_rev_share=get_revenue_share( + return ValidatedLicensingConfig( + isSet=licensing_config["is_set"], + mintingFee=licensing_config["minting_fee"], + licensingHook=validate_address(licensing_config["licensing_hook"]), + hookData=licensing_config["hook_data"], + commercialRevShare=get_revenue_share( licensing_config["commercial_rev_share"] ), disabled=licensing_config["disabled"], - expect_minimum_group_reward_share=get_revenue_share( + expectMinimumGroupRewardShare=get_revenue_share( licensing_config["expect_minimum_group_reward_share"], RevShareType.EXPECT_MINIMUM_GROUP_REWARD_SHARE, ), - expect_group_reward_pool=validate_address( + expectGroupRewardPool=validate_address( licensing_config["expect_group_reward_pool"] ), ) diff --git a/tests/unit/utils/test_licensing_config_data.py b/tests/unit/utils/test_licensing_config_data.py index f0fdf9f..aff1196 100644 --- a/tests/unit/utils/test_licensing_config_data.py +++ b/tests/unit/utils/test_licensing_config_data.py @@ -4,6 +4,7 @@ from story_protocol_python_sdk.utils.licensing_config_data import ( LicensingConfig, LicensingConfigData, + ValidatedLicensingConfig, ) @@ -11,15 +12,15 @@ class TestValidateLicenseConfig: def test_validate_license_config_default_values(self): """Test validate_license_config with no input returns default values.""" result = LicensingConfigData.validate_license_config() - assert result == LicensingConfig( - is_set=False, - minting_fee=0, - licensing_hook=ZERO_ADDRESS, - hook_data=ZERO_HASH, - commercial_rev_share=0, + assert result == ValidatedLicensingConfig( + isSet=False, + mintingFee=0, + licensingHook=ZERO_ADDRESS, + hookData=ZERO_HASH, + commercialRevShare=0, disabled=False, - expect_minimum_group_reward_share=0, - expect_group_reward_pool=ZERO_ADDRESS, + expectMinimumGroupRewardShare=0, + expectGroupRewardPool=ZERO_ADDRESS, ) def test_validate_license_config_valid_input(self): @@ -37,15 +38,15 @@ def test_validate_license_config_valid_input(self): result = LicensingConfigData.validate_license_config(input_config) - assert result == LicensingConfig( - is_set=True, - minting_fee=100, - licensing_hook=ZERO_ADDRESS, - hook_data="0xabcdef", - commercial_rev_share=50 * 10**6, + assert result == ValidatedLicensingConfig( + isSet=True, + mintingFee=100, + licensingHook=ZERO_ADDRESS, + hookData="0xabcdef", + commercialRevShare=50 * 10**6, disabled=False, - expect_minimum_group_reward_share=25 * 10**6, - expect_group_reward_pool=ZERO_ADDRESS, + expectMinimumGroupRewardShare=25 * 10**6, + expectGroupRewardPool=ZERO_ADDRESS, ) def test_validate_license_config_invalid_commercial_rev_share_negative(self): @@ -144,8 +145,8 @@ def test_validate_license_config_invalid_minting_fee_negative(self): with pytest.raises(ValueError, match="The minting_fee must be greater than 0."): LicensingConfigData.validate_license_config(input_config) - def test_validate_license_config_invalid_address(self): - """Test validate_license_config raises error for invalid address.""" + def test_validate_license_config_invalid_licensing_hook_address(self): + """Test validate_license_config raises error for invalid licensing_hook address.""" input_config: LicensingConfig = { "is_set": False, "minting_fee": 0, @@ -160,6 +161,22 @@ def test_validate_license_config_invalid_address(self): with pytest.raises(ValueError, match="Invalid address: invalid_address."): LicensingConfigData.validate_license_config(input_config) + def test_validate_license_config_invalid_expect_group_reward_pool_address(self): + """Test validate_license_config raises error for invalid expect_group_reward_pool address.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": "invalid_address", + } + + with pytest.raises(ValueError, match="Invalid address: invalid_address."): + LicensingConfigData.validate_license_config(input_config) + class TestLicensingConfigFromTuple: def test_licensing_config_from_tuple(self): From 2ed785fd3e6efff5d455623aa588c07300e3f76c Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Sep 2025 14:52:36 +0800 Subject: [PATCH 3/8] feat: enhance LicensingConfigData validation with module registry checks and add unit tests for licensing hook registration --- .../utils/licensing_config_data.py | 14 +- .../unit/utils/test_licensing_config_data.py | 159 +++++++++++++++--- 2 files changed, 152 insertions(+), 21 deletions(-) diff --git a/src/story_protocol_python_sdk/utils/licensing_config_data.py b/src/story_protocol_python_sdk/utils/licensing_config_data.py index 0517ebb..67007e9 100644 --- a/src/story_protocol_python_sdk/utils/licensing_config_data.py +++ b/src/story_protocol_python_sdk/utils/licensing_config_data.py @@ -3,6 +3,9 @@ from ens.ens import Address, HexStr +from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ( + ModuleRegistryClient, +) from story_protocol_python_sdk.types.common import RevShareType from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH from story_protocol_python_sdk.utils.validation import ( @@ -86,7 +89,9 @@ def from_tuple(cls, tuple_data: tuple) -> LicensingConfig: @classmethod def validate_license_config( - cls, licensing_config: LicensingConfig | None = None + cls, + module_registry_client: ModuleRegistryClient, + licensing_config: LicensingConfig | None = None, ) -> ValidatedLicensingConfig: """ Validates and normalizes licensing configuration. @@ -115,7 +120,12 @@ def validate_license_config( ) if licensing_config["minting_fee"] < 0: - raise ValueError("The minting_fee must be greater than 0.") + raise ValueError("The minting fee must be greater than 0.") + if licensing_config["licensing_hook"] != ZERO_ADDRESS: + if not module_registry_client.isRegistered( + licensing_config["licensing_hook"] + ): + raise ValueError("The licensing hook is not registered.") return ValidatedLicensingConfig( isSet=licensing_config["is_set"], diff --git a/tests/unit/utils/test_licensing_config_data.py b/tests/unit/utils/test_licensing_config_data.py index aff1196..46ea42d 100644 --- a/tests/unit/utils/test_licensing_config_data.py +++ b/tests/unit/utils/test_licensing_config_data.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock, Mock + import pytest from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH @@ -8,10 +10,23 @@ ) +@pytest.fixture +def mock_module_registry_client(): + """Mock module registry client fixture with configurable registration status.""" + + def _mock_module_registry_client(is_registered=True): + return Mock(isRegistered=MagicMock(return_value=is_registered)) + + return _mock_module_registry_client + + class TestValidateLicenseConfig: - def test_validate_license_config_default_values(self): + def test_validate_license_config_default_values(self, mock_module_registry_client): """Test validate_license_config with no input returns default values.""" - result = LicensingConfigData.validate_license_config() + result = LicensingConfigData.validate_license_config( + mock_module_registry_client() + ) + assert result == ValidatedLicensingConfig( isSet=False, mintingFee=0, @@ -23,7 +38,7 @@ def test_validate_license_config_default_values(self): expectGroupRewardPool=ZERO_ADDRESS, ) - def test_validate_license_config_valid_input(self): + def test_validate_license_config_valid_input(self, mock_module_registry_client): """Test validate_license_config with valid input.""" input_config: LicensingConfig = { "is_set": True, @@ -36,7 +51,9 @@ def test_validate_license_config_valid_input(self): "expect_group_reward_pool": ZERO_ADDRESS, } - result = LicensingConfigData.validate_license_config(input_config) + result = LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) assert result == ValidatedLicensingConfig( isSet=True, @@ -49,7 +66,9 @@ def test_validate_license_config_valid_input(self): expectGroupRewardPool=ZERO_ADDRESS, ) - def test_validate_license_config_invalid_commercial_rev_share_negative(self): + def test_validate_license_config_invalid_commercial_rev_share_negative( + self, mock_module_registry_client + ): """Test validate_license_config raises error for negative commercial_rev_share.""" input_config: LicensingConfig = { "is_set": False, @@ -66,9 +85,13 @@ def test_validate_license_config_invalid_commercial_rev_share_negative(self): ValueError, match="The commercial_rev_share must be between 0 and 100.", ): - LicensingConfigData.validate_license_config(input_config) + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) - def test_validate_license_config_invalid_commercial_rev_share_too_high(self): + def test_validate_license_config_invalid_commercial_rev_share_too_high( + self, mock_module_registry_client + ): """Test validate_license_config raises error for commercial_rev_share > 100.""" input_config: LicensingConfig = { "is_set": False, @@ -85,10 +108,12 @@ def test_validate_license_config_invalid_commercial_rev_share_too_high(self): ValueError, match="The commercial_rev_share must be between 0 and 100.", ): - LicensingConfigData.validate_license_config(input_config) + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) def test_validate_license_config_invalid_expect_minimum_group_reward_share_negative( - self, + self, mock_module_registry_client ): """Test validate_license_config raises error for negative expect_minimum_group_reward_share.""" input_config: LicensingConfig = { @@ -106,10 +131,12 @@ def test_validate_license_config_invalid_expect_minimum_group_reward_share_negat ValueError, match="The expect_minimum_group_reward_share must be between 0 and 100.", ): - LicensingConfigData.validate_license_config(input_config) + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) def test_validate_license_config_invalid_expect_minimum_group_reward_share_too_high( - self, + self, mock_module_registry_client ): """Test validate_license_config raises error for expect_minimum_group_reward_share > 100.""" input_config: LicensingConfig = { @@ -127,9 +154,13 @@ def test_validate_license_config_invalid_expect_minimum_group_reward_share_too_h ValueError, match="The expect_minimum_group_reward_share must be between 0 and 100.", ): - LicensingConfigData.validate_license_config(input_config) + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) - def test_validate_license_config_invalid_minting_fee_negative(self): + def test_validate_license_config_invalid_minting_fee_negative( + self, mock_module_registry_client + ): """Test validate_license_config raises error for negative minting_fee.""" input_config: LicensingConfig = { "is_set": False, @@ -142,10 +173,14 @@ def test_validate_license_config_invalid_minting_fee_negative(self): "expect_group_reward_pool": ZERO_ADDRESS, } - with pytest.raises(ValueError, match="The minting_fee must be greater than 0."): - LicensingConfigData.validate_license_config(input_config) + with pytest.raises(ValueError, match="The minting fee must be greater than 0."): + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) - def test_validate_license_config_invalid_licensing_hook_address(self): + def test_validate_license_config_invalid_licensing_hook_address( + self, mock_module_registry_client + ): """Test validate_license_config raises error for invalid licensing_hook address.""" input_config: LicensingConfig = { "is_set": False, @@ -159,9 +194,13 @@ def test_validate_license_config_invalid_licensing_hook_address(self): } with pytest.raises(ValueError, match="Invalid address: invalid_address."): - LicensingConfigData.validate_license_config(input_config) + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) - def test_validate_license_config_invalid_expect_group_reward_pool_address(self): + def test_validate_license_config_invalid_expect_group_reward_pool_address( + self, mock_module_registry_client + ): """Test validate_license_config raises error for invalid expect_group_reward_pool address.""" input_config: LicensingConfig = { "is_set": False, @@ -175,7 +214,89 @@ def test_validate_license_config_invalid_expect_group_reward_pool_address(self): } with pytest.raises(ValueError, match="Invalid address: invalid_address."): - LicensingConfigData.validate_license_config(input_config) + LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) + + def test_validate_license_config_unregistered_licensing_hook( + self, mock_module_registry_client + ): + """Test validate_license_config raises error for unregistered licensing hook.""" + + mock_client = mock_module_registry_client(is_registered=False) + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": "0x1234567890123456789012345678901234567890", + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + with pytest.raises(ValueError, match="The licensing hook is not registered."): + LicensingConfigData.validate_license_config(mock_client, input_config) + + def test_validate_license_config_registered_licensing_hook( + self, mock_module_registry_client + ): + """Test validate_license_config succeeds for registered licensing hook.""" + + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": "0x1234567890123456789012345678901234567890", + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + result = LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) + + assert result == ValidatedLicensingConfig( + isSet=False, + mintingFee=0, + licensingHook="0x1234567890123456789012345678901234567890", + hookData=ZERO_HASH, + commercialRevShare=0, + disabled=False, + expectMinimumGroupRewardShare=0, + expectGroupRewardPool=ZERO_ADDRESS, + ) + + def test_validate_license_config_zero_address_licensing_hook_skips_registration_check( + self, mock_module_registry_client + ): + """Test validate_license_config skips registration check for ZERO_ADDRESS licensing hook.""" + input_config: LicensingConfig = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + result = LicensingConfigData.validate_license_config( + mock_module_registry_client(), input_config + ) + assert result == ValidatedLicensingConfig( + isSet=False, + mintingFee=0, + licensingHook=ZERO_ADDRESS, + hookData=ZERO_HASH, + commercialRevShare=0, + disabled=False, + expectMinimumGroupRewardShare=0, + expectGroupRewardPool=ZERO_ADDRESS, + ) class TestLicensingConfigFromTuple: From 62e671034e3bae55526b22c07c88a9eab46a2f24 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Sep 2025 16:07:01 +0800 Subject: [PATCH 4/8] feat: add getLicensingConfig method to LicenseRegistryClient and update License class to utilize LicensingConfig for improved licensing configuration management --- src/story_protocol_python_sdk/__init__.py | 2 + .../LicenseRegistry/LicenseRegistry_client.py | 5 + .../resources/License.py | 118 ++++++++---------- .../scripts/config.json | 7 +- 4 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 7222ed6..335037f 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -28,6 +28,7 @@ ) from .utils.derivative_data import DerivativeDataInput from .utils.ip_metadata import IPMetadataInput +from .utils.licensing_config_data import LicensingConfig __all__ = [ "StoryClient", @@ -45,6 +46,7 @@ "ClaimRewardsResponse", "ClaimReward", "CollectRoyaltiesResponse", + "LicensingConfig", "RegisterPILTermsAndAttachResponse", # Constants "ZERO_ADDRESS", diff --git a/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py b/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py index 12e9eb7..d8243fe 100644 --- a/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py +++ b/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py @@ -39,6 +39,11 @@ def __init__(self, web3: Web3): def exists(self, licenseTemplate, licenseTermsId): return self.contract.functions.exists(licenseTemplate, licenseTermsId).call() + def getLicensingConfig(self, ipId, licenseTemplate, licenseTermsId): + return self.contract.functions.getLicensingConfig( + ipId, licenseTemplate, licenseTermsId + ).call() + def getRoyaltyPercent(self, ipId, licenseTemplate, licenseTermsId): return self.contract.functions.getRoyaltyPercent( ipId, licenseTemplate, licenseTermsId diff --git a/src/story_protocol_python_sdk/resources/License.py b/src/story_protocol_python_sdk/resources/License.py index 0f553ac..5fe63bf 100644 --- a/src/story_protocol_python_sdk/resources/License.py +++ b/src/story_protocol_python_sdk/resources/License.py @@ -1,6 +1,4 @@ -# src/story_protcol_python_sdk/resources/License.py - -from ens.ens import HexStr +from ens.ens import Address, HexStr from web3 import Web3 from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( @@ -21,8 +19,15 @@ from story_protocol_python_sdk.types.common import RevShareType from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS from story_protocol_python_sdk.utils.license_terms import LicenseTerms +from story_protocol_python_sdk.utils.licensing_config_data import ( + LicensingConfig, + LicensingConfigData, +) from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction -from story_protocol_python_sdk.utils.validation import get_revenue_share +from story_protocol_python_sdk.utils.validation import ( + get_revenue_share, + validate_address, +) class License: @@ -522,7 +527,7 @@ def set_licensing_config( self, ip_id: str, license_terms_id: int, - licensing_config: dict, + licensing_config: LicensingConfig, license_template: str | None = None, tx_options: dict | None = None, ) -> dict: @@ -531,86 +536,36 @@ def set_licensing_config( :param ip_id str: The address of the IP for which the configuration is being set. :param license_terms_id int: The ID of the license terms within the license template. - :param licensing_config dict: The licensing configuration for the license. - :param isSet bool: Whether the configuration is set or not. - :param mintingFee int: The minting fee to be paid when minting license tokens. - :param hookData str: The data to be used by the licensing hook. - :param licensingHook str: The hook contract address for the licensing module, or address(0) if none. - :param commercialRevShare int: The commercial revenue share percentage. - :param disabled bool: Whether the license is disabled or not. - :param expectMinimumGroupRewardShare int: The minimum percentage of the group's reward share (0-100%, as 100 * 10^6). - :param expectGroupRewardPool str: The address of the expected group reward pool. + :param licensing_config `LicensingConfig`: The licensing configuration for the license. :param license_template str: [Optional] The address of the license template used. If not specified, config applies to all licenses. :param tx_options dict: [Optional] Transaction options. :return dict: A dictionary containing the transaction hash and success status. """ try: - # Input validation - required_params = { - "isSet", - "mintingFee", - "hookData", - "licensingHook", - "commercialRevShare", - "disabled", - "expectMinimumGroupRewardShare", - "expectGroupRewardPool", - } - - # Check for missing parameters - missing_params = required_params - set(licensing_config.keys()) - if missing_params: - raise ValueError( - f"Missing required licensing_config parameters: {', '.join(missing_params)}. " - f"All parameters must be explicitly provided." - ) - - licensing_config["commercialRevShare"] = ( - self.license_terms_util.get_revenue_share( - licensing_config["commercialRevShare"] - ) + validated_licensing_config = LicensingConfigData.validate_license_config( + self.module_registry_client, licensing_config ) - - if licensing_config["mintingFee"] < 0: - raise ValueError("The minting fee must be greater than 0.") - - if not license_template: - license_template = ZERO_ADDRESS + if license_template is None: + license_template = self.license_template_client.contract.address + else: + validate_address(license_template) if ( license_template == ZERO_ADDRESS - and licensing_config["commercialRevShare"] != 0 + and validated_licensing_config["commercialRevShare"] != 0 ): raise ValueError( "The license template cannot be zero address if commercial revenue share is not zero." ) - # Convert addresses to checksum format - ip_id = self.web3.to_checksum_address(ip_id) - if license_template: - license_template = self.web3.to_checksum_address(license_template) - licensing_config["licensingHook"] = self.web3.to_checksum_address( - licensing_config["licensingHook"] - ) - licensing_config["expectGroupRewardPool"] = self.web3.to_checksum_address( - licensing_config["expectGroupRewardPool"] - ) - # Check if IP is registered - if not self.ip_asset_registry_client.isRegistered(ip_id): + if not self.ip_asset_registry_client.isRegistered(validate_address(ip_id)): raise ValueError(f"The licensor IP with id {ip_id} is not registered.") # Check if license terms exist if not self.license_template_client.exists(license_terms_id): raise ValueError(f"License terms id {license_terms_id} does not exist.") - # Check if licensing hook is registered if provided - if licensing_config["licensingHook"] != ZERO_ADDRESS: - if not self.module_registry_client.isRegistered( - licensing_config["licensingHook"] - ): - raise ValueError("The licensing hook is not registered.") - if license_template == ZERO_ADDRESS and license_terms_id != 0: raise ValueError( "The license template is zero address but license terms id is not zero." @@ -623,7 +578,7 @@ def set_licensing_config( ip_id, license_template, license_terms_id, - licensing_config, + validated_licensing_config, tx_options=tx_options, ) @@ -634,3 +589,36 @@ def set_licensing_config( except Exception as e: raise ValueError(f"Failed to set licensing config: {str(e)}") + + def get_licensing_config( + self, + ip_id: Address, + license_terms_id: int, + license_template: Address | None = None, + ) -> LicensingConfig: + """ + Gets the licensing configuration for a specific license terms of an IP. + + :param ip_id Address: The address of the IP for which the configuration is being retrieved. + :param license_terms_id int: The ID of the license terms within the license template. + :param license_template Address: [Optional] The address of the license template. + Defaults to visit https://docs.story.foundation/docs/programmable-ip-license for more information if not provided. + :return LicensingConfig: A dictionary containing the licensing configuration. + """ + try: + if not self.web3.is_address(ip_id): + raise ValueError(f"Invalid IP id address: {ip_id}") + + if license_template is None: + license_template = self.license_template_client.contract.address + else: + validate_address(license_template) + + licensing_config = self.license_registry_client.getLicensingConfig( + ip_id, license_template, license_terms_id + ) + + return LicensingConfigData.from_tuple(licensing_config) + + except Exception as e: + raise ValueError(f"Failed to get licensing config: {str(e)}") diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index bf797ad..ff624de 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -190,7 +190,12 @@ { "contract_name": "LicenseRegistry", "contract_address": "0x529a750E02d8E2f15649c13D69a465286a780e24", - "functions": ["exists", "hasIpAttachedLicenseTerms", "getRoyaltyPercent"] + "functions": [ + "exists", + "hasIpAttachedLicenseTerms", + "getRoyaltyPercent", + "getLicensingConfig" + ] }, { "contract_name": "RoyaltyPolicyLRP", From eae219d66ec42d65c033e609c1f3c8e0ae2e4d62 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Sep 2025 17:18:12 +0800 Subject: [PATCH 5/8] refactor: reorganize licensing configuration tests into TestLicensingConfig class and update assertions for improved clarity and structure --- tests/integration/test_integration_license.py | 113 ++++++++---------- 1 file changed, 48 insertions(+), 65 deletions(-) diff --git a/tests/integration/test_integration_license.py b/tests/integration/test_integration_license.py index f0cdc2a..881ffe0 100644 --- a/tests/integration/test_integration_license.py +++ b/tests/integration/test_integration_license.py @@ -1,8 +1,8 @@ -# tests/integration/test_integration_license.py - import pytest from story_protocol_python_sdk.story_client import StoryClient +from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS +from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfig from .setup_for_integration import ( PIL_LICENSE_TEMPLATE, @@ -217,38 +217,6 @@ def test_predict_minting_license_fee( assert isinstance(response["amount"], int), "'amount' is not an integer." -def test_set_licensing_config( - story_client: StoryClient, ip_id, register_commercial_remix_pil -): - licensing_config = { - "mintingFee": 1, - "isSet": True, - "licensingHook": "0x0000000000000000000000000000000000000000", - "hookData": "0xFcd3243590d29B131a26B1554B0b21a5B43e622e", - "commercialRevShare": 0, - "disabled": False, - "expectMinimumGroupRewardShare": 1, - "expectGroupRewardPool": "0x0000000000000000000000000000000000000000", - } - - response = story_client.License.set_licensing_config( - ip_id=ip_id, - license_terms_id=register_commercial_remix_pil, - licensing_config=licensing_config, - license_template=PIL_LICENSE_TEMPLATE, - ) - - assert ( - response is not None - ), "Response is None, indicating the contract interaction failed." - assert "tx_hash" in response, "Response does not contain 'tx_hash'" - assert response["tx_hash"] is not None, "'tx_hash' is None" - assert isinstance(response["tx_hash"], str), "'tx_hash' is not a string" - assert len(response["tx_hash"]) > 0, "'tx_hash' is empty" - assert "success" in response, "Response does not contain 'success'" - assert response["success"] is True, "'success' is not True" - - def test_register_pil_terms_with_no_minting_fee(story_client: StoryClient): """Test registering PIL terms with no minting fee.""" response = story_client.License.register_pil_terms( @@ -334,37 +302,6 @@ def test_multi_token_minting(story_client: StoryClient, ip_id, setup_license_ter assert len(response["license_token_ids"]) > 0 -def test_set_licensing_config_with_hooks( - story_client: StoryClient, ip_id, register_commercial_remix_pil -): - """Test setting licensing configuration with hooks enabled.""" - licensing_config = { - "mintingFee": 100, - "isSet": True, - "licensingHook": "0x0000000000000000000000000000000000000000", - "hookData": "0x1234567890", # Different hook data - "commercialRevShare": 100, # 50% revenue share - "disabled": False, - "expectMinimumGroupRewardShare": 10, # 10% minimum group reward - "expectGroupRewardPool": "0x0000000000000000000000000000000000000000", - } - - response = story_client.License.set_licensing_config( - ip_id=ip_id, - license_terms_id=register_commercial_remix_pil, - licensing_config=licensing_config, - license_template=PIL_LICENSE_TEMPLATE, - ) - - assert response is not None - assert "tx_hash" in response - assert response["tx_hash"] is not None - assert isinstance(response["tx_hash"], str) - assert len(response["tx_hash"]) > 0 - assert "success" in response - assert response["success"] is True - - def test_predict_minting_fee_with_multiple_tokens( story_client: StoryClient, ip_id, setup_license_terms ): @@ -384,3 +321,49 @@ def test_predict_minting_fee_with_multiple_tokens( assert response["amount"] is not None assert isinstance(response["amount"], int) assert response["amount"] > 0 # Amount should be positive for multiple tokens + + +class TestLicensingConfig: + def test_set_licensing_config( + self, story_client: StoryClient, ip_id, register_commercial_remix_pil + ): + """Test setting licensing configuration.""" + + response = story_client.License.set_licensing_config( + ip_id=ip_id, + license_terms_id=register_commercial_remix_pil, + licensing_config=LicensingConfig( + minting_fee=100, + is_set=True, + licensing_hook=ZERO_ADDRESS, + hook_data=b"", + commercial_rev_share=100, + disabled=False, + expect_minimum_group_reward_share=10, + expect_group_reward_pool=ZERO_ADDRESS, + ), + license_template=PIL_LICENSE_TEMPLATE, + ) + + assert response["tx_hash"] is not None + assert response["success"] is True + + def test_get_licensing_config( + self, story_client: StoryClient, ip_id, register_commercial_remix_pil + ): + """Test getting licensing configuration.""" + response = story_client.License.get_licensing_config( + ip_id=ip_id, + license_terms_id=register_commercial_remix_pil, + license_template=PIL_LICENSE_TEMPLATE, + ) + assert response == LicensingConfig( + is_set=True, + minting_fee=100, + licensing_hook=ZERO_ADDRESS, + hook_data=b"", + disabled=False, + expect_minimum_group_reward_share=10 * 10**6, + expect_group_reward_pool=ZERO_ADDRESS, + commercial_rev_share=100 * 10**6, + ) From 7d78c3ecc752f62271cbeb7154a418fce591c703 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Sep 2025 14:48:13 +0800 Subject: [PATCH 6/8] refactor: update License class methods to return consistent dict types and improve logging checks; enhance test structure by utilizing existing fixtures and updating assertions --- .../resources/License.py | 20 +- tests/unit/conftest.py | 1 + tests/unit/resources/test_license.py | 934 ++++++++++-------- 3 files changed, 534 insertions(+), 421 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/License.py b/src/story_protocol_python_sdk/resources/License.py index 5fe63bf..d2d926b 100644 --- a/src/story_protocol_python_sdk/resources/License.py +++ b/src/story_protocol_python_sdk/resources/License.py @@ -188,7 +188,7 @@ def register_commercial_use_pil( currency: str, royalty_policy: str | None = None, tx_options: dict | None = None, - ) -> dict | None: + ) -> dict: """ Convenient function to register a PIL commercial use license to the registry. @@ -220,8 +220,8 @@ def register_commercial_use_pil( tx_options=tx_options, ) - if not response["tx_receipt"].logs: - return None + if not response["tx_receipt"]["logs"]: + return {"tx_hash": response["tx_hash"]} target_logs = self._parse_tx_license_terms_registered_event( response["tx_receipt"] @@ -238,7 +238,7 @@ def register_commercial_remix_pil( commercial_rev_share: int, royalty_policy: str, tx_options: dict | None = None, - ) -> dict | None: + ) -> dict: """ Convenient function to register a PIL commercial remix license to the registry. @@ -272,8 +272,8 @@ def register_commercial_remix_pil( tx_options=tx_options, ) - if not response["tx_receipt"].logs: - return None + if not response["tx_receipt"]["logs"]: + return {"tx_hash": response["tx_hash"]} target_logs = self._parse_tx_license_terms_registered_event( response["tx_receipt"] @@ -380,12 +380,8 @@ def mint_license_tokens( :return dict: A dictionary with the transaction hash and the license token IDs. """ try: - if not self.web3.is_address(license_template): - raise ValueError(f'Address "{license_template}" is invalid.') - - if not self.web3.is_address(receiver): - raise ValueError(f'Address "{receiver}" is invalid.') - + validate_address(license_template) + validate_address(receiver) is_registered = self.ip_asset_registry_client.isRegistered(licensor_ip_id) if not is_registered: raise ValueError( diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 484fbfc..e5389e8 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -44,6 +44,7 @@ def create_mock_contract(*args, **kwargs): mock_web3.eth.wait_for_transaction_receipt = MagicMock( return_value={"status": 1, "logs": []} ) + mock_web3.to_wei = MagicMock(return_value=1) return mock_web3 diff --git a/tests/unit/resources/test_license.py b/tests/unit/resources/test_license.py index 8934200..f1d560c 100644 --- a/tests/unit/resources/test_license.py +++ b/tests/unit/resources/test_license.py @@ -1,104 +1,41 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from _pytest.fixtures import fixture -from eth_utils import is_address, to_checksum_address from web3 import Web3 from story_protocol_python_sdk.resources.License import License -from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID +from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS +from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfig +from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH +from tests.unit.resources.test_ip_account import ZERO_HASH -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -VALID_ADDRESS = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" -TX_HASH = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997" - -class MockWeb3: - """Mock Web3 instance with required functionality.""" - - def __init__(self): - self.eth = MagicMock() - - @staticmethod - def to_checksum_address(address): - if not is_address(address): - raise ValueError(f'Address "{address}" is invalid') - return to_checksum_address(address) - - @staticmethod - def to_bytes(hexstr=None, **kwargs): - return Web3.to_bytes(hexstr=hexstr, **kwargs) - - @staticmethod - def to_wei(number, unit): - return Web3.to_wei(number, unit) - - @staticmethod - def is_address(address): - return is_address(address) - - @staticmethod - def keccak(text=None, hexstr=None, primitive=None): - return Web3.keccak(text=text, hexstr=hexstr) - - def is_connected(self): - return True - - -class MockTxHash: - """Mock transaction hash that returns hash without '0x' prefix.""" - - def hex(self): - return TX_HASH[2:] - - -@pytest.fixture -def mock_web3(): - return MockWeb3() - - -@pytest.fixture -def mock_account(): - account = MagicMock() - account.address = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" - return account - - -@pytest.fixture -def mock_signed_txn(): - signed_txn = MagicMock() - signed_txn.rawTransaction = b"signed_tx" - return signed_txn - - -@pytest.fixture -def license_client(mock_web3, mock_account): - chain_id = 1315 - return License(mock_web3, mock_account, chain_id) +@fixture +def license(mock_web3, mock_account): + return License(web3=mock_web3, account=mock_account, chain_id=CHAIN_ID) class TestPILTermsRegistration: """Tests for PIL (Programmable IP License) terms registration.""" - def test_register_pil_terms_license_terms_id_registered( - self, license_client: License - ): + def test_register_pil_terms_license_terms_id_registered(self, license: License): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=1 + license.license_template_client, "getLicenseTermsId", return_value=1 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyToken", return_value=True, ): - response = license_client.register_pil_terms( + response = license.register_pil_terms( default_minting_fee=1513, - currency=VALID_ADDRESS, - royalty_policy=VALID_ADDRESS, + currency=ADDRESS, + royalty_policy=ADDRESS, transferable=False, expiration=0, commercial_use=True, @@ -117,41 +54,31 @@ def test_register_pil_terms_license_terms_id_registered( assert response["license_terms_id"] == 1 assert "tx_hash" not in response - def test_register_pil_terms_success( - self, license_client, mock_signed_txn, mock_account - ): + def test_register_pil_terms_success(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyToken", return_value=True, ), patch.object( - license_client.license_template_client, + license.license_template_client, "build_registerLicenseTerms_transaction", return_value={ - "from": mock_account.address, + "from": ADDRESS, "nonce": 1, "gas": 2000000, "gasPrice": Web3.to_wei("100", "gwei"), }, - ), patch.object( - mock_account, "sign_transaction", return_value=mock_signed_txn - ), patch.object( - license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() - ), patch.object( - license_client.web3.eth, - "wait_for_transaction_receipt", - return_value=MagicMock(), ): - response = license_client.register_pil_terms( + response = license.register_pil_terms( transferable=False, - royalty_policy=VALID_ADDRESS, + royalty_policy=ADDRESS, default_minting_fee=1513, expiration=0, commercial_use=True, @@ -165,25 +92,23 @@ def test_register_pil_terms_success( derivatives_approval=False, derivatives_reciprocal=False, derivative_rev_ceiling=0, - currency=VALID_ADDRESS, + currency=ADDRESS, uri="", ) assert "tx_hash" in response - assert response["tx_hash"] == TX_HASH[2:] + assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) - def test_register_pil_terms_commercial_rev_share_error_more_than_100( - self, license_client - ): + def test_register_pil_terms_commercial_rev_share_error_more_than_100(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyToken", return_value=True, ): @@ -191,9 +116,9 @@ def test_register_pil_terms_commercial_rev_share_error_more_than_100( with pytest.raises( ValueError, match="commercial_rev_share should be between 0 and 100." ): - license_client.register_pil_terms( + license.register_pil_terms( transferable=False, - royalty_policy=VALID_ADDRESS, + royalty_policy=ADDRESS, default_minting_fee=1, expiration=0, commercial_use=True, @@ -207,21 +132,19 @@ def test_register_pil_terms_commercial_rev_share_error_more_than_100( derivatives_approval=False, derivatives_reciprocal=False, derivative_rev_ceiling=0, - currency=VALID_ADDRESS, + currency=ADDRESS, uri="", ) - def test_register_pil_terms_commercial_rev_share_error_less_than_0( - self, license_client - ): + def test_register_pil_terms_commercial_rev_share_error_less_than_0(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyToken", return_value=True, ): @@ -229,9 +152,9 @@ def test_register_pil_terms_commercial_rev_share_error_less_than_0( with pytest.raises( ValueError, match="commercial_rev_share should be between 0 and 100." ): - license_client.register_pil_terms( + license.register_pil_terms( transferable=False, - royalty_policy=VALID_ADDRESS, + royalty_policy=ADDRESS, default_minting_fee=1, expiration=0, commercial_use=True, @@ -245,7 +168,7 @@ def test_register_pil_terms_commercial_rev_share_error_less_than_0( derivatives_approval=False, derivatives_reciprocal=False, derivative_rev_ceiling=0, - currency=VALID_ADDRESS, + currency=ADDRESS, uri="", ) @@ -254,137 +177,110 @@ class TestNonComSocialRemixingPIL: """Tests for non-commercial social remixing PIL functionality.""" def test_register_non_com_social_remixing_pil_license_terms_id_registered( - self, license_client + self, license ): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=1 + license.license_template_client, "getLicenseTermsId", return_value=1 ): - response = license_client.register_non_com_social_remixing_pil() + response = license.register_non_com_social_remixing_pil() assert response["license_terms_id"] == 1 assert "tx_hash" not in response - def test_register_non_com_social_remixing_pil_success( - self, license_client, mock_signed_txn, mock_account - ): + def test_register_non_com_social_remixing_pil_success(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_template_client, + license.license_template_client, "build_registerLicenseTerms_transaction", return_value={ - "from": mock_account.address, + "from": ADDRESS, "nonce": 1, "gas": 2000000, "gasPrice": Web3.to_wei("100", "gwei"), }, ), patch.object( - mock_account, "sign_transaction", return_value=mock_signed_txn - ), patch.object( - license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() - ), patch.object( - license_client.web3.eth, "get_transaction_count", return_value=1 - ), patch.object( - license_client.web3.eth, - "wait_for_transaction_receipt", - return_value=MagicMock(), - ), patch.object( - license_client, "_parse_tx_license_terms_registered_event", return_value=1 + license, "_parse_tx_license_terms_registered_event", return_value=1 ): - response = license_client.register_non_com_social_remixing_pil() + response = license.register_non_com_social_remixing_pil() assert "tx_hash" in response - assert response["tx_hash"] == TX_HASH[2:] + assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) assert "license_terms_id" in response assert response["license_terms_id"] == 1 - def test_register_non_com_social_remixing_pil_error(self, license_client): + def test_register_non_com_social_remixing_pil_error(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_template_client, + license.license_template_client, "build_registerLicenseTerms_transaction", side_effect=Exception("request fail."), ): with pytest.raises(Exception, match="request fail."): - license_client.register_non_com_social_remixing_pil() + license.register_non_com_social_remixing_pil() class TestCommercialUsePIL: """Tests for commercial use PIL functionality.""" - def test_register_commercial_use_pil_license_terms_id_registered( - self, license_client - ): + def test_register_commercial_use_pil_license_terms_id_registered(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=1 + license.license_template_client, "getLicenseTermsId", return_value=1 ): - response = license_client.register_commercial_use_pil( + response = license.register_commercial_use_pil( default_minting_fee=1, currency=ZERO_ADDRESS ) assert response["license_terms_id"] == 1 assert "tx_hash" not in response - def test_register_commercial_use_pil_success( - self, license_client, mock_signed_txn, mock_account - ): + def test_register_commercial_use_pil_success_without_logs(self, license: License): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_template_client, + license.license_template_client, "build_registerLicenseTerms_transaction", return_value={ - "from": mock_account.address, + "from": ADDRESS, "nonce": 1, "gas": 2000000, "gasPrice": Web3.to_wei("100", "gwei"), }, ), patch.object( - mock_account, "sign_transaction", return_value=mock_signed_txn - ), patch.object( - license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() - ), patch.object( - license_client.web3.eth, "get_transaction_count", return_value=1 - ), patch.object( - license_client.web3.eth, - "wait_for_transaction_receipt", - return_value=MagicMock(), - ), patch.object( - license_client, "_parse_tx_license_terms_registered_event", return_value=1 + license, "_parse_tx_license_terms_registered_event", return_value=1 ): - response = license_client.register_commercial_use_pil( + response = license.register_commercial_use_pil( default_minting_fee=1, currency=ZERO_ADDRESS ) + assert response is not None assert "tx_hash" in response - assert response["tx_hash"] == TX_HASH[2:] + assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) - assert "license_terms_id" in response - assert response["license_terms_id"] == 1 - def test_register_commercial_use_pil_error(self, license_client): + def test_register_commercial_use_pil_error(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_template_client, + license.license_template_client, "build_registerLicenseTerms_transaction", side_effect=Exception("request fail."), ): with pytest.raises(Exception, match="request fail."): - license_client.register_commercial_use_pil( + license.register_commercial_use_pil( default_minting_fee=1, currency=ZERO_ADDRESS ) @@ -392,17 +288,15 @@ def test_register_commercial_use_pil_error(self, license_client): class TestCommercialRemixPIL: """Tests for commercial remix PIL functionality.""" - def test_register_commercial_remix_pil_license_terms_id_registered( - self, license_client - ): + def test_register_commercial_remix_pil_license_terms_id_registered(self, license): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=1 + license.license_template_client, "getLicenseTermsId", return_value=1 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ): - response = license_client.register_commercial_remix_pil( + response = license.register_commercial_remix_pil( default_minting_fee=1, commercial_rev_share=100, currency=ZERO_ADDRESS, @@ -411,86 +305,72 @@ def test_register_commercial_remix_pil_license_terms_id_registered( assert response["license_terms_id"] == 1 assert "tx_hash" not in response - def test_register_commercial_remix_pil_success( - self, license_client, mock_signed_txn, mock_account - ): + def test_register_commercial_remix_pil_success(self, license: License): with patch.object( - license_client.license_template_client, "getLicenseTermsId", return_value=0 + license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( - license_client.license_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyPolicy", return_value=True, ), patch.object( - license_client.license_template_client, + license.license_template_client, "build_registerLicenseTerms_transaction", return_value={ - "from": mock_account.address, + "from": ADDRESS, "nonce": 1, "gas": 2000000, "gasPrice": Web3.to_wei("100", "gwei"), }, ), patch.object( - mock_account, "sign_transaction", return_value=mock_signed_txn - ), patch.object( - license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() - ), patch.object( - license_client.web3.eth, "get_transaction_count", return_value=1 - ), patch.object( - license_client.web3.eth, - "wait_for_transaction_receipt", - return_value=MagicMock(), - ), patch.object( - license_client, "_parse_tx_license_terms_registered_event", return_value=1 + license, "_parse_tx_license_terms_registered_event", return_value=1 ): - response = license_client.register_commercial_remix_pil( + response = license.register_commercial_remix_pil( default_minting_fee=1, commercial_rev_share=100, currency=ZERO_ADDRESS, royalty_policy=ZERO_ADDRESS, ) + assert response is not None assert "tx_hash" in response - assert response["tx_hash"] == TX_HASH[2:] + assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) - assert response["license_terms_id"] == 1 class TestLicenseAttachment: """Tests for license attachment functionality.""" - def test_attach_license_terms_ip_not_registered(self, license_client): + def test_attach_license_terms_ip_not_registered(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=False + license.ip_asset_registry_client, "isRegistered", return_value=False ): with pytest.raises( ValueError, match=f"The IP with id {ZERO_ADDRESS} is not registered." ): - license_client.attach_license_terms( + license.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, ) - def test_attach_license_terms_license_terms_not_exist(self, license_client): + def test_attach_license_terms_license_terms_not_exist(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True - ), patch.object( - license_client.license_registry_client, "exists", return_value=False - ): + license.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object(license.license_registry_client, "exists", return_value=False): with pytest.raises(ValueError, match="License terms id 1 do not exist."): - license_client.attach_license_terms( + license.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, ) - def test_attach_license_terms_already_attached(self, license_client): + def test_attach_license_terms_already_attached(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True + license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( - license_client.license_registry_client, "exists", return_value=True + license.license_registry_client, "exists", return_value=True ), patch.object( - license_client.license_registry_client, + license.license_registry_client, "hasIpAttachedLicenseTerms", return_value=True, ): @@ -498,67 +378,55 @@ def test_attach_license_terms_already_attached(self, license_client): ValueError, match=f"License terms id 1 is already attached to the IP with id {ZERO_ADDRESS}.", ): - license_client.attach_license_terms( + license.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, ) - def test_attach_license_terms_success( - self, license_client, mock_signed_txn, mock_account - ): + def test_attach_license_terms_success(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True + license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( - license_client.license_registry_client, "exists", return_value=True + license.license_registry_client, "exists", return_value=True ), patch.object( - license_client.license_registry_client, + license.license_registry_client, "hasIpAttachedLicenseTerms", return_value=False, ), patch.object( - license_client.licensing_module_client, + license.licensing_module_client, "build_attachLicenseTerms_transaction", return_value={ - "from": mock_account.address, + "from": ADDRESS, "nonce": 1, "gas": 2000000, "gasPrice": Web3.to_wei("100", "gwei"), }, ), patch.object( - mock_account, "sign_transaction", return_value=mock_signed_txn - ), patch.object( - license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() - ), patch.object( - license_client.web3.eth, "get_transaction_count", return_value=1 - ), patch.object( - license_client.web3.eth, - "wait_for_transaction_receipt", - return_value=MagicMock(), - ), patch.object( - license_client, "_parse_tx_license_terms_registered_event", return_value=1 + license, "_parse_tx_license_terms_registered_event", return_value=1 ): - response = license_client.attach_license_terms( + response = license.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1 ) assert "tx_hash" in response - assert response["tx_hash"] == TX_HASH[2:] + assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) class TestLicenseTokens: """Tests for license token minting functionality.""" - def test_mint_license_tokens_licensor_ip_not_registered(self, license_client): + def test_mint_license_tokens_licensor_ip_not_registered(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=False + license.ip_asset_registry_client, "isRegistered", return_value=False ): with pytest.raises( ValueError, match=f"The licensor IP with id {ZERO_ADDRESS} is not registered.", ): - license_client.mint_license_tokens( + license.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, @@ -566,14 +434,12 @@ def test_mint_license_tokens_licensor_ip_not_registered(self, license_client): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_license_terms_not_exist(self, license_client): + def test_mint_license_tokens_license_terms_not_exist(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True - ), patch.object( - license_client.license_template_client, "exists", return_value=False - ): + license.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object(license.license_template_client, "exists", return_value=False): with pytest.raises(ValueError, match="License terms id 1 do not exist."): - license_client.mint_license_tokens( + license.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, @@ -581,13 +447,13 @@ def test_mint_license_tokens_license_terms_not_exist(self, license_client): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_not_attached(self, license_client): + def test_mint_license_tokens_not_attached(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True + license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( - license_client.license_template_client, "exists", return_value=True + license.license_template_client, "exists", return_value=True ), patch.object( - license_client.license_registry_client, + license.license_registry_client, "hasIpAttachedLicenseTerms", return_value=False, ): @@ -595,7 +461,7 @@ def test_mint_license_tokens_not_attached(self, license_client): ValueError, match=f"License terms id 1 is not attached to the IP with id {ZERO_ADDRESS}.", ): - license_client.mint_license_tokens( + license.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, @@ -603,14 +469,12 @@ def test_mint_license_tokens_not_attached(self, license_client): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_invalid_template(self, license_client): + def test_mint_license_tokens_invalid_template(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True + license.ip_asset_registry_client, "isRegistered", return_value=True ): - with pytest.raises( - ValueError, match='Address "invalid address" is invalid' - ): - license_client.mint_license_tokens( + with pytest.raises(ValueError, match="Invalid address: invalid address"): + license.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template="invalid address", license_terms_id=1, @@ -618,14 +482,12 @@ def test_mint_license_tokens_invalid_template(self, license_client): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_invalid_receiver(self, license_client): + def test_mint_license_tokens_invalid_receiver(self, license: License): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True + license.ip_asset_registry_client, "isRegistered", return_value=True ): - with pytest.raises( - ValueError, match='Address "invalid address" is invalid' - ): - license_client.mint_license_tokens( + with pytest.raises(ValueError, match="Invalid address: invalid address"): + license.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, @@ -633,39 +495,29 @@ def test_mint_license_tokens_invalid_receiver(self, license_client): receiver="invalid address", ) - def test_mint_license_tokens_success( - self, license_client, mock_signed_txn, mock_account - ): + def test_mint_license_tokens_success(self, license): with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True + license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( - license_client.license_template_client, "exists", return_value=True + license.license_template_client, "exists", return_value=True ), patch.object( - license_client.license_registry_client, + license.license_registry_client, "hasIpAttachedLicenseTerms", return_value=True, ), patch.object( - license_client.licensing_module_client, + license.licensing_module_client, "build_mintLicenseTokens_transaction", return_value={ - "from": mock_account.address, + "from": ADDRESS, "nonce": 1, "gas": 2000000, "gasPrice": Web3.to_wei("100", "gwei"), }, ), patch.object( - mock_account, "sign_transaction", return_value=mock_signed_txn - ), patch.object( - license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() - ), patch.object( - license_client.web3.eth, - "wait_for_transaction_receipt", - return_value=MagicMock(), - ), patch.object( - license_client, "_parse_tx_license_terms_registered_event", return_value=1 + license, "_parse_tx_license_terms_registered_event", return_value=1 ): - response = license_client.mint_license_tokens( + response = license.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, @@ -674,14 +526,14 @@ def test_mint_license_tokens_success( ) assert "tx_hash" in response - assert response["tx_hash"] == TX_HASH[2:] + assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) class TestLicenseTerms: """Tests for retrieving license terms.""" - def test_get_license_terms_success(self, license_client): + def test_get_license_terms_success(self, license): mock_response = { "terms": { "transferable": True, @@ -704,16 +556,16 @@ def test_get_license_terms_success(self, license_client): } } with patch.object( - license_client.license_template_client, + license.license_template_client, "getLicenseTerms", return_value=mock_response, ): - response = license_client.get_license_terms(1) + response = license.get_license_terms(1) assert response == mock_response - def test_get_license_terms_not_exist(self, license_client): + def test_get_license_terms_not_exist(self, license): with patch.object( - license_client.license_template_client, + license.license_template_client, "getLicenseTerms", side_effect=Exception("Given licenseTermsId is not exist."), ): @@ -721,128 +573,7 @@ def test_get_license_terms_not_exist(self, license_client): ValueError, match="Failed to get license terms: Given licenseTermsId is not exist.", ): - license_client.get_license_terms(1) - - -class TestLicensingConfig: - """Tests for license configuration functionality.""" - - def test_set_licensing_config_missing_params(self, license_client): - incomplete_config = { - "isSet": True, - "mintingFee": 0, - } - with pytest.raises( - ValueError, match="Missing required licensing_config parameters:" - ): - license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, - license_terms_id=1, - licensing_config=incomplete_config, - ) - - def test_set_licensing_config_negative_minting_fee(self, license_client): - config = { - "isSet": True, - "mintingFee": -1, - "hookData": "0x", - "licensingHook": ZERO_ADDRESS, - "commercialRevShare": 0, - "disabled": False, - "expectMinimumGroupRewardShare": 0, - "expectGroupRewardPool": ZERO_ADDRESS, - } - with pytest.raises( - ValueError, - match="Failed to set licensing config: The minting fee must be greater than 0.", - ): - license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, license_terms_id=1, licensing_config=config - ) - - def test_set_licensing_config_unregistered_licensing_hook(self, license_client): - custom_address = "0x1234567890123456789012345678901234567890" - config = { - "isSet": True, - "mintingFee": 1, - "hookData": "0x", - "licensingHook": custom_address, - "commercialRevShare": 0, - "disabled": False, - "expectMinimumGroupRewardShare": 0, - "expectGroupRewardPool": ZERO_ADDRESS, - } - with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True - ), patch.object( - license_client.module_registry_client, "isRegistered", return_value=False - ): - with pytest.raises( - ValueError, - match="Failed to set licensing config: The licensing hook is not registered.", - ): - license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, license_terms_id=1, licensing_config=config - ) - - def test_set_licensing_config_template_terms_mismatch(self, license_client): - config = { - "isSet": True, - "mintingFee": 1, - "hookData": "0x", - "licensingHook": ZERO_ADDRESS, - "commercialRevShare": 0, - "disabled": False, - "expectMinimumGroupRewardShare": 0, - "expectGroupRewardPool": ZERO_ADDRESS, - } - with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True - ): - with pytest.raises( - ValueError, - match="Failed to set licensing config: The license template is zero address but license terms id is not zero.", - ): - license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, - license_terms_id=1, - license_template=ZERO_ADDRESS, - licensing_config=config, - ) - - def test_set_licensing_config_zero_address_with_rev_share(self, license_client): - config = { - "isSet": True, - "mintingFee": 1, - "hookData": "0x", - "licensingHook": ZERO_ADDRESS, - "commercialRevShare": 10, - "disabled": False, - "expectMinimumGroupRewardShare": 0, - "expectGroupRewardPool": ZERO_ADDRESS, - } - with patch.object( - license_client.ip_asset_registry_client, "isRegistered", return_value=True - ): - with pytest.raises( - ValueError, - match="Failed to set licensing config: The license template cannot be zero address if commercial revenue share is not zero.", - ): - license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, - license_terms_id=0, - license_template=ZERO_ADDRESS, - licensing_config=config, - ) - - -######################################################################################## -##TODO: Need to refactor the previous test case - - -@fixture -def license(mock_web3, mock_account): - return License(web3=mock_web3, account=mock_account, chain_id=CHAIN_ID) + license.get_license_terms(1) @fixture @@ -877,6 +608,21 @@ def _patch(has_ip_attached_license_terms=True): return _patch +@fixture +def default_licensing_config() -> LicensingConfig: + """Default licensing configuration for testing.""" + return { + "is_set": True, + "minting_fee": 1, + "licensing_hook": ZERO_ADDRESS, + "hook_data": "0x", + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + class TestMintLicenseTokens: def test_default_value_when_not_provided( self, @@ -927,3 +673,373 @@ def test_call_value_when_provided( call_args = mock_build_mintLicenseTokens_transaction.call_args[0] assert call_args[6] == 10 # max_minting_fee assert call_args[7] == 10 * 10**6 # max_revenue_share + + +class TestSetLicensingConfig: + """Tests for setLicensingConfig validation errors in execution order.""" + + def test_set_licensing_config_negative_minting_fee( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when minting fee is negative.""" + config: LicensingConfig = default_licensing_config.copy() + config["minting_fee"] = -1 + + with pytest.raises( + ValueError, + match="Failed to set licensing config: The minting fee must be greater than 0.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_invalid_licensing_hook_address( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when licensing hook is not a valid address.""" + config: LicensingConfig = default_licensing_config.copy() + config["licensing_hook"] = "invalid_hook_address" + + with pytest.raises( + ValueError, + match="Failed to set licensing config: Invalid address: invalid_hook_address.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_unregistered_licensing_hook( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when licensing hook is not registered.""" + custom_hook_address = "0x1234567890123456789012345678901234567890" + config: LicensingConfig = default_licensing_config.copy() + config["licensing_hook"] = custom_hook_address + + with patch.object( + license.module_registry_client, "isRegistered", return_value=False + ): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The licensing hook is not registered.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_commercial_rev_share_below_zero( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when commercial revenue share is below 0%.""" + config: LicensingConfig = default_licensing_config.copy() + config["commercial_rev_share"] = -1 # < 0% + + with pytest.raises( + ValueError, + match="Failed to set licensing config: The commercial_rev_share must be between 0 and 100.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_commercial_rev_share_above_100( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when commercial revenue share is above 100%.""" + config: LicensingConfig = default_licensing_config.copy() + config["commercial_rev_share"] = 101 # > 100% + + with pytest.raises( + ValueError, + match="Failed to set licensing config: The commercial_rev_share must be between 0 and 100.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_expect_minimum_group_reward_share_below_zero( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when expect minimum group reward share is below 0%.""" + config: LicensingConfig = default_licensing_config.copy() + config["expect_minimum_group_reward_share"] = -1 # < 0% + + with pytest.raises( + ValueError, + match="Failed to set licensing config: The expect_minimum_group_reward_share must be between 0 and 100.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_expect_minimum_group_reward_share_above_100( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when expect minimum group reward share is above 100%.""" + config: LicensingConfig = default_licensing_config.copy() + config["expect_minimum_group_reward_share"] = 101 # > 100% + + with pytest.raises( + ValueError, + match="Failed to set licensing config: The expect_minimum_group_reward_share must be between 0 and 100.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_invalid_group_reward_pool_address( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when group reward pool is not a valid address.""" + config: LicensingConfig = default_licensing_config.copy() + config["expect_group_reward_pool"] = "invalid_pool_address" + + with pytest.raises( + ValueError, + match="Failed to set licensing config: Invalid address: invalid_pool_address.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=config, + ) + + def test_set_licensing_config_invalid_license_template_address( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when license template is not a valid address.""" + with pytest.raises( + ValueError, + match="Failed to set licensing config: Invalid address: invalid_template.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + license_template="invalid_template", + licensing_config=default_licensing_config, + ) + + def test_set_licensing_config_zero_address_template_with_non_zero_rev_share( + self, + license: License, + default_licensing_config: LicensingConfig, + ): + """Test validation error when license template is zero address but commercial revenue share is not zero.""" + config: LicensingConfig = default_licensing_config.copy() + config["commercial_rev_share"] = 10 # Non-zero + + with patch.object( + license.ip_asset_registry_client, "isRegistered", return_value=True + ): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The license template cannot be zero address if commercial revenue share is not zero.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=0, + license_template=ZERO_ADDRESS, + licensing_config=config, + ) + + def test_set_licensing_config_invalid_ip_id_address( + self, license: License, default_licensing_config: LicensingConfig + ): + """Test validation error when IP ID is not a valid address.""" + with pytest.raises( + ValueError, + match="Failed to set licensing config: Invalid address: invalid_address.", + ): + license.set_licensing_config( + ip_id="invalid_address", + license_terms_id=1, + licensing_config=default_licensing_config, + ) + + def test_set_licensing_config_ip_not_registered( + self, + license: License, + default_licensing_config: LicensingConfig, + patch_is_registered, + ): + """Test validation error when IP is not registered.""" + with patch_is_registered(is_registered=False): + with pytest.raises( + ValueError, + match=f"Failed to set licensing config: The licensor IP with id {ZERO_ADDRESS} is not registered.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=default_licensing_config, + ) + + def test_set_licensing_config_license_terms_not_exist( + self, + license: License, + default_licensing_config: LicensingConfig, + patch_is_registered, + patch_exists, + ): + """Test validation error when license terms ID does not exist.""" + with patch_is_registered(is_registered=True), patch_exists(exists=False): + with pytest.raises( + ValueError, + match="Failed to set licensing config: License terms id 1 does not exist.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=default_licensing_config, + ) + + def test_set_licensing_config_template_zero_address_with_non_zero_terms_id( + self, + license: License, + default_licensing_config: LicensingConfig, + patch_is_registered, + ): + """Test validation error when license template is zero address but license terms ID is not zero.""" + with patch_is_registered(): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The license template is zero address but license terms id is not zero.", + ): + license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + license_template=ZERO_ADDRESS, + licensing_config=default_licensing_config, + ) + + def test_set_licensing_config_success_with_default_template( + self, + license: License, + patch_is_registered, + patch_exists, + ): + """Test successful licensing config setting with default license template.""" + with patch_is_registered(), patch_exists(): + with patch.object( + license.licensing_module_client, + "build_setLicensingConfig_transaction", + return_value=TX_HASH, + ) as mock_build_setLicensingConfig_transaction: + + result = license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=LicensingConfig( + is_set=True, + minting_fee=1, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=10, + disabled=False, + expect_minimum_group_reward_share=100, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + + assert result["success"] is True + assert result["tx_hash"] == TX_HASH.hex() # Convert bytes to hex string + assert ( + mock_build_setLicensingConfig_transaction.call_args[0][1] + == license.license_template_client.contract.address + ) + assert mock_build_setLicensingConfig_transaction.call_args[0][3] == { + "isSet": True, + "mintingFee": 1, + "licensingHook": ZERO_ADDRESS, + "hookData": ZERO_HASH, + "commercialRevShare": 10 * 10**6, + "disabled": False, + "expectMinimumGroupRewardShare": 100 * 10**6, + "expectGroupRewardPool": ZERO_ADDRESS, + } + + def test_set_licensing_config_success_with_custom_template( + self, + license: License, + default_licensing_config: LicensingConfig, + patch_is_registered, + patch_exists, + ): + """Test successful licensing config setting with custom license template.""" + custom_template = "0x1234567890123456789012345678901234567890" + + with patch_is_registered(is_registered=True), patch_exists(exists=True): + with patch.object( + license.licensing_module_client, + "build_setLicensingConfig_transaction", + return_value={"tx_hash": TX_HASH}, + ) as mock_build_setLicensingConfig_transaction: + result = license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + license_template=custom_template, + licensing_config=default_licensing_config, + ) + + assert result["success"] is True + assert result["tx_hash"] == TX_HASH.hex() + assert ( + mock_build_setLicensingConfig_transaction.call_args[0][1] + == custom_template + ) + assert mock_build_setLicensingConfig_transaction.call_args[0][3] == { + "isSet": True, + "mintingFee": 1, + "licensingHook": ZERO_ADDRESS, + "hookData": "0x", + "commercialRevShare": 0, + "disabled": False, + "expectMinimumGroupRewardShare": 0, + "expectGroupRewardPool": ZERO_ADDRESS, + } + + def test_set_licensing_config_success_with_tx_options( + self, + license: License, + default_licensing_config: LicensingConfig, + patch_is_registered, + patch_exists, + ): + """Test successful licensing config setting with transaction options.""" + tx_options = {"gasPrice": 20000000000, "nonce": 3} + + with patch_is_registered(is_registered=True), patch_exists(exists=True): + with patch.object( + license.licensing_module_client, + "build_setLicensingConfig_transaction", + return_value={"tx_hash": TX_HASH}, + ) as mock_build_setLicensingConfig_transaction: + result = license.set_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + licensing_config=default_licensing_config, + tx_options=tx_options, + ) + + assert result["success"] is True + assert result["tx_hash"] == TX_HASH.hex() + + assert mock_build_setLicensingConfig_transaction.call_args[0][4] == { + "from": "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c", + "gasPrice": 1, + "nonce": 3, + } From 84549d9c06e60f6d6353b12b5a843aa307016efe Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Sep 2025 15:11:56 +0800 Subject: [PATCH 7/8] refactor: replace direct address validation in License class with a dedicated validate_address function; enhance type hints in test cases for improved clarity --- .../resources/License.py | 3 +- tests/unit/resources/test_license.py | 199 +++++++++++++++--- 2 files changed, 173 insertions(+), 29 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/License.py b/src/story_protocol_python_sdk/resources/License.py index d2d926b..1481096 100644 --- a/src/story_protocol_python_sdk/resources/License.py +++ b/src/story_protocol_python_sdk/resources/License.py @@ -602,8 +602,7 @@ def get_licensing_config( :return LicensingConfig: A dictionary containing the licensing configuration. """ try: - if not self.web3.is_address(ip_id): - raise ValueError(f"Invalid IP id address: {ip_id}") + validate_address(ip_id) if license_template is None: license_template = self.license_template_client.contract.address diff --git a/tests/unit/resources/test_license.py b/tests/unit/resources/test_license.py index f1d560c..8f7e056 100644 --- a/tests/unit/resources/test_license.py +++ b/tests/unit/resources/test_license.py @@ -1,3 +1,4 @@ +from typing import Callable from unittest.mock import patch import pytest @@ -12,7 +13,7 @@ @fixture -def license(mock_web3, mock_account): +def license(mock_web3, mock_account) -> License: return License(web3=mock_web3, account=mock_account, chain_id=CHAIN_ID) @@ -54,7 +55,7 @@ def test_register_pil_terms_license_terms_id_registered(self, license: License): assert response["license_terms_id"] == 1 assert "tx_hash" not in response - def test_register_pil_terms_success(self, license): + def test_register_pil_terms_success(self, license: License): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( @@ -100,7 +101,9 @@ def test_register_pil_terms_success(self, license): assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) - def test_register_pil_terms_commercial_rev_share_error_more_than_100(self, license): + def test_register_pil_terms_commercial_rev_share_error_more_than_100( + self, license: License + ): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( @@ -136,7 +139,9 @@ def test_register_pil_terms_commercial_rev_share_error_more_than_100(self, licen uri="", ) - def test_register_pil_terms_commercial_rev_share_error_less_than_0(self, license): + def test_register_pil_terms_commercial_rev_share_error_less_than_0( + self, license: License + ): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( @@ -177,7 +182,7 @@ class TestNonComSocialRemixingPIL: """Tests for non-commercial social remixing PIL functionality.""" def test_register_non_com_social_remixing_pil_license_terms_id_registered( - self, license + self, license: License ): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=1 @@ -186,7 +191,7 @@ def test_register_non_com_social_remixing_pil_license_terms_id_registered( assert response["license_terms_id"] == 1 assert "tx_hash" not in response - def test_register_non_com_social_remixing_pil_success(self, license): + def test_register_non_com_social_remixing_pil_success(self, license: License): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( @@ -209,7 +214,7 @@ def test_register_non_com_social_remixing_pil_success(self, license): assert "license_terms_id" in response assert response["license_terms_id"] == 1 - def test_register_non_com_social_remixing_pil_error(self, license): + def test_register_non_com_social_remixing_pil_error(self, license: License): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( @@ -229,7 +234,9 @@ def test_register_non_com_social_remixing_pil_error(self, license): class TestCommercialUsePIL: """Tests for commercial use PIL functionality.""" - def test_register_commercial_use_pil_license_terms_id_registered(self, license): + def test_register_commercial_use_pil_license_terms_id_registered( + self, license: License + ): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=1 ): @@ -267,7 +274,7 @@ def test_register_commercial_use_pil_success_without_logs(self, license: License assert response["tx_hash"] == TX_HASH.hex() assert isinstance(response["tx_hash"], str) - def test_register_commercial_use_pil_error(self, license): + def test_register_commercial_use_pil_error(self, license: License): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=0 ), patch.object( @@ -288,7 +295,9 @@ def test_register_commercial_use_pil_error(self, license): class TestCommercialRemixPIL: """Tests for commercial remix PIL functionality.""" - def test_register_commercial_remix_pil_license_terms_id_registered(self, license): + def test_register_commercial_remix_pil_license_terms_id_registered( + self, license: License + ): with patch.object( license.license_template_client, "getLicenseTermsId", return_value=1 ), patch.object( @@ -340,7 +349,7 @@ def test_register_commercial_remix_pil_success(self, license: License): class TestLicenseAttachment: """Tests for license attachment functionality.""" - def test_attach_license_terms_ip_not_registered(self, license): + def test_attach_license_terms_ip_not_registered(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=False ): @@ -353,7 +362,7 @@ def test_attach_license_terms_ip_not_registered(self, license): license_terms_id=1, ) - def test_attach_license_terms_license_terms_not_exist(self, license): + def test_attach_license_terms_license_terms_not_exist(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object(license.license_registry_client, "exists", return_value=False): @@ -384,7 +393,7 @@ def test_attach_license_terms_already_attached(self, license): license_terms_id=1, ) - def test_attach_license_terms_success(self, license): + def test_attach_license_terms_success(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( @@ -418,7 +427,7 @@ def test_attach_license_terms_success(self, license): class TestLicenseTokens: """Tests for license token minting functionality.""" - def test_mint_license_tokens_licensor_ip_not_registered(self, license): + def test_mint_license_tokens_licensor_ip_not_registered(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=False ): @@ -434,7 +443,7 @@ def test_mint_license_tokens_licensor_ip_not_registered(self, license): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_license_terms_not_exist(self, license): + def test_mint_license_tokens_license_terms_not_exist(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object(license.license_template_client, "exists", return_value=False): @@ -447,7 +456,7 @@ def test_mint_license_tokens_license_terms_not_exist(self, license): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_not_attached(self, license): + def test_mint_license_tokens_not_attached(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( @@ -469,7 +478,7 @@ def test_mint_license_tokens_not_attached(self, license): receiver=ZERO_ADDRESS, ) - def test_mint_license_tokens_invalid_template(self, license): + def test_mint_license_tokens_invalid_template(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=True ): @@ -495,7 +504,7 @@ def test_mint_license_tokens_invalid_receiver(self, license: License): receiver="invalid address", ) - def test_mint_license_tokens_success(self, license): + def test_mint_license_tokens_success(self, license: License): with patch.object( license.ip_asset_registry_client, "isRegistered", return_value=True ), patch.object( @@ -533,7 +542,7 @@ def test_mint_license_tokens_success(self, license): class TestLicenseTerms: """Tests for retrieving license terms.""" - def test_get_license_terms_success(self, license): + def test_get_license_terms_success(self, license: License): mock_response = { "terms": { "transferable": True, @@ -563,7 +572,7 @@ def test_get_license_terms_success(self, license): response = license.get_license_terms(1) assert response == mock_response - def test_get_license_terms_not_exist(self, license): + def test_get_license_terms_not_exist(self, license: License): with patch.object( license.license_template_client, "getLicenseTerms", @@ -577,8 +586,8 @@ def test_get_license_terms_not_exist(self, license): @fixture -def patch_is_registered(license): - def _patch(is_registered=True): +def patch_is_registered(license: License) -> Callable: + def _patch(is_registered: bool = True): return patch.object( license.ip_asset_registry_client, "isRegistered", return_value=is_registered ) @@ -587,8 +596,8 @@ def _patch(is_registered=True): @fixture -def patch_exists(license): - def _patch(exists=True): +def patch_exists(license: License) -> Callable: + def _patch(exists: bool = True): return patch.object( license.license_template_client, "exists", return_value=exists ) @@ -597,8 +606,8 @@ def _patch(exists=True): @fixture -def patch_has_ip_attached_license_terms(license): - def _patch(has_ip_attached_license_terms=True): +def patch_has_ip_attached_license_terms(license: License) -> Callable: + def _patch(has_ip_attached_license_terms: bool = True): return patch.object( license.license_registry_client, "hasIpAttachedLicenseTerms", @@ -835,7 +844,7 @@ def test_set_licensing_config_invalid_license_template_address( def test_set_licensing_config_zero_address_template_with_non_zero_rev_share( self, - license: License, + license, default_licensing_config: LicensingConfig, ): """Test validation error when license template is zero address but commercial revenue share is not zero.""" @@ -1043,3 +1052,139 @@ def test_set_licensing_config_success_with_tx_options( "gasPrice": 1, "nonce": 3, } + + +class TestGetLicensingConfig: + """Tests for getLicensingConfig functionality.""" + + def test_get_licensing_config_invalid_ip_id_address(self, license: License): + """Test validation error when IP ID is not a valid address.""" + with pytest.raises( + ValueError, + match="Failed to get licensing config: Invalid address: invalid_address.", + ): + license.get_licensing_config( + ip_id="invalid_address", + license_terms_id=1, + ) + + def test_get_licensing_config_invalid_license_template_address( + self, license: License + ): + """Test validation error when license template is not a valid address.""" + with pytest.raises( + ValueError, + match="Failed to get licensing config: Invalid address: invalid_template", + ): + license.get_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + license_template="invalid_template", + ) + + def test_get_licensing_config_success_with_default_template(self, license: License): + """Test successful licensing config retrieval with default license template.""" + mock_tuple_data = ( + True, # is_set + 100, # minting_fee + ZERO_ADDRESS, # licensing_hook + ZERO_HASH, # hook_data + 10 * 10**6, # commercial_rev_share (converted to raw value) + False, # disabled + 50 * 10**6, # expect_minimum_group_reward_share (converted to raw value) + ZERO_ADDRESS, # expect_group_reward_pool + ) + + with patch.object( + license.license_registry_client, + "getLicensingConfig", + return_value=mock_tuple_data, + ) as mock_get_licensing_config: + result = license.get_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + ) + + # Verify the correct parameters were passed to the contract call + mock_get_licensing_config.assert_called_once_with( + ZERO_ADDRESS, license.license_template_client.contract.address, 1 + ) + + # Verify the returned LicensingConfig structure + expected_config = { + "is_set": True, + "minting_fee": 100, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_HASH, + "commercial_rev_share": 10 * 10**6, + "disabled": False, + "expect_minimum_group_reward_share": 50 * 10**6, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + assert result == expected_config + assert isinstance(result, dict) + assert len(result) == 8 + + def test_get_licensing_config_success_with_custom_template(self, license: License): + """Test successful licensing config retrieval with custom license template.""" + custom_template = "0x1234567890123456789012345678901234567890" + mock_tuple_data = ( + False, # is_set + 0, # minting_fee + ZERO_ADDRESS, # licensing_hook + "0x", # hook_data + 0, # commercial_rev_share + True, # disabled + 0, # expect_minimum_group_reward_share + ZERO_ADDRESS, # expect_group_reward_pool + ) + + with patch.object( + license.license_registry_client, + "getLicensingConfig", + return_value=mock_tuple_data, + ) as mock_get_licensing_config: + result = license.get_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + license_template=custom_template, + ) + + # Verify the correct template was passed to the contract call + mock_get_licensing_config.assert_called_once_with( + ZERO_ADDRESS, custom_template, 1 + ) + + # Verify the returned LicensingConfig structure + expected_config = { + "is_set": False, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": "0x", + "commercial_rev_share": 0, + "disabled": True, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + } + + assert result == expected_config + assert isinstance(result, dict) + assert result["disabled"] is True + assert result["is_set"] is False + + def test_get_licensing_config_contract_call_failure(self, license: License): + """Test error handling when contract call fails.""" + with patch.object( + license.license_registry_client, + "getLicensingConfig", + side_effect=Exception("Contract call failed"), + ): + with pytest.raises( + ValueError, + match="Failed to get licensing config: Contract call failed", + ): + license.get_licensing_config( + ip_id=ZERO_ADDRESS, + license_terms_id=1, + ) From 63b7feb173a95825c0229b8cdb729f1f36b90cce Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Sep 2025 15:55:39 +0800 Subject: [PATCH 8/8] refactor: streamline transaction hash handling in License class methods for consistency; update licensing hook documentation for clarity --- src/story_protocol_python_sdk/resources/License.py | 11 ++++++----- .../utils/licensing_config_data.py | 2 +- tests/unit/resources/test_license.py | 8 ++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/License.py b/src/story_protocol_python_sdk/resources/License.py index 1481096..0abe68c 100644 --- a/src/story_protocol_python_sdk/resources/License.py +++ b/src/story_protocol_python_sdk/resources/License.py @@ -219,14 +219,14 @@ def register_commercial_use_pil( complete_license_terms, tx_options=tx_options, ) - + tx_hash = response["tx_hash"] if not response["tx_receipt"]["logs"]: - return {"tx_hash": response["tx_hash"]} + return {"tx_hash": tx_hash} target_logs = self._parse_tx_license_terms_registered_event( response["tx_receipt"] ) - return {"tx_hash": response["tx_hash"], "license_terms_id": target_logs} + return {"tx_hash": tx_hash, "license_terms_id": target_logs} except Exception as e: raise e @@ -272,13 +272,14 @@ def register_commercial_remix_pil( tx_options=tx_options, ) + tx_hash = response["tx_hash"] if not response["tx_receipt"]["logs"]: - return {"tx_hash": response["tx_hash"]} + return {"tx_hash": tx_hash} target_logs = self._parse_tx_license_terms_registered_event( response["tx_receipt"] ) - return {"tx_hash": response["tx_hash"], "license_terms_id": target_logs} + return {"tx_hash": tx_hash, "license_terms_id": target_logs} except Exception as e: raise e diff --git a/src/story_protocol_python_sdk/utils/licensing_config_data.py b/src/story_protocol_python_sdk/utils/licensing_config_data.py index 67007e9..abe3a8a 100644 --- a/src/story_protocol_python_sdk/utils/licensing_config_data.py +++ b/src/story_protocol_python_sdk/utils/licensing_config_data.py @@ -25,7 +25,7 @@ class LicensingConfig(TypedDict): This contract's `beforeMintLicenseTokens` function is executed before a user mints a License Token, allowing for custom validation or business logic to be enforced during the minting process. For detailed documentation on licensing hook, visit https://docs.story.foundation/concepts/hooks#licensing-hooks - hook_data: The data to be used by the licensing hook. Set to a `zeroHash` if no data is provided. For detailed documentation on hook data, visit https://docs.story.foundation/concepts/hooks#hook-data + hook_data: The data to be used by the licensing hook. For detailed documentation on hook data, visit https://docs.story.foundation/concepts/hooks#hook-data commercial_rev_share: Percentage of revenue that must be shared with the licensor. Must be between 0 and 100 (where 100% represents 100_000_000). disabled: Whether the licensing is disabled or not. If this is true, then no licenses can be minted and no more derivatives can be attached at all. expect_minimum_group_reward_share: The minimum percentage of the group’s reward share (from 0 to 100%, represented as 100_000_000) that can be allocated to the IP when it is added to the group. diff --git a/tests/unit/resources/test_license.py b/tests/unit/resources/test_license.py index 8f7e056..23ce92e 100644 --- a/tests/unit/resources/test_license.py +++ b/tests/unit/resources/test_license.py @@ -946,7 +946,7 @@ def test_set_licensing_config_success_with_default_template( with patch.object( license.licensing_module_client, "build_setLicensingConfig_transaction", - return_value=TX_HASH, + return_value={"nonce": 1}, ) as mock_build_setLicensingConfig_transaction: result = license.set_licensing_config( @@ -995,7 +995,7 @@ def test_set_licensing_config_success_with_custom_template( with patch.object( license.licensing_module_client, "build_setLicensingConfig_transaction", - return_value={"tx_hash": TX_HASH}, + return_value={"nonce": 1}, ) as mock_build_setLicensingConfig_transaction: result = license.set_licensing_config( ip_id=ZERO_ADDRESS, @@ -1029,7 +1029,7 @@ def test_set_licensing_config_success_with_tx_options( patch_exists, ): """Test successful licensing config setting with transaction options.""" - tx_options = {"gasPrice": 20000000000, "nonce": 3} + tx_options = {"gasPrice": 1, "nonce": 3} with patch_is_registered(is_registered=True), patch_exists(exists=True): with patch.object( @@ -1049,7 +1049,7 @@ def test_set_licensing_config_success_with_tx_options( assert mock_build_setLicensingConfig_transaction.call_args[0][4] == { "from": "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c", - "gasPrice": 1, + "gasPrice": 1, # mock_web3.to_wei return 1 "nonce": 3, }