From 675083c2ff786a17ff1aa12e4299e47803f64202 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 12 Aug 2025 14:07:44 +0800 Subject: [PATCH 1/3] Add the mintAndRegisterIpAndMakeDerivative method from contract --- .../DerivativeWorkflows_client.py | 20 +++++++++++++++++++ .../scripts/config.json | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py b/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py index 8cc23315..dfd6d7cd 100644 --- a/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py @@ -36,6 +36,26 @@ def __init__(self, web3: Web3): abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) + def mintAndRegisterIpAndMakeDerivative( + self, spgNftContract, derivData, ipMetadata, recipient, allowDuplicates + ): + return self.contract.functions.mintAndRegisterIpAndMakeDerivative( + spgNftContract, derivData, ipMetadata, recipient, allowDuplicates + ).transact() + + def build_mintAndRegisterIpAndMakeDerivative_transaction( + self, + spgNftContract, + derivData, + ipMetadata, + recipient, + allowDuplicates, + tx_params, + ): + return self.contract.functions.mintAndRegisterIpAndMakeDerivative( + spgNftContract, derivData, ipMetadata, recipient, allowDuplicates + ).build_transaction(tx_params) + def registerIpAndMakeDerivative( self, nftContract, tokenId, derivData, ipMetadata, sigMetadataAndRegister ): diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index bdc43954..89604bb6 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -228,7 +228,10 @@ { "contract_name": "DerivativeWorkflows", "contract_address": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", - "functions": ["registerIpAndMakeDerivative"] + "functions": [ + "registerIpAndMakeDerivative", + "mintAndRegisterIpAndMakeDerivative" + ] } ] } From d13960c50a843a440a1598ed43c8f0708d742f13 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 15 Aug 2025 15:11:59 +0800 Subject: [PATCH 2/3] feat: implement mint_and_register_ip_and_make_derivative method --- src/story_protocol_python_sdk/__init__.py | 3 + .../resources/IPAsset.py | 52 ++++++++ .../types/resource/IPAsset.py | 18 +++ tests/integration/conftest.py | 66 +++++++++- .../integration/test_integration_ip_asset.py | 100 +++++++-------- tests/unit/resources/test_ip_asset.py | 118 +++++++++++++++++- 6 files changed, 299 insertions(+), 58 deletions(-) create mode 100644 src/story_protocol_python_sdk/types/resource/IPAsset.py diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index e8276bad..d8af7a06 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -8,6 +8,7 @@ from .resources.WIP import WIP from .story_client import StoryClient from .types.common import AccessPermission +from .types.resource.IPAsset import RegistrationResponse from .utils.constants import ( DEFAULT_FUNCTION_SELECTOR, MAX_ROYALTY_TOKEN, @@ -28,9 +29,11 @@ "IPAccount", "Dispute", "WIP", + # Types "AccessPermission", "DerivativeDataInput", "IPMetadataInput", + "RegistrationResponse", # Constants "ZERO_ADDRESS", "ZERO_HASH", diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 2aea9ff0..cdd9afe6 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1,5 +1,6 @@ """Module for handling IP Account operations and transactions.""" +from ens.ens import Address from web3 import Web3 from story_protocol_python_sdk.abi.AccessController.AccessController_client import ( @@ -34,6 +35,7 @@ ) from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient from story_protocol_python_sdk.types.common import AccessPermission +from story_protocol_python_sdk.types.resource.IPAsset import RegistrationResponse from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import ( DerivativeData, @@ -44,6 +46,7 @@ from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction +from story_protocol_python_sdk.utils.validation import validate_address class IPAsset: @@ -852,6 +855,55 @@ def register_derivative_ip( except Exception as e: raise e + def mint_and_register_ip_and_make_derivative( + self, + spg_nft_contract: str, + deriv_data: DerivativeDataInput, + ip_metadata: IPMetadataInput | None = None, + recipient: Address | None = None, + allow_duplicates: bool = True, + tx_options: dict | None = None, + ) -> RegistrationResponse: + """ + Mint an NFT from a collection and register it as a derivative IP without license tokens. + + :param spg_nft_contract str: The address of the SPGNFT collection. + :param deriv_data `DerivativeDataInput`: The derivative data to be used for register derivative. + :param ip_metadata `IPMetadataInput`: [Optional] The desired metadata for the newly minted NFT and newly registered IP. + :param recipient str: [Optional] The address to receive the minted NFT. If not provided, the client's own wallet address will be used. + :param allow_duplicates bool: [Optional] Set to true to allow minting an NFT with a duplicate metadata hash. (default: True) + :param tx_options dict: [Optional] Transaction options. + :return RegistrationResponse: Dictionary with the tx hash, IP ID and token ID. + """ + + try: + validated_deriv_data = DerivativeData.from_input( + web3=self.web3, input_data=deriv_data + ).get_validated_data() + response = build_and_send_transaction( + self.web3, + self.account, + self.derivative_workflows_client.build_mintAndRegisterIpAndMakeDerivative_transaction, + validate_address(spg_nft_contract), + validated_deriv_data, + IPMetadata.from_input(ip_metadata).get_validated_data(), + ( + validate_address(recipient) + if recipient is not None + else self.account.address + ), + allow_duplicates, + tx_options=tx_options, + ) + ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"]) + return { + "tx_hash": response["tx_hash"], + "ip_id": ip_registered["ip_id"], + "token_id": ip_registered["token_id"], + } + except Exception as e: + raise e + def _validate_max_rts(self, max_rts: int): """ Validates the maximum number of royalty tokens. diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py new file mode 100644 index 00000000..feb67c1c --- /dev/null +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -0,0 +1,18 @@ +from typing import Optional, TypedDict + +from ens.ens import Address, HexStr + + +class RegistrationResponse(TypedDict): + """ + Response structure for IP asset registration operations. + + Attributes: + ip_id: The IP ID of the registered IP asset + tx_hash: The transaction hash of the registration transaction + token_id: [Optional] The token ID of the registered IP asset + """ + + ip_id: Address + tx_hash: HexStr + token_id: Optional[int] diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9ed11c26..df29ef69 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,8 +1,12 @@ import pytest from story_protocol_python_sdk.story_client import StoryClient +from story_protocol_python_sdk.utils.constants import ( + ROYALTY_POLICY_LAP_ADDRESS, + ZERO_ADDRESS, +) from tests.integration.config.test_config import account, account_2, web3 -from tests.integration.config.utils import get_story_client +from tests.integration.config.utils import MockERC20, get_story_client @pytest.fixture(scope="session") @@ -15,3 +19,63 @@ def story_client() -> StoryClient: def story_client_2() -> StoryClient: """Fixture to provide the secondary story client""" return get_story_client(web3, account_2) + + +@pytest.fixture(scope="module") +def nft_collection(story_client: StoryClient): + """Fixture to provide the SPG NFT collection""" + tx_data = story_client.NFTClient.create_nft_collection( + name="test-collection", + symbol="TEST", + max_supply=100, + is_public_minting=True, + mint_open=True, + contract_uri="test-uri", + mint_fee_recipient=account.address, + ) + return tx_data["nft_contract"] + + +@pytest.fixture(scope="module") +def parent_ip_and_license_terms(story_client: StoryClient, nft_collection): + """Fixture to provide the parent IP and license terms""" + response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( + spg_nft_contract=nft_collection, + terms=[ + { + "terms": { + "transferable": True, + "royalty_policy": ROYALTY_POLICY_LAP_ADDRESS, + "default_minting_fee": 0, + "expiration": 0, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 50, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": MockERC20, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 0, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, + } + ], + ) + return { + "parent_ip_id": response["ip_id"], + "license_terms_id": response["license_terms_ids"][0], + } diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 26dd29de..6091175d 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -5,6 +5,7 @@ from story_protocol_python_sdk.story_client import StoryClient from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput from story_protocol_python_sdk.utils.ip_metadata import IPMetadataInput +from tests.integration.config.test_config import account_2 from .setup_for_integration import ( PIL_LICENSE_TEMPLATE, @@ -254,62 +255,6 @@ def test_mint_register_ip(self, story_client: StoryClient, nft_collection): class TestSPGNFTOperations: - @pytest.fixture(scope="module") - def nft_collection(self, story_client: StoryClient): - tx_data = story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - max_supply=100, - is_public_minting=True, - mint_open=True, - contract_uri="test-uri", - mint_fee_recipient=account.address, - ) - return tx_data["nft_contract"] - - @pytest.fixture(scope="module") - def parent_ip_and_license_terms(self, story_client: StoryClient, nft_collection): - response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( - spg_nft_contract=nft_collection, - terms=[ - { - "terms": { - "transferable": True, - "royalty_policy": ROYALTY_POLICY, - "default_minting_fee": 0, - "expiration": 0, - "commercial_use": True, - "commercial_attribution": False, - "commercializer_checker": ZERO_ADDRESS, - "commercializer_checker_data": ZERO_ADDRESS, - "commercial_rev_share": 50, - "commercial_rev_ceiling": 0, - "derivatives_allowed": True, - "derivatives_attribution": True, - "derivatives_approval": False, - "derivatives_reciprocal": True, - "derivative_rev_ceiling": 0, - "currency": MockERC20, - "uri": "", - }, - "licensing_config": { - "is_set": True, - "minting_fee": 0, - "hook_data": ZERO_ADDRESS, - "licensing_hook": ZERO_ADDRESS, - "commercial_rev_share": 0, - "disabled": False, - "expect_minimum_group_reward_share": 0, - "expect_group_reward_pool": ZERO_ADDRESS, - }, - } - ], - ) - - return { - "parent_ip_id": response["ip_id"], - "license_terms_id": response["license_terms_ids"][0], - } # def test_register_ip_asset_with_metadata(self, story_client, nft_collection): # token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account, "test-metadata") @@ -693,3 +638,46 @@ def test_mint_with_existing_metadata_hash_no_duplicates( metadata_hash=metadata_hash, allow_duplicates=False, ) + + +class TestMintAndRegisterIpAndMakeDerivative: + def test_default_value( + self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms + ): + response = story_client.IPAsset.mint_and_register_ip_and_make_derivative( + spg_nft_contract=nft_collection, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms["parent_ip_id"]], + license_terms_ids=[parent_ip_and_license_terms["license_terms_id"]], + ), + ) + assert response is not None + assert isinstance(response["tx_hash"], str) + assert isinstance(response["ip_id"], str) + assert isinstance(response["token_id"], int) + + def test_with_custom_value( + self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms + ): + response = story_client.IPAsset.mint_and_register_ip_and_make_derivative( + spg_nft_contract=nft_collection, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms["parent_ip_id"]], + license_terms_ids=[parent_ip_and_license_terms["license_terms_id"]], + max_minting_fee=10000, + max_rts=10, + max_revenue_share=100, + ), + ip_metadata=IPMetadataInput( + ip_metadata_uri="https://example.com/metadata/custom-value.json", + ip_metadata_hash=web3.keccak(text="custom-value-metadata"), + nft_metadata_uri="https://example.com/metadata/custom-value.json", + nft_metadata_hash=web3.keccak(text="custom-value-metadata"), + ), + recipient=account_2.address, + allow_duplicates=False, + ) + assert response is not None + assert isinstance(response["tx_hash"], str) + assert isinstance(response["ip_id"], str) + assert isinstance(response["token_id"], int) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 45c7e02e..b487d5d2 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -1,10 +1,13 @@ from unittest.mock import patch import pytest +from ens.ens import HexStr from story_protocol_python_sdk.resources.IPAsset import IPAsset from story_protocol_python_sdk.utils.constants import ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput +from story_protocol_python_sdk.utils.ip_metadata import IPMetadataInput +from tests.integration.config.utils import ZERO_ADDRESS from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH @@ -39,7 +42,9 @@ def _mock(is_registered: bool = False): def mock_parse_ip_registered_event(ip_asset): def _mock(): return patch.object( - ip_asset, "_parse_tx_ip_registered_event", return_value={"ip_id": IP_ID} + ip_asset, + "_parse_tx_ip_registered_event", + return_value={"ip_id": IP_ID, "token_id": 3}, ) return _mock @@ -172,3 +177,114 @@ def test_success( ) assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID + + +class TestMintAndRegisterIpAndMakeDerivative: + def test_throw_error_when_spg_nft_contract_is_invalid( + self, ip_asset, mock_license_registry_client + ): + with mock_license_registry_client(): + with pytest.raises(ValueError, match="Invalid address: invalid."): + ip_asset.mint_and_register_ip_and_make_derivative( + spg_nft_contract="invalid", + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + ), + ) + + def test_success_and_expect_value_when_default_values_not_provided( + self, + ip_asset: IPAsset, + mock_license_registry_client, + mock_parse_ip_registered_event, + ): + with mock_parse_ip_registered_event(), mock_license_registry_client(): + with patch.object( + ip_asset.derivative_workflows_client, + "build_mintAndRegisterIpAndMakeDerivative_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_transaction: + result = ip_asset.mint_and_register_ip_and_make_derivative( + spg_nft_contract=ADDRESS, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + ), + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert mock_build_transaction.call_args[0][1] == { + "parentIpIds": [IP_ID, IP_ID], + "licenseTermsIds": [1, 2], + "maxMintingFee": 0, + "maxRts": 100 * 10**6, + "maxRevenueShare": 100 * 10**6, + "royaltyContext": ZERO_ADDRESS, + "licenseTemplate": "0x1234567890123456789012345678901234567890", + } + assert mock_build_transaction.call_args[0][2] == { + "ipMetadataURI": "", + "ipMetadataHash": ZERO_HASH, + "nftMetadataURI": "", + "nftMetadataHash": ZERO_HASH, + } + assert ( + mock_build_transaction.call_args[0][3] + == "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" + ) # recipient + assert mock_build_transaction.call_args[0][4] # allowDuplicates + + def test_with_custom_value( + self, + ip_asset: IPAsset, + mock_license_registry_client, + mock_parse_ip_registered_event, + ): + with mock_parse_ip_registered_event(), mock_license_registry_client(): + with patch.object( + ip_asset.derivative_workflows_client, + "build_mintAndRegisterIpAndMakeDerivative_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_transaction: + result = ip_asset.mint_and_register_ip_and_make_derivative( + spg_nft_contract=ADDRESS, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + max_minting_fee=10, + max_rts=100, + max_revenue_share=10, + license_template=ADDRESS, + ), + ip_metadata=IPMetadataInput( + ip_metadata_uri="https://example.com/metadata/custom-value.json", + ip_metadata_hash=HexStr("ip_metadata_hash"), + nft_metadata_uri="https://example.com/metadata/custom-value.json", + nft_metadata_hash=HexStr("nft_metadata_hash"), + ), + recipient=ADDRESS, + allow_duplicates=False, + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + + assert mock_build_transaction.call_args[0][1] == { + "parentIpIds": [IP_ID, IP_ID], + "licenseTermsIds": [1, 2], + "maxMintingFee": 10, + "maxRts": 100, + "maxRevenueShare": 10 * 10**6, + "royaltyContext": ZERO_ADDRESS, + "licenseTemplate": ADDRESS, + } + assert mock_build_transaction.call_args[0][2] == { + "ipMetadataURI": "https://example.com/metadata/custom-value.json", + "ipMetadataHash": "ip_metadata_hash", + "nftMetadataURI": "https://example.com/metadata/custom-value.json", + "nftMetadataHash": "nft_metadata_hash", + } + assert mock_build_transaction.call_args[0][3] == ADDRESS # recipient + assert not mock_build_transaction.call_args[0][4] # allowDuplicates From ba4d73dc76c83edbb7b991bc3d935df7ca3b2b4f Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 15 Aug 2025 15:54:05 +0800 Subject: [PATCH 3/3] Fix merge conflict --- .../resources/IPAsset.py | 2 +- tests/unit/resources/test_ip_asset.py | 256 +++++++++++++++++- 2 files changed, 255 insertions(+), 3 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index c6129d50..86155419 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -293,7 +293,7 @@ def register_derivative( return {"tx_hash": response["tx_hash"]} except Exception as e: - raise ValueError("Failed to register derivative") from e + raise ValueError(f"Failed to register derivative: {str(e)}") from e def register_derivative_with_license_tokens( self, diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index b487d5d2..6a888aa1 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -6,9 +6,16 @@ from story_protocol_python_sdk.resources.IPAsset import IPAsset from story_protocol_python_sdk.utils.constants import ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput -from story_protocol_python_sdk.utils.ip_metadata import IPMetadataInput +from story_protocol_python_sdk.utils.ip_metadata import IPMetadata, IPMetadataInput from tests.integration.config.utils import ZERO_ADDRESS -from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH +from tests.unit.fixtures.data import ( + ADDRESS, + CHAIN_ID, + IP_ID, + LICENSE_TERMS, + LICENSING_CONFIG, + TX_HASH, +) @pytest.fixture(scope="class") @@ -61,6 +68,18 @@ def _mock(): return _mock +@pytest.fixture(scope="class") +def mock_parse_tx_license_terms_attached_event(ip_asset): + def _mock(): + return patch.object( + ip_asset, + "_parse_tx_license_terms_attached_event", + return_value=[1, 2], + ) + + return _mock + + class TestIPAssetRegister: def test_register_invalid_deadline_type( self, ip_asset, mock_get_ip_id, mock_is_registered @@ -179,6 +198,239 @@ def test_success( assert result["ip_id"] == IP_ID +class TestMint: + def test_mint_successful(self, ip_asset): + result = ip_asset.mint( + nft_contract=ADDRESS, + to_address=ADDRESS, + metadata_uri="", + metadata_hash=ZERO_HASH, + ) + assert result == f"0x{TX_HASH.hex()}" + + def test_mint_failed_transaction(self, ip_asset): + with patch.object(ip_asset.web3.eth, "send_raw_transaction") as mock_send: + mock_send.side_effect = Exception("Transaction failed") + with pytest.raises(Exception, match="Transaction failed"): + ip_asset.mint( + nft_contract=ADDRESS, + to_address=ADDRESS, + metadata_uri="", + metadata_hash=ZERO_HASH, + allow_duplicates=False, + ) + + +class TestRegisterIpAndAttachPilTerms: + def test_token_id_is_already_registered( + self, ip_asset, mock_get_ip_id, mock_is_registered + ): + with mock_get_ip_id(), mock_is_registered(True): + with pytest.raises( + ValueError, match="The NFT with id 3 is already registered as IP." + ): + ip_asset.register_ip_and_attach_pil_terms( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[], + ) + + def test_royalty_policy_commercial_rev_share_is_less_than_0( + self, ip_asset, mock_get_ip_id, mock_is_registered + ): + with mock_get_ip_id(), mock_is_registered(): + with pytest.raises( + ValueError, match="commercial_rev_share should be between 0 and 100." + ): + ip_asset.register_ip_and_attach_pil_terms( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[ + { + "terms": { + **LICENSE_TERMS, + "commercial_rev_share": -1, + }, + } + ], + ) + + def test_transaction_to_be_called_with_correct_parameters( + self, + ip_asset: IPAsset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + mock_parse_tx_license_terms_attached_event, + mock_signature_related_methods, + ): + with mock_get_ip_id(), mock_is_registered(), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), mock_signature_related_methods(): + with patch.object( + ip_asset.license_attachment_workflows_client, + "build_registerIpAndAttachPILTerms_transaction", + ) as mock_build_registerIpAndAttachPILTerms_transaction: + + ip_asset.register_ip_and_attach_pil_terms( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[ + { + "terms": LICENSE_TERMS, + "licensing_config": LICENSING_CONFIG, + } + ], + ) + call_args = mock_build_registerIpAndAttachPILTerms_transaction.call_args[0] + assert call_args[0] == ADDRESS + assert call_args[1] == 3 + assert call_args[2] == IPMetadata.from_input().get_validated_data() + assert call_args[3] == [ + { + "terms": { + "transferable": True, + "royaltyPolicy": "0x1234567890123456789012345678901234567890", + "defaultMintingFee": 10, + "expiration": 100, + "commercialUse": True, + "commercialAttribution": True, + "commercializerChecker": True, + "commercializerCheckerData": b"mock_bytes", + "commercialRevShare": 19000000, + "commercialRevCeiling": 0, + "derivativesAllowed": True, + "derivativesAttribution": True, + "derivativesApproval": True, + "derivativesReciprocal": True, + "derivativeRevCeiling": 100, + "currency": "0x1234567890123456789012345678901234567890", + "uri": "https://example.com", + }, + "licensingConfig": { + "isSet": True, + "mintingFee": 10, + "hookData": b"mock_bytes", + "licensingHook": "0x1234567890123456789012345678901234567890", + "commercialRevShare": 10000000, + "disabled": False, + "expectMinimumGroupRewardShare": 10000000, + "expectGroupRewardPool": "0x1234567890123456789012345678901234567890", + }, + } + ] + + def test_success( + self, + ip_asset: IPAsset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + mock_signature_related_methods, + mock_parse_tx_license_terms_attached_event, + ): + with mock_get_ip_id(), mock_is_registered(), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), mock_signature_related_methods(): + result = ip_asset.register_ip_and_attach_pil_terms( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[ + { + "terms": LICENSE_TERMS, + "licensing_config": LICENSING_CONFIG, + } + ], + ip_metadata={ + "ip_metadata_uri": "https://example.com/metadata/custom-value.json", + "ip_metadata_hash": "ip_metadata_hash", + "nft_metadata_uri": "https://example.com/metadata/custom-value.json", + "nft_metadata_hash": "nft_metadata_hash", + }, + ) + assert result == { + "tx_hash": TX_HASH.hex(), + "ip_id": IP_ID, + "license_terms_ids": [1, 2], + "token_id": 3, + } + + +class TestRegisterDerivative: + def test_child_ip_is_not_registered( + self, ip_asset: IPAsset, mock_get_ip_id, mock_is_registered + ): + with mock_get_ip_id(), mock_is_registered(False): + with pytest.raises( + ValueError, + match=f"Failed to register derivative: The child IP with id {IP_ID} is not registered", + ): + ip_asset.register_derivative( + child_ip_id=IP_ID, + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + ) + + def test_default_value_when_not_provided( + self, + ip_asset: IPAsset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + mock_license_registry_client, + ): + with mock_get_ip_id(), mock_is_registered( + True + ), mock_parse_ip_registered_event(), mock_license_registry_client(): + with patch.object( + ip_asset.licensing_module_client, + "build_registerDerivative_transaction", + ) as mock_build_registerDerivative_transaction: + + ip_asset.register_derivative( + child_ip_id=IP_ID, + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + ) + call_args = mock_build_registerDerivative_transaction.call_args[0] + print(call_args) + assert ( + call_args[3] == "0x1234567890123456789012345678901234567890" + ) # license_template + assert ( + call_args[4] == "0x0000000000000000000000000000000000000000" + ) # royalty_context + assert call_args[5] == 0 # max_minting_fee + assert call_args[6] == 100000000 # max_rts + assert call_args[7] == 100 * 10**6 # max_revenue_share + + def test_call_value_when_provided( + self, + ip_asset: IPAsset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + mock_license_registry_client, + ): + with mock_get_ip_id(), mock_is_registered( + True + ), mock_parse_ip_registered_event(), mock_license_registry_client(): + with patch.object( + ip_asset.licensing_module_client, + "build_registerDerivative_transaction", + ) as mock_build_registerDerivative_transaction: + ip_asset.register_derivative( + child_ip_id=IP_ID, + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + max_revenue_share=10, + max_minting_fee=10, + max_rts=100, + license_template=ADDRESS, + ) + call_args = mock_build_registerDerivative_transaction.call_args[0] + assert call_args[7] == 10 * 10**6 # max_revenue_share + assert call_args[5] == 10 # max_minting_fee + assert call_args[6] == 100 # max_rts + assert call_args[3] == ADDRESS # license_template + + class TestMintAndRegisterIpAndMakeDerivative: def test_throw_error_when_spg_nft_contract_is_invalid( self, ip_asset, mock_license_registry_client