diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 7222ed6c..335037f3 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 12e9eb75..d8243fec 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 0f553aca..0abe68c0 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: @@ -183,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. @@ -214,14 +219,14 @@ def register_commercial_use_pil( complete_license_terms, tx_options=tx_options, ) - - if not response["tx_receipt"].logs: - return None + tx_hash = response["tx_hash"] + if not response["tx_receipt"]["logs"]: + 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 @@ -233,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. @@ -267,13 +272,14 @@ def register_commercial_remix_pil( tx_options=tx_options, ) - if not response["tx_receipt"].logs: - return None + tx_hash = response["tx_hash"] + if not response["tx_receipt"]["logs"]: + 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 @@ -375,12 +381,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( @@ -522,7 +524,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 +533,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 +575,7 @@ def set_licensing_config( ip_id, license_template, license_terms_id, - licensing_config, + validated_licensing_config, tx_options=tx_options, ) @@ -634,3 +586,35 @@ 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: + validate_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 bf797ad8..ff624de8 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", 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 00000000..abe3a8af --- /dev/null +++ b/src/story_protocol_python_sdk/utils/licensing_config_data.py @@ -0,0 +1,146 @@ +from dataclasses import dataclass +from typing import TypedDict + +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 ( + 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. 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 + + +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: + """ + 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, + module_registry_client: ModuleRegistryClient, + licensing_config: LicensingConfig | None = None, + ) -> ValidatedLicensingConfig: + """ + 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 ValidatedLicensingConfig( + isSet=False, + mintingFee=0, + licensingHook=ZERO_ADDRESS, + hookData=ZERO_HASH, + commercialRevShare=0, + disabled=False, + expectMinimumGroupRewardShare=0, + expectGroupRewardPool=ZERO_ADDRESS, + ) + + if licensing_config["minting_fee"] < 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"], + 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"], + expectMinimumGroupRewardShare=get_revenue_share( + licensing_config["expect_minimum_group_reward_share"], + RevShareType.EXPECT_MINIMUM_GROUP_REWARD_SHARE, + ), + expectGroupRewardPool=validate_address( + licensing_config["expect_group_reward_pool"] + ), + ) diff --git a/tests/integration/test_integration_license.py b/tests/integration/test_integration_license.py index f0cdc2a1..881ffe01 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, + ) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 484fbfc6..e5389e8c 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 8934200b..23ce92e8 100644 --- a/tests/unit/resources/test_license.py +++ b/tests/unit/resources/test_license.py @@ -1,104 +1,42 @@ -from unittest.mock import MagicMock, patch +from typing import Callable +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) -> License: + 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 +55,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: 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 +93,25 @@ 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 + 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_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyToken", return_value=True, ): @@ -191,9 +119,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 +135,21 @@ 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 + 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_terms_util.royalty_module_client, + license.license_terms_util.royalty_module_client, "isWhitelistedRoyaltyToken", return_value=True, ): @@ -229,9 +157,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 +173,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 +182,112 @@ 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: 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: 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: 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 + self, license: 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: 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 ) @@ -393,16 +296,16 @@ class TestCommercialRemixPIL: """Tests for commercial remix PIL functionality.""" def test_register_commercial_remix_pil_license_terms_id_registered( - self, license_client + 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, ): - 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 +314,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: 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: 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 +387,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: 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: 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 +443,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: 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 +456,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: 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 +470,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 +478,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: 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 +491,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 +504,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: 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 +535,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: License): mock_response = { "terms": { "transferable": True, @@ -704,16 +565,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: License): with patch.object( - license_client.license_template_client, + license.license_template_client, "getLicenseTerms", side_effect=Exception("Given licenseTermsId is not exist."), ): @@ -721,133 +582,12 @@ 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 -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 ) @@ -856,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 ) @@ -866,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", @@ -877,6 +617,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 +682,509 @@ 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, + 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={"nonce": 1}, + ) 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={"nonce": 1}, + ) 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": 1, "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, # mock_web3.to_wei return 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, + ) 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 00000000..46ea42d0 --- /dev/null +++ b/tests/unit/utils/test_licensing_config_data.py @@ -0,0 +1,327 @@ +from unittest.mock import MagicMock, Mock + +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, + ValidatedLicensingConfig, +) + + +@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, mock_module_registry_client): + """Test validate_license_config with no input returns default values.""" + result = LicensingConfigData.validate_license_config( + mock_module_registry_client() + ) + + assert result == ValidatedLicensingConfig( + isSet=False, + mintingFee=0, + licensingHook=ZERO_ADDRESS, + hookData=ZERO_HASH, + commercialRevShare=0, + disabled=False, + expectMinimumGroupRewardShare=0, + expectGroupRewardPool=ZERO_ADDRESS, + ) + + 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, + "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( + mock_module_registry_client(), input_config + ) + + assert result == ValidatedLicensingConfig( + isSet=True, + mintingFee=100, + licensingHook=ZERO_ADDRESS, + hookData="0xabcdef", + commercialRevShare=50 * 10**6, + disabled=False, + expectMinimumGroupRewardShare=25 * 10**6, + expectGroupRewardPool=ZERO_ADDRESS, + ) + + 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, + "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( + mock_module_registry_client(), input_config + ) + + 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, + "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( + mock_module_registry_client(), input_config + ) + + def test_validate_license_config_invalid_expect_minimum_group_reward_share_negative( + self, mock_module_registry_client + ): + """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( + mock_module_registry_client(), input_config + ) + + def test_validate_license_config_invalid_expect_minimum_group_reward_share_too_high( + self, mock_module_registry_client + ): + """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( + mock_module_registry_client(), input_config + ) + + 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, + "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( + mock_module_registry_client(), input_config + ) + + 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, + "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( + mock_module_registry_client(), input_config + ) + + 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, + "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( + 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: + 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, + )