From e99ba9afcc36c452feb9888dc9c3fe49a1b0b5c3 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 28 Nov 2025 15:42:39 +0800 Subject: [PATCH 1/8] Removing workflow_dispatch condition --- .github/workflows/pr-internal.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-internal.yaml b/.github/workflows/pr-internal.yaml index 77b1629..40f1266 100644 --- a/.github/workflows/pr-internal.yaml +++ b/.github/workflows/pr-internal.yaml @@ -12,7 +12,7 @@ on: jobs: Timestamp: - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} uses: storyprotocol/gha-workflows/.github/workflows/reusable-timestamp.yml@main tests: From 3aeb1a12c2bd485858e67240925fb3288d7c5b2b Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 1 Dec 2025 14:10:05 +0800 Subject: [PATCH 2/8] feat: add support for registering IP assets with minted and mint-on-demand NFTs --- src/story_protocol_python_sdk/__init__.py | 6 + .../resources/IPAsset.py | 195 ++++++++++++++++++ .../types/resource/IPAsset.py | 59 +++++- 3 files changed, 259 insertions(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 68c6b8a..22f7b5c 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -17,9 +17,12 @@ BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, LicenseTermsDataInput, + MintedNFT, + MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse, RegisteredIP, + RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, @@ -69,6 +72,9 @@ "RegisterPILTermsAndAttachResponse", "RoyaltyShareInput", "LicenseTermsInput", + "MintNFT", + "MintedNFT", + "RegisterIpAssetResponse", # 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 c307c6b..b681c70 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -54,9 +54,12 @@ BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, LicenseTermsDataInput, + MintedNFT, + MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse, RegisteredIP, + RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, @@ -1455,6 +1458,198 @@ def register_pil_terms_and_attach( except Exception as e: raise e + def register_ip_asset( + self, + nft: MintNFT | MintedNFT, + license_terms_data: list[LicenseTermsDataInput] | None = None, + royalty_shares: list[RoyaltyShareInput] | None = None, + ip_metadata: IPMetadataInput | None = None, + deadline: int | None = None, + tx_options: dict | None = None, + ) -> RegisterIpAssetResponse: + """ + Register an IP asset, supporting both minted and mint-on-demand NFTs, + with optional license terms and royalty shares. + + This method automatically selects and calls the appropriate workflow from 6 available + methods based on your input parameters: + + For already minted NFT (type="minted"): + - With `license_terms_data` + `royalty_shares`: `register_ip_and_attach_pil_terms_and_distribute_royalty_tokens` + - With `license_terms_data` only: `register_ip_and_attach_pil_terms` + - Basic registration: `register` + + For new minted NFT (type="mint"): + - With `license_terms_data` + `royalty_shares`: `mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens` + - With license_terms_data only: `mint_and_register_ip_asset_with_pil_terms` + - Basic registration: `mint_and_register_ip` + + :param nft `MintNFT` | `MintedNFT`: The NFT to be registered as an IP asset. + - For `MintNFT`: Mint a new NFT from an SPG NFT contract. + - For `MintedNFT`: Register an already minted NFT. + :param license_terms_data `list[LicenseTermsDataInput]`: [Optional] License terms and configuration to be attached to the IP asset. + :param royalty_shares `list[RoyaltyShareInput]`: [Optional] Authors of the IP and their shares of the royalty tokens. + Can only be specified when `license_terms_data` is also provided. + :param ip_metadata `IPMetadataInput`: [Optional] The desired metadata for the newly registered IP. + :param deadline int: [Optional] Signature deadline in seconds. (default: 1000 seconds) + :param tx_options dict: [Optional] Transaction options. + :return `RegisterIpAssetResponse`: Response with transaction hash, IP ID, token ID, and optionally license terms IDs, royalty vault, and distribute royalty tokens transaction hash. + :raises ValueError: If `royalty_shares` is provided without `license_terms_data`. + :raises ValueError: If the `NFT` type is invalid. + """ + try: + # Validate `royalty_shares` without `license_terms_data` + if royalty_shares and not license_terms_data: + raise ValueError( + "License terms data must be provided when royalty shares are specified." + ) + + if nft.type == "minted": + return self._handle_minted_nft_registration( + nft=nft, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + deadline=deadline, + tx_options=tx_options, + ) + elif nft.type == "mint": + return self._handle_mint_nft_registration( + nft=nft, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + tx_options=tx_options, + ) + + except Exception as e: + raise ValueError(f"Failed to register IP Asset: {str(e)}") from e + + def _handle_minted_nft_registration( + self, + nft: MintedNFT, + license_terms_data: list[LicenseTermsDataInput] | None, + royalty_shares: list[RoyaltyShareInput] | None, + ip_metadata: IPMetadataInput | None, + deadline: int | None, + tx_options: dict | None, + ) -> RegisterIpAssetResponse: + """ + Handle registration for already minted NFTs with optional license terms and royalty shares. + """ + # Convert ip_metadata to dict format for methods that expect dict + ip_metadata_dict = IPMetadata.from_input(ip_metadata).get_validated_data() + if license_terms_data and royalty_shares: + royalty_result = ( + self.register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + nft_contract=nft.nft_contract, + token_id=nft.token_id, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + deadline=deadline, + tx_options=tx_options, + ) + ) + return RegisterIpAssetResponse( + tx_hash=royalty_result["tx_hash"], + ip_id=royalty_result["ip_id"], + token_id=royalty_result["token_id"], + license_terms_ids=royalty_result["license_terms_ids"], + royalty_vault=royalty_result["royalty_vault"], + distribute_royalty_tokens_tx_hash=royalty_result[ + "distribute_royalty_tokens_tx_hash" + ], + ) + + if license_terms_data: + terms_result = self.register_ip_and_attach_pil_terms( + nft_contract=nft.nft_contract, + token_id=nft.token_id, + license_terms_data=license_terms_data, + ip_metadata=ip_metadata_dict, + deadline=deadline, + tx_options=tx_options, + ) + return RegisterIpAssetResponse( + tx_hash=terms_result["tx_hash"], + ip_id=terms_result["ip_id"], + token_id=terms_result["token_id"], + license_terms_ids=terms_result["license_terms_ids"], + ) + + basic_result = self.register( + nft_contract=nft.nft_contract, + token_id=nft.token_id, + ip_metadata=ip_metadata_dict, + deadline=deadline, + tx_options=tx_options, + ) + return RegisterIpAssetResponse( + tx_hash=basic_result["tx_hash"], + ip_id=basic_result["ip_id"], + ) + + def _handle_mint_nft_registration( + self, + nft: MintNFT, + license_terms_data: list[LicenseTermsDataInput] | None, + royalty_shares: list[RoyaltyShareInput] | None, + ip_metadata: IPMetadataInput | None, + tx_options: dict | None, + ) -> RegisterIpAssetResponse: + """ + Handle minting and registration of new NFTs with optional license terms and royalty shares. + """ + ip_metadata_dict = IPMetadata.from_input(ip_metadata).get_validated_data() + + if license_terms_data and royalty_shares: + royalty_result = self.mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + spg_nft_contract=nft.spg_nft_contract, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + recipient=nft.recipient, + allow_duplicates=nft.allow_duplicates, + tx_options=tx_options, + ) + return RegisterIpAssetResponse( + tx_hash=royalty_result["tx_hash"], + ip_id=royalty_result["ip_id"], + token_id=royalty_result["token_id"], + license_terms_ids=royalty_result["license_terms_ids"], + royalty_vault=royalty_result["royalty_vault"], + ) + + if license_terms_data: + terms_result = self.mint_and_register_ip_asset_with_pil_terms( + spg_nft_contract=nft.spg_nft_contract, + terms=license_terms_data, + ip_metadata=ip_metadata_dict, + recipient=nft.recipient, + allow_duplicates=nft.allow_duplicates, + tx_options=tx_options, + ) + return RegisterIpAssetResponse( + tx_hash=terms_result["tx_hash"], + ip_id=terms_result["ip_id"], + token_id=terms_result["token_id"], + license_terms_ids=terms_result["license_terms_ids"], + ) + + basic_result = self.mint_and_register_ip( + spg_nft_contract=nft.spg_nft_contract, + recipient=nft.recipient, + ip_metadata=ip_metadata_dict, + allow_duplicates=nft.allow_duplicates, + tx_options=tx_options, + ) + return RegisterIpAssetResponse( + tx_hash=basic_result["tx_hash"], + ip_id=basic_result["ip_id"], + token_id=basic_result["token_id"], + ) + def _validate_derivative_data(self, derivative_data: dict) -> dict: """ Validates the derivative data and returns processed internal data. diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index c325f92..924c5be 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TypedDict +from typing import Literal, TypedDict from ens.ens import Address, HexStr @@ -149,3 +149,60 @@ class BatchMintAndRegisterIPResponse(TypedDict): tx_hash: HexStr registered_ips: list[RegisteredIP] + + +@dataclass +class MintNFT: + """ + Configuration for minting a new NFT from an SPG NFT contract. + + Attributes: + type: Must be "mint" to indicate a new NFT will be minted. + spg_nft_contract: The address of the SPG NFT contract. + You can create one via `client.nft_client.create_nft_collection`. + recipient: [Optional] The address to receive the NFT. Defaults to caller's wallet address. + allow_duplicates: [Optional] Set to true to allow minting an NFT with a duplicate metadata hash. (default: True) + """ + + type: Literal["mint"] + spg_nft_contract: Address + recipient: Address | None = None + allow_duplicates: bool = True + + +@dataclass +class MintedNFT: + """ + Configuration for registering an already minted NFT as an IP asset. + + Attributes: + type: Must be "minted" to indicate an existing NFT. + nft_contract: The address of the NFT contract. + token_id: The token ID of the NFT. + """ + + type: Literal["minted"] + nft_contract: Address + token_id: int + + +class RegisterIpAssetResponse(TypedDict, total=False): + """ + Response structure for unified IP asset registration. + Fields vary based on the registration method used. + + Attributes: + tx_hash: The transaction hash of the registration transaction. + ip_id: The IP ID of the registered IP asset. + token_id: The token ID of the registered IP asset. + license_terms_ids: [Optional] The IDs of the license terms attached to the IP asset. + royalty_vault: [Optional] The royalty vault address of the registered IP asset. + distribute_royalty_tokens_tx_hash: [Optional] The transaction hash of the distribute royalty tokens transaction. + """ + + tx_hash: HexStr + ip_id: Address + token_id: int + license_terms_ids: list[int] + royalty_vault: Address + distribute_royalty_tokens_tx_hash: HexStr From 15571892fc4aafd01446a0a14ce01647d281d4e1 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 1 Dec 2025 16:10:43 +0800 Subject: [PATCH 3/8] feat: add comprehensive tests for registering IP assets with minted and mint-on-demand NFTs, including scenarios for license terms and royalty shares --- .../integration/test_integration_ip_asset.py | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 8ad6547..0f6a432 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -9,6 +9,8 @@ LicenseTermsDataInput, LicenseTermsInput, LicensingConfig, + MintedNFT, + MintNFT, RoyaltyShareInput, StoryClient, ) @@ -1153,3 +1155,315 @@ def test_batch_mint_and_register_ip( for ip_registered in response["registered_ips"]: assert isinstance(ip_registered["ip_id"], str) and ip_registered["ip_id"] assert isinstance(ip_registered["token_id"], int) + + +class TestRegisterIpAsset: + """Test suite for the unified register_ip_asset method that supports 6 different workflows""" + + @pytest.fixture(scope="class") + def test_register_ip_asset_minted_basic( + self, story_client: StoryClient, nft_collection + ): + """Test basic registration for already minted NFT (uses register internally)""" + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + response = story_client.IPAsset.register_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + ip_metadata=COMMON_IP_METADATA, + deadline=1000, + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + + def test_register_ip_asset_minted_with_license_terms( + self, story_client: StoryClient, nft_collection + ): + """Test registration with license terms for already minted NFT (uses register_ip_and_attach_pil_terms internally)""" + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + response = story_client.IPAsset.register_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + license_terms_data=[ + LicenseTermsDataInput( + terms=LicenseTermsInput( + transferable=True, + royalty_policy=ROYALTY_POLICY, + default_minting_fee=10000, + expiration=1000, + commercial_use=True, + commercial_attribution=False, + commercializer_checker=ZERO_ADDRESS, + commercializer_checker_data=ZERO_HASH, + commercial_rev_share=10, + commercial_rev_ceiling=0, + derivatives_allowed=True, + derivatives_attribution=True, + derivatives_approval=False, + derivatives_reciprocal=True, + derivative_rev_ceiling=0, + currency=WIP_TOKEN_ADDRESS, + uri="test-minted-license-terms", + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=10, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + ], + ip_metadata=COMMON_IP_METADATA, + deadline=1000, + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + assert ( + isinstance(response["token_id"], int) and response["token_id"] == token_id + ) + assert ( + isinstance(response["license_terms_ids"], list) + and len(response["license_terms_ids"]) > 0 + ) + + def test_register_ip_asset_minted_with_license_terms_and_royalty_shares( + self, story_client: StoryClient, nft_collection + ): + """Test registration with license terms and royalty shares for already minted NFT + (uses register_ip_and_attach_pil_terms_and_distribute_royalty_tokens internally) + """ + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + royalty_shares = [ + RoyaltyShareInput(recipient=account.address, percentage=40.0), + RoyaltyShareInput(recipient=account_2.address, percentage=60.0), + ] + + response = story_client.IPAsset.register_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + license_terms_data=[ + LicenseTermsDataInput( + terms=LicenseTermsInput( + transferable=True, + royalty_policy=ROYALTY_POLICY, + default_minting_fee=10000, + expiration=1000, + commercial_use=True, + commercial_attribution=False, + commercializer_checker=ZERO_ADDRESS, + commercializer_checker_data=ZERO_HASH, + commercial_rev_share=10, + commercial_rev_ceiling=0, + derivatives_allowed=True, + derivatives_attribution=True, + derivatives_approval=False, + derivatives_reciprocal=True, + derivative_rev_ceiling=0, + currency=WIP_TOKEN_ADDRESS, + uri="test-minted-license-terms-with-royalty", + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=10, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + ], + royalty_shares=royalty_shares, + ip_metadata=COMMON_IP_METADATA, + deadline=1000, + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + assert ( + isinstance(response["token_id"], int) and response["token_id"] == token_id + ) + assert ( + isinstance(response["license_terms_ids"], list) + and len(response["license_terms_ids"]) > 0 + ) + assert isinstance(response["royalty_vault"], str) and response["royalty_vault"] + assert ( + isinstance(response["distribute_royalty_tokens_tx_hash"], str) + and response["distribute_royalty_tokens_tx_hash"] + ) + + def test_register_ip_asset_mint_basic( + self, story_client: StoryClient, nft_collection + ): + """Test basic registration with mint (uses mint_and_register_ip internally)""" + response = story_client.IPAsset.register_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + recipient=account_2.address, + allow_duplicates=True, + ), + ip_metadata=COMMON_IP_METADATA, + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + assert isinstance(response["token_id"], int) + + def test_register_ip_asset_mint_with_license_terms( + self, story_client: StoryClient, nft_collection + ): + """Test registration with mint and license terms (uses mint_and_register_ip_asset_with_pil_terms internally)""" + response = story_client.IPAsset.register_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + recipient=account_2.address, + allow_duplicates=True, + ), + license_terms_data=[ + LicenseTermsDataInput( + terms=LicenseTermsInput( + transferable=True, + royalty_policy=ROYALTY_POLICY, + default_minting_fee=10000, + expiration=1000, + commercial_use=True, + commercial_attribution=False, + commercializer_checker=ZERO_ADDRESS, + commercializer_checker_data=ZERO_HASH, + commercial_rev_share=10, + commercial_rev_ceiling=0, + derivatives_allowed=True, + derivatives_attribution=True, + derivatives_approval=False, + derivatives_reciprocal=True, + derivative_rev_ceiling=0, + currency=WIP_TOKEN_ADDRESS, + uri="test-mint-license-terms", + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=10, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + ], + ip_metadata=COMMON_IP_METADATA, + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + assert isinstance(response["token_id"], int) + assert ( + isinstance(response["license_terms_ids"], list) + and len(response["license_terms_ids"]) > 0 + ) + + def test_register_ip_asset_mint_with_license_terms_and_royalty_shares( + self, story_client: StoryClient, nft_collection + ): + """Test registration with mint, license terms and royalty shares + (uses mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens internally) + """ + royalty_shares = [ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ] + + response = story_client.IPAsset.register_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + recipient=account_2.address, + allow_duplicates=True, + ), + license_terms_data=[ + LicenseTermsDataInput( + terms=LicenseTermsInput( + transferable=True, + royalty_policy=ROYALTY_POLICY, + default_minting_fee=10000, + expiration=1000, + commercial_use=True, + commercial_attribution=False, + commercializer_checker=ZERO_ADDRESS, + commercializer_checker_data=ZERO_HASH, + commercial_rev_share=10, + commercial_rev_ceiling=0, + derivatives_allowed=True, + derivatives_attribution=True, + derivatives_approval=False, + derivatives_reciprocal=True, + derivative_rev_ceiling=0, + currency=WIP_TOKEN_ADDRESS, + uri="test-mint-license-terms-with-royalty", + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=10, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + ], + royalty_shares=royalty_shares, + ip_metadata=COMMON_IP_METADATA, + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + assert isinstance(response["token_id"], int) + assert ( + isinstance(response["license_terms_ids"], list) + and len(response["license_terms_ids"]) > 0 + ) + assert isinstance(response["royalty_vault"], str) and response["royalty_vault"] + + def test_register_ip_asset_royalty_shares_without_license_terms_error( + self, story_client: StoryClient, nft_collection + ): + """Test error case when royalty_shares is provided without license_terms_data""" + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + with pytest.raises(ValueError) as exc_info: + story_client.IPAsset.register_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=100.0), + ], + ) + + assert "License terms data must be provided" in str(exc_info.value) From 05dd3486828301eb30a8df5e7f07280a444b8d3d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Dec 2025 15:28:46 +0800 Subject: [PATCH 4/8] feat: Add the unit tests --- .../resources/IPAsset.py | 20 +- .../utils/ip_metadata.py | 53 +++ tests/unit/resources/test_ip_asset.py | 317 +++++++++++++++++- 3 files changed, 380 insertions(+), 10 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index b681c70..8c9d922 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -76,7 +76,12 @@ DerivativeDataInput, ) from story_protocol_python_sdk.utils.function_signature import get_function_signature -from story_protocol_python_sdk.utils.ip_metadata import IPMetadata, IPMetadataInput +from story_protocol_python_sdk.utils.ip_metadata import ( + IPMetadata, + IPMetadataInput, + get_ip_metadata_dict, + is_initial_ip_metadata, +) from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.royalty import get_royalty_shares from story_protocol_python_sdk.utils.sign import Sign @@ -200,7 +205,7 @@ def register( }, } - if ip_metadata: + if not is_initial_ip_metadata(ip_metadata) and ip_metadata: req_object["ipMetadata"].update( { "ipMetadataURI": ip_metadata.get("ip_metadata_uri", ""), @@ -448,7 +453,6 @@ def mint_and_register_ip_asset_with_pil_terms( f"The NFT contract address {spg_nft_contract} is not valid." ) license_terms = self._validate_license_terms_data(terms) - metadata = { "ipMetadataURI": "", "ipMetadataHash": ZERO_HASH, @@ -1537,8 +1541,9 @@ def _handle_minted_nft_registration( """ Handle registration for already minted NFTs with optional license terms and royalty shares. """ - # Convert ip_metadata to dict format for methods that expect dict - ip_metadata_dict = IPMetadata.from_input(ip_metadata).get_validated_data() + # In order to compatible with the previous version, we need to convert the ip_metadata to dict format for methods that expect dict + # We can remove this function after these method become the internal methods. + ip_metadata_dict = get_ip_metadata_dict(ip_metadata) if license_terms_data and royalty_shares: royalty_result = ( self.register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( @@ -1601,8 +1606,9 @@ def _handle_mint_nft_registration( """ Handle minting and registration of new NFTs with optional license terms and royalty shares. """ - ip_metadata_dict = IPMetadata.from_input(ip_metadata).get_validated_data() - + # In order to compatible with the previous version, we need to convert the ip_metadata to dict format for methods that expect dict. + # We can remove this function after these method become the internal methods. + ip_metadata_dict = get_ip_metadata_dict(ip_metadata) if license_terms_data and royalty_shares: royalty_result = self.mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( spg_nft_contract=nft.spg_nft_contract, diff --git a/src/story_protocol_python_sdk/utils/ip_metadata.py b/src/story_protocol_python_sdk/utils/ip_metadata.py index 4242d79..d4e62c2 100644 --- a/src/story_protocol_python_sdk/utils/ip_metadata.py +++ b/src/story_protocol_python_sdk/utils/ip_metadata.py @@ -77,3 +77,56 @@ def get_validated_data(self) -> dict: "nftMetadataURI": self.nft_metadata_uri, "nftMetadataHash": self.nft_metadata_hash, } + + +# In order to compatible with the previous version, we need to convert the ip_metadata to dict format for methods that expect dict. +# We can remove this function after these method become the internal methods. +def get_ip_metadata_dict(ip_metadata: IPMetadataInput | None = None) -> dict: + ip_metadata_dict = { + "ip_metadata_uri": "", + "ip_metadata_hash": ZERO_HASH, + "nft_metadata_uri": "", + "nft_metadata_hash": ZERO_HASH, + } + + if ip_metadata: + ip_metadata_dict.update( + { + "ip_metadata_uri": ( + ip_metadata.ip_metadata_uri + if ip_metadata.ip_metadata_uri is not None + else "" + ), + "ip_metadata_hash": ( + ip_metadata.ip_metadata_hash + if ip_metadata.ip_metadata_hash is not None + else ZERO_HASH + ), + "nft_metadata_uri": ( + ip_metadata.nft_metadata_uri + if ip_metadata.nft_metadata_uri is not None + else "" + ), + "nft_metadata_hash": ( + ip_metadata.nft_metadata_hash + if ip_metadata.nft_metadata_hash is not None + else ZERO_HASH + ), + } + ) + + return ip_metadata_dict + + +# In order to compatible with the previous version, we need to check if the ip_metadata is the initial ip metadata. +# We can move into the IPMetadata class after these method become the internal methods. +def is_initial_ip_metadata(ip_metadata: dict | None = None) -> bool: + if ip_metadata is None: + return True + + return ( + ip_metadata.get("ip_metadata_uri", "") == "" + and ip_metadata.get("ip_metadata_hash", ZERO_HASH) == ZERO_HASH + and ip_metadata.get("nft_metadata_uri", "") == "" + and ip_metadata.get("nft_metadata_hash", ZERO_HASH) == ZERO_HASH + ) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index d427825..d40899e 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -4,7 +4,7 @@ from ens.ens import HexStr from web3 import Web3 -from story_protocol_python_sdk import RoyaltyShareInput +from story_protocol_python_sdk import MintedNFT, MintNFT, RoyaltyShareInput from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( IPAccountImplClient, ) @@ -107,11 +107,11 @@ def _mock(): @pytest.fixture def mock_get_royalty_vault_address_by_ip_id(ip_asset): - def _mock(): + def _mock(royalty_vault=ADDRESS): return patch.object( ip_asset, "get_royalty_vault_address_by_ip_id", - return_value=ADDRESS, + return_value=royalty_vault, ) return _mock @@ -2070,3 +2070,314 @@ def test_batch_mint_transaction_success( assert result["registered_ips"][0]["ip_id"] == IP_ID assert result["registered_ips"][0]["token_id"] == 3 assert result["registered_ips"][1]["ip_id"] == ADDRESS + + +class TestRegisterIpAsset: + def test_throw_not_provided_license_terms_data_when_royalty_shares_provided_for_minted_nft( + self, ip_asset: IPAsset + ): + with pytest.raises( + ValueError, + match="License terms data must be provided when royalty shares are specified.", + ): + ip_asset.register_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0) + ], + ) + + def test_throw_not_provided_license_terms_data_when_royalty_shares_provided_for_mint_nft( + self, ip_asset: IPAsset + ): + with pytest.raises( + ValueError, + match="License terms data must be provided when royalty shares are specified.", + ): + ip_asset.register_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0) + ], + ) + + def test_success_when_license_terms_data_and_royalty_shares_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_is_registered, + mock_get_ip_id, + mock_signature_related_methods, + mock_parse_ip_registered_event, + mock_ip_account_impl_client, + mock_parse_tx_license_terms_attached_event, + mock_get_royalty_vault_address_by_ip_id, + ): + royalty_shares = [ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + ] + royalty_shares_obj = get_royalty_shares(royalty_shares) + royalty_vault = HexStr("0x" + "a" * 64) + with ( + mock_is_registered(is_registered=False), + mock_get_ip_id(), + mock_signature_related_methods(), + mock_parse_ip_registered_event(), + mock_ip_account_impl_client(), + mock_parse_tx_license_terms_attached_event(), + mock_get_royalty_vault_address_by_ip_id(royalty_vault), + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction", + return_value={ + "tx_hash": TX_HASH.hex(), + }, + ) as mock_build_register_transaction, + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_distributeRoyaltyTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_distribute, + patch.object( + ip_asset.sign_util, + "get_signature", + return_value={"signature": "signature"}, + ), + ): + result = ip_asset.register_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=royalty_shares, + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == 3 + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) + assert mock_distribute.call_args[0][0] == IP_ID + assert ( + mock_distribute.call_args[0][1] == royalty_shares_obj["royalty_shares"] + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["license_terms_ids"] is not None + assert result["royalty_vault"] == royalty_vault + assert result["distribute_royalty_tokens_tx_hash"] == TX_HASH.hex() + + def test_success_when_license_terms_data_and_royalty_shares_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_parse_tx_license_terms_attached_event, + mock_get_royalty_vault_address_by_ip_id, + ): + royalty_shares = [ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + ] + royalty_shares_obj = get_royalty_shares(royalty_shares) + royalty_vault = HexStr("0x" + "a" * 64) + with ( + mock_parse_ip_registered_event(), + mock_parse_tx_license_terms_attached_event(), + mock_get_royalty_vault_address_by_ip_id(royalty_vault), + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintNFT( + type="mint", spg_nft_contract=ADDRESS, allow_duplicates=False + ), + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=royalty_shares, + ip_metadata=IP_METADATA, + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) + assert ( + mock_build_register_transaction.call_args[0][4] + == royalty_shares_obj["royalty_shares"] + ) + assert mock_build_register_transaction.call_args[0][5] is False + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["license_terms_ids"] is not None + assert result["royalty_vault"] == royalty_vault + + def test_success_when_license_terms_data_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_parse_tx_license_terms_attached_event, + ): + with ( + mock_parse_ip_registered_event(), + mock_parse_tx_license_terms_attached_event(), + patch.object( + ip_asset.license_attachment_workflows_client, + "build_mintAndRegisterIpAndAttachPILTerms_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintNFT( + type="mint", spg_nft_contract=ADDRESS, allow_duplicates=False + ), + license_terms_data=LICENSE_TERMS_DATA, + ip_metadata=IP_METADATA, + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) + assert mock_build_register_transaction.call_args[0][4] is False + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["license_terms_ids"] is not None + + def test_success_when_license_terms_data_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_parse_tx_license_terms_attached_event, + mock_get_ip_id, + mock_signature_related_methods, + mock_is_registered, + ): + with ( + mock_parse_ip_registered_event(), + mock_parse_tx_license_terms_attached_event(), + mock_get_ip_id(), + mock_signature_related_methods(), + mock_is_registered(is_registered=False), + patch.object( + ip_asset.license_attachment_workflows_client, + "build_registerIpAndAttachPILTerms_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + license_terms_data=LICENSE_TERMS_DATA, + ip_metadata=IP_METADATA, + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == 3 + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["license_terms_ids"] is not None + + def test_success_when_ip_metadata_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_signature_related_methods, + mock_is_registered, + ): + with ( + mock_parse_ip_registered_event(), + mock_get_ip_id(), + mock_signature_related_methods(), + mock_is_registered(is_registered=False), + patch.object( + ip_asset.registration_workflows_client, + "build_registerIp_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + ip_metadata=IP_METADATA, + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == 3 + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + + def test_success_when_ip_metadata_not_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_signature_related_methods, + mock_is_registered, + ): + with ( + mock_parse_ip_registered_event(), + mock_get_ip_id(), + mock_signature_related_methods(), + mock_is_registered(is_registered=False), + patch.object( + ip_asset.ip_asset_registry_client, + "build_register_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + ) + print( + "mock_build_register_transaction.call_args", + mock_build_register_transaction.call_args, + ) + assert mock_build_register_transaction.call_args[0][0] == 1315 + assert mock_build_register_transaction.call_args[0][1] == ADDRESS + assert mock_build_register_transaction.call_args[0][2] == 3 + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + + def test_success_when_ip_metadata_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_signature_related_methods, + mock_is_registered, + ): + with ( + mock_parse_ip_registered_event(), + mock_get_ip_id(), + mock_signature_related_methods(), + mock_is_registered(is_registered=False), + patch.object( + ip_asset.registration_workflows_client, + "build_mintAndRegisterIp_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintNFT( + type="mint", spg_nft_contract=ADDRESS, allow_duplicates=False + ), + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) + assert mock_build_register_transaction.call_args[0][3] is False + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 From 4833340ef2f760ae398f85c4f20a2e6a34e1f52b Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Dec 2025 15:41:57 +0800 Subject: [PATCH 5/8] refactor: update contract method names and improve registration logic for IP assets --- .../resources/IPAsset.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 8c9d922..4139140 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1479,14 +1479,18 @@ def register_ip_asset( methods based on your input parameters: For already minted NFT (type="minted"): - - With `license_terms_data` + `royalty_shares`: `register_ip_and_attach_pil_terms_and_distribute_royalty_tokens` - - With `license_terms_data` only: `register_ip_and_attach_pil_terms` - - Basic registration: `register` + - With `license_terms_data` + `royalty_shares`: + `registerIpAndAttachPILTermsAndDeployRoyaltyVault` (contract method) + + `distributeRoyaltyTokens` (contract method) + - With `license_terms_data` only: `registerIpAndAttachPILTerms` (contract method) + - Basic registration: + - If `ip_metadata` is not provided, calls the `register` (contract method). + - If `ip_metadata` is provided, calls the `registerIp` (contract method). For new minted NFT (type="mint"): - - With `license_terms_data` + `royalty_shares`: `mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens` - - With license_terms_data only: `mint_and_register_ip_asset_with_pil_terms` - - Basic registration: `mint_and_register_ip` + - With `license_terms_data` + `royalty_shares`: `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_transaction` (contract method) + - With license_terms_data only: `mintAndRegisterIpAndAttachPILTerms` (contract method) + - Basic registration: `mintAndRegisterIp` (contract method) :param nft `MintNFT` | `MintedNFT`: The NFT to be registered as an IP asset. - For `MintNFT`: Mint a new NFT from an SPG NFT contract. @@ -1499,10 +1503,8 @@ def register_ip_asset( :param tx_options dict: [Optional] Transaction options. :return `RegisterIpAssetResponse`: Response with transaction hash, IP ID, token ID, and optionally license terms IDs, royalty vault, and distribute royalty tokens transaction hash. :raises ValueError: If `royalty_shares` is provided without `license_terms_data`. - :raises ValueError: If the `NFT` type is invalid. """ try: - # Validate `royalty_shares` without `license_terms_data` if royalty_shares and not license_terms_data: raise ValueError( "License terms data must be provided when royalty shares are specified." From f1a25e0a848dddc447a90243cb25cfa91420c650 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Dec 2025 16:03:24 +0800 Subject: [PATCH 6/8] feat: enhance integration tests for IP asset registration with new scenarios for metadata handling --- .../integration/test_integration_ip_asset.py | 39 ++++++++----------- tests/unit/resources/test_ip_asset.py | 2 +- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 0f6a432..4090454 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -1161,10 +1161,10 @@ class TestRegisterIpAsset: """Test suite for the unified register_ip_asset method that supports 6 different workflows""" @pytest.fixture(scope="class") - def test_register_ip_asset_minted_basic( + def test_register_ip_asset_minted_basic_with_ip_metadata( self, story_client: StoryClient, nft_collection ): - """Test basic registration for already minted NFT (uses register internally)""" + """Test basic registration for already minted NFT with ip metadata (uses register internally)""" token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) response = story_client.IPAsset.register_ip_asset( @@ -1180,6 +1180,21 @@ def test_register_ip_asset_minted_basic( assert isinstance(response["tx_hash"], str) and response["tx_hash"] assert isinstance(response["ip_id"], str) and response["ip_id"] + def test_register_ip_asset_minted_basic_without_ip_metadata( + self, story_client: StoryClient, nft_collection + ): + """Test basic registration for already minted NFT without ip metadata (uses register internally)""" + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + response = story_client.IPAsset.register_ip_asset( + nft=MintedNFT( + type="minted", nft_contract=nft_collection, token_id=token_id + ), + ) + + assert isinstance(response["tx_hash"], str) and response["tx_hash"] + assert isinstance(response["ip_id"], str) and response["ip_id"] + def test_register_ip_asset_minted_with_license_terms( self, story_client: StoryClient, nft_collection ): @@ -1447,23 +1462,3 @@ def test_register_ip_asset_mint_with_license_terms_and_royalty_shares( and len(response["license_terms_ids"]) > 0 ) assert isinstance(response["royalty_vault"], str) and response["royalty_vault"] - - def test_register_ip_asset_royalty_shares_without_license_terms_error( - self, story_client: StoryClient, nft_collection - ): - """Test error case when royalty_shares is provided without license_terms_data""" - token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) - - with pytest.raises(ValueError) as exc_info: - story_client.IPAsset.register_ip_asset( - nft=MintedNFT( - type="minted", - nft_contract=nft_collection, - token_id=token_id, - ), - royalty_shares=[ - RoyaltyShareInput(recipient=account.address, percentage=100.0), - ], - ) - - assert "License terms data must be provided" in str(exc_info.value) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index d40899e..4393653 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -2078,7 +2078,7 @@ def test_throw_not_provided_license_terms_data_when_royalty_shares_provided_for_ ): with pytest.raises( ValueError, - match="License terms data must be provided when royalty shares are specified.", + match="Failed to register IP Asset: License terms data must be provided when royalty shares are specified.", ): ip_asset.register_ip_asset( nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), From 895af13354366682e2893c3eb5efc6751b10c2ad Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Dec 2025 17:18:13 +0800 Subject: [PATCH 7/8] feat: update contract method name in IPAsset and add unit test for IP asset registration with metadata --- .../resources/IPAsset.py | 2 +- tests/unit/resources/test_ip_asset.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 4139140..5b893b0 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1488,7 +1488,7 @@ def register_ip_asset( - If `ip_metadata` is provided, calls the `registerIp` (contract method). For new minted NFT (type="mint"): - - With `license_terms_data` + `royalty_shares`: `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_transaction` (contract method) + - With `license_terms_data` + `royalty_shares`: `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens` (contract method) - With license_terms_data only: `mintAndRegisterIpAndAttachPILTerms` (contract method) - Basic registration: `mintAndRegisterIp` (contract method) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 4393653..148d1ce 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -2315,6 +2315,41 @@ def test_success_when_ip_metadata_provided_for_minted_nft( assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID + def test_success_when_ip_metadata_provided_for_minted_nft_without_ip_metadata( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_signature_related_methods, + mock_is_registered, + ): + partialIpMetadata = IPMetadataInput( + ip_metadata_uri="https://example.com/metadata/custom-value.json" + ) + with ( + mock_parse_ip_registered_event(), + mock_get_ip_id(), + mock_signature_related_methods(), + mock_is_registered(is_registered=False), + patch.object( + ip_asset.registration_workflows_client, + "build_registerIp_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + ip_metadata=partialIpMetadata, + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == 3 + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(partialIpMetadata).get_validated_data() + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + def test_success_when_ip_metadata_not_provided_for_minted_nft( self, ip_asset: IPAsset, From 44bc4e17a7916af484675a1a45908812a811d7fb Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 2 Dec 2025 17:52:40 +0800 Subject: [PATCH 8/8] feat: mark legacy methods as deprecated in IPAsset, recommending use of register_ip_asset() --- src/story_protocol_python_sdk/resources/IPAsset.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 5b893b0..13a5f56 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -3,6 +3,7 @@ from dataclasses import asdict, is_dataclass from ens.ens import Address, HexStr +from typing_extensions import deprecated from web3 import Web3 from story_protocol_python_sdk.abi.AccessController.AccessController_client import ( @@ -163,6 +164,7 @@ def build_mint_transaction( return tx_hash + @deprecated("Use register_ip_asset() instead.") def register( self, nft_contract: str, @@ -390,6 +392,7 @@ def register_derivative_with_license_tokens( f"Failed to register derivative with license tokens: {str(e)}" ) + @deprecated("Use register_ip_asset() instead.") def mint_and_register_ip_asset_with_pil_terms( self, spg_nft_contract: str, @@ -503,6 +506,7 @@ def mint_and_register_ip_asset_with_pil_terms( except Exception as e: raise e + @deprecated("Use register_ip_asset() instead.") def mint_and_register_ip( self, spg_nft_contract: str, @@ -619,6 +623,7 @@ def batch_mint_and_register_ip( except Exception as e: raise ValueError(f"Failed to batch mint and register IP: {str(e)}") + @deprecated("Use register_ip_asset() instead.") def register_ip_and_attach_pil_terms( self, nft_contract: str, @@ -1046,6 +1051,7 @@ def register_ip_and_make_derivative_with_license_tokens( f"Failed to register IP and make derivative with license tokens: {str(e)}" ) from e + @deprecated("Use register_ip_asset() instead.") def mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( self, spg_nft_contract: Address, @@ -1275,6 +1281,7 @@ def register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( f"Failed to register derivative IP and distribute royalty tokens: {str(e)}" ) from e + @deprecated("Use register_ip_asset() instead.") def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( self, nft_contract: Address,