From 917ac0af14012f89adbbd0e5cdad5c66d4cec6f6 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 3 Dec 2025 15:20:57 +0800 Subject: [PATCH 01/12] feat: add register_derivative_ip_asset method for registering derivative IP assets with enhanced functionality --- src/story_protocol_python_sdk/__init__.py | 2 + .../resources/IPAsset.py | 214 ++++++++++++++++++ .../types/resource/IPAsset.py | 20 ++ 3 files changed, 236 insertions(+) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 22f7b5c..4953f30 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -21,6 +21,7 @@ MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse, + RegisterDerivativeIpAssetResponse, RegisteredIP, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, @@ -75,6 +76,7 @@ "MintNFT", "MintedNFT", "RegisterIpAssetResponse", + "RegisterDerivativeIpAssetResponse", # 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 13a5f56..cc82455 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -59,6 +59,7 @@ MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse, + RegisterDerivativeIpAssetResponse, RegisteredIP, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, @@ -1665,6 +1666,219 @@ def _handle_mint_nft_registration( token_id=basic_result["token_id"], ) + def register_derivative_ip_asset( + self, + nft: MintNFT | MintedNFT, + deriv_data: DerivativeDataInput | None = None, + license_token_ids: list[int] | None = None, + royalty_shares: list[RoyaltyShareInput] | None = None, + max_rts: int = MAX_ROYALTY_TOKEN, + ip_metadata: IPMetadataInput | None = None, + deadline: int | None = None, + tx_options: dict | None = None, + ) -> RegisterDerivativeIpAssetResponse: + """ + Register a derivative IP asset, supporting both minted and mint-on-demand NFTs, + with optional `deriv_data`, `royalty_shares` and `license_token_ids`. + + 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 `deriv_data` + `royalty_shares`: + `registerIpAndMakeDerivativeAndDeployRoyaltyVault` (contract method) + + `distributeRoyaltyTokens` (contract method) + - With `deriv_data` only: `registerIpAndMakeDerivative` (contract method) + - With `license_token_ids` only: `registerIpAndMakeDerivativeWithLicenseTokens` (contract method) + + For new minted NFT (type="mint"): + - With `deriv_data` + `royalty_shares`: `mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens` (contract method) + - With `deriv_data` only: `mintAndRegisterIpAndMakeDerivative` (contract method) + - With `license_token_ids` only: `mintAndRegisterIpAndMakeDerivativeWithLicenseTokens` (contract method) + + :param nft `MintNFT` | `MintedNFT`: The NFT to be registered as a derivative IP asset. + - For `MintNFT`: Mint a new NFT from an SPG NFT contract. + - For `MintedNFT`: Register an already minted NFT. + :param deriv_data `DerivativeDataInput`: [Optional] The derivative data containing parent IP information and licensing terms. + Can be used independently or together with `royalty_shares` for royalty distribution. + :param license_token_ids list[int]: [Optional] The IDs of the license tokens to be burned for linking the IP to parent IPs. + :param royalty_shares `list[RoyaltyShareInput]`: [Optional] Authors of the IP and their shares of the royalty tokens. + Can only be specified when `deriv_data` is also provided. + :param max_rts int: [Optional] The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). (default: 100,000,000) + :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 `RegisterDerivativeIpAssetResponse`: Response with transaction hash, IP ID, token ID, and optionally royalty vault and distribute royalty tokens transaction hash. + :raises ValueError: If `royalty_shares` is provided without `deriv_data`. + :raises ValueError: If neither `deriv_data` nor `license_token_ids` are provided. + """ + try: + if royalty_shares and not deriv_data: + raise ValueError( + "deriv_data must be provided when royalty_shares are provided." + ) + + has_deriv_data = deriv_data is not None + has_license_tokens = ( + license_token_ids is not None and len(license_token_ids) > 0 + ) + + if not has_deriv_data and not has_license_tokens: + raise ValueError( + "Either deriv_data or license_token_ids must be provided." + ) + + if nft.type == "minted": + return self._handle_minted_nft_derivative_registration( + nft=nft, + deriv_data=deriv_data, + license_token_ids=license_token_ids, + royalty_shares=royalty_shares, + max_rts=max_rts, + ip_metadata=ip_metadata, + deadline=deadline, + tx_options=tx_options, + ) + + return self._handle_mint_nft_derivative_registration( + nft=nft, + deriv_data=deriv_data, + license_token_ids=license_token_ids, + royalty_shares=royalty_shares, + max_rts=max_rts, + ip_metadata=ip_metadata, + tx_options=tx_options, + ) + + except Exception as e: + raise ValueError(f"Failed to register derivative IP Asset: {str(e)}") from e + + def _handle_minted_nft_derivative_registration( + self, + nft: MintedNFT, + deriv_data: DerivativeDataInput | None, + license_token_ids: list[int] | None, + royalty_shares: list[RoyaltyShareInput] | None, + max_rts: int, + ip_metadata: IPMetadataInput | None, + deadline: int | None, + tx_options: dict | None, + ) -> RegisterDerivativeIpAssetResponse: + """ + Handle derivative registration for already minted NFTs. + """ + if royalty_shares and deriv_data: + royalty_result = self.register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + nft_contract=nft.nft_contract, + token_id=nft.token_id, + deriv_data=deriv_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + deadline=deadline or 1000, + tx_options=tx_options, + ) + return RegisterDerivativeIpAssetResponse( + tx_hash=royalty_result["tx_hash"], + ip_id=royalty_result["ip_id"], + token_id=royalty_result["token_id"], + royalty_vault=royalty_result["royalty_vault"], + distribute_royalty_tokens_tx_hash=royalty_result[ + "distribute_royalty_tokens_tx_hash" + ], + ) + + if deriv_data: + deriv_result = self.register_derivative_ip( + nft_contract=nft.nft_contract, + token_id=nft.token_id, + deriv_data=deriv_data, + metadata=ip_metadata, + deadline=deadline, + tx_options=tx_options, + ) + return RegisterDerivativeIpAssetResponse( + tx_hash=deriv_result["tx_hash"], + ip_id=deriv_result["ip_id"], + ) + + # Use license_token_ids + token_result = self.register_ip_and_make_derivative_with_license_tokens( + nft_contract=nft.nft_contract, + token_id=nft.token_id, + license_token_ids=license_token_ids, # type: ignore + max_rts=max_rts, + deadline=deadline or 1000, + ip_metadata=ip_metadata, + tx_options=tx_options, + ) + return RegisterDerivativeIpAssetResponse( + tx_hash=token_result["tx_hash"], + ip_id=token_result["ip_id"], + token_id=token_result["token_id"], + ) + + def _handle_mint_nft_derivative_registration( + self, + nft: MintNFT, + deriv_data: DerivativeDataInput | None, + license_token_ids: list[int] | None, + royalty_shares: list[RoyaltyShareInput] | None, + max_rts: int, + ip_metadata: IPMetadataInput | None, + tx_options: dict | None, + ) -> RegisterDerivativeIpAssetResponse: + """ + Handle derivative registration for minting new NFTs. + """ + if royalty_shares and deriv_data: + royalty_result = self.mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( + spg_nft_contract=nft.spg_nft_contract, + deriv_data=deriv_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + recipient=nft.recipient, + allow_duplicates=nft.allow_duplicates, + tx_options=tx_options, + ) + return RegisterDerivativeIpAssetResponse( + tx_hash=royalty_result["tx_hash"], + ip_id=royalty_result["ip_id"], + token_id=royalty_result["token_id"], + ) + + if deriv_data: + deriv_result = self.mint_and_register_ip_and_make_derivative( + spg_nft_contract=nft.spg_nft_contract, + deriv_data=deriv_data, + ip_metadata=ip_metadata, + recipient=nft.recipient, + allow_duplicates=nft.allow_duplicates, + tx_options=tx_options, + ) + return RegisterDerivativeIpAssetResponse( + tx_hash=deriv_result["tx_hash"], + ip_id=deriv_result["ip_id"], + token_id=deriv_result["token_id"], + ) + + # Use license_token_ids + token_result = ( + self.mint_and_register_ip_and_make_derivative_with_license_tokens( + spg_nft_contract=nft.spg_nft_contract, + license_token_ids=license_token_ids, # type: ignore + max_rts=max_rts, + recipient=nft.recipient, + allow_duplicates=nft.allow_duplicates, + ip_metadata=ip_metadata, + tx_options=tx_options, + ) + ) + return RegisterDerivativeIpAssetResponse( + tx_hash=token_result["tx_hash"], + ip_id=token_result["ip_id"], + token_id=token_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 924c5be..37c4a53 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -206,3 +206,23 @@ class RegisterIpAssetResponse(TypedDict, total=False): license_terms_ids: list[int] royalty_vault: Address distribute_royalty_tokens_tx_hash: HexStr + + +class RegisterDerivativeIpAssetResponse(TypedDict, total=False): + """ + Response structure for unified derivative 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. + 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 + royalty_vault: Address + distribute_royalty_tokens_tx_hash: HexStr From d04709f88ff216ae035113b9fccfc335f59ea040 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 3 Dec 2025 17:20:33 +0800 Subject: [PATCH 02/12] feat: add integration tests for registering derivative IP assets with various workflows and licensing options --- .../integration/test_integration_ip_asset.py | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 4090454..ff3daf4 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -1462,3 +1462,254 @@ 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"] + + +class TestRegisterDerivativeIpAsset: + """Test suite for the unified register_derivative_ip_asset method that supports 6 different workflows""" + + @pytest.fixture(scope="class") + def parent_ip_with_commercial_license( + self, story_client: StoryClient, nft_collection + ): + """Fixture to provide a parent IP with commercial license terms attached""" + response = story_client.IPAsset.register_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + allow_duplicates=True, + ), + license_terms_data=[ + LicenseTermsDataInput( + terms=LicenseTermsInput( + 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_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-parent-license-for-derivative", + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=0, + 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, + ), + ) + ], + ) + return { + "parent_ip_id": response["ip_id"], + "license_terms_id": response["license_terms_ids"][0], + } + + def test_register_derivative_ip_asset_minted_with_deriv_data( + self, + story_client: StoryClient, + nft_collection, + parent_ip_with_commercial_license, + ): + """Test derivative registration for already minted NFT with deriv_data + (uses register_derivative_ip internally) + """ + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + response = story_client.IPAsset.register_derivative_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_with_commercial_license["parent_ip_id"]], + license_terms_ids=[ + parent_ip_with_commercial_license["license_terms_id"] + ], + max_minting_fee=0, + max_rts=100_000_000, + max_revenue_share=100, + ), + 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_derivative_ip_asset_minted_with_deriv_data_and_royalty_shares( + self, + story_client: StoryClient, + nft_collection, + parent_ip_with_commercial_license, + ): + """Test derivative registration for already minted NFT with deriv_data and royalty_shares + (uses register_derivative_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=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ] + + response = story_client.IPAsset.register_derivative_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_with_commercial_license["parent_ip_id"]], + license_terms_ids=[ + parent_ip_with_commercial_license["license_terms_id"] + ], + max_minting_fee=0, + max_rts=100_000_000, + max_revenue_share=100, + ), + 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["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_derivative_ip_asset_minted_with_license_token_ids( + self, + story_client: StoryClient, + nft_collection, + mint_and_approve_license_token, + ): + """Test derivative registration for already minted NFT with license_token_ids + (uses register_ip_and_make_derivative_with_license_tokens internally) + """ + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + response = story_client.IPAsset.register_derivative_ip_asset( + nft=MintedNFT( + type="minted", + nft_contract=nft_collection, + token_id=token_id, + ), + license_token_ids=[mint_and_approve_license_token[0]], + max_rts=100_000_000, + 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 + ) + + def test_register_derivative_ip_asset_mint_with_deriv_data( + self, + story_client: StoryClient, + nft_collection, + parent_ip_with_commercial_license, + ): + """Test derivative registration with minting new NFT and deriv_data + (uses mint_and_register_ip_and_make_derivative internally) + """ + response = story_client.IPAsset.register_derivative_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + ), + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_with_commercial_license["parent_ip_id"]], + license_terms_ids=[ + parent_ip_with_commercial_license["license_terms_id"] + ], + ), + 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_derivative_ip_asset_mint_with_deriv_data_and_royalty_shares( + self, + story_client: StoryClient, + nft_collection, + parent_ip_with_commercial_license, + ): + """Test derivative registration with minting new NFT, deriv_data and royalty_shares + (uses mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens internally) + """ + royalty_shares = [ + RoyaltyShareInput(recipient=account.address, percentage=60.0), + RoyaltyShareInput(recipient=account_2.address, percentage=40.0), + ] + + response = story_client.IPAsset.register_derivative_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + ), + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_with_commercial_license["parent_ip_id"]], + license_terms_ids=[ + parent_ip_with_commercial_license["license_terms_id"] + ], + ), + 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) + + def test_register_derivative_ip_asset_mint_with_license_token_ids( + self, + story_client: StoryClient, + nft_collection, + mint_and_approve_license_token, + ): + """Test derivative registration with minting new NFT and license_token_ids + (uses mint_and_register_ip_and_make_derivative_with_license_tokens internally) + """ + response = story_client.IPAsset.register_derivative_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + ), + license_token_ids=[mint_and_approve_license_token[1]], + 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) From db518c29ec45691b1d5cdc683bc858a9664485d5 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Dec 2025 10:50:36 +0800 Subject: [PATCH 03/12] feat: mark legacy methods in IPAsset as deprecated, recommending use of register_derivative_ip_asset --- src/story_protocol_python_sdk/resources/IPAsset.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index cc82455..1870f9f 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -770,6 +770,7 @@ def register_ip_and_attach_pil_terms( except Exception as e: raise e + @deprecated("Deprecated: Use register_derivative_ip_asset instead.") def register_derivative_ip( self, nft_contract: str, @@ -853,6 +854,7 @@ def register_derivative_ip( except Exception as e: raise e + @deprecated("Deprecated: Use register_derivative_ip_asset instead.") def mint_and_register_ip_and_make_derivative( self, spg_nft_contract: str, @@ -900,6 +902,7 @@ def mint_and_register_ip_and_make_derivative( except Exception as e: raise e + @deprecated("Deprecated: Use register_derivative_ip_asset instead.") def mint_and_register_ip_and_make_derivative_with_license_tokens( self, spg_nft_contract: Address, @@ -951,6 +954,7 @@ def mint_and_register_ip_and_make_derivative_with_license_tokens( except Exception as e: raise e + @deprecated("Deprecated: Use register_derivative_ip_asset instead.") def register_ip_and_make_derivative_with_license_tokens( self, nft_contract: str, @@ -1117,6 +1121,7 @@ def mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( f"Failed to mint, register IP, attach PIL terms and distribute royalty tokens: {str(e)}" ) from e + @deprecated("Deprecated: Use register_derivative_ip_asset instead.") def mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( self, spg_nft_contract: Address, @@ -1177,6 +1182,7 @@ def mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( f"Failed to mint, register IP, make derivative and distribute royalty tokens: {str(e)}" ) from e + @deprecated("Deprecated: Use register_derivative_ip_asset instead.") def register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( self, nft_contract: Address, From 9dd16571868103dd528cad24e06489ff324b15ac Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Dec 2025 15:55:27 +0800 Subject: [PATCH 04/12] feat: add unit tests --- .../resources/IPAsset.py | 21 +- .../utils/constants.py | 2 +- tests/unit/resources/test_ip_asset.py | 530 +++++++++++++++++- 3 files changed, 531 insertions(+), 22 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 1870f9f..0476cb1 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1,6 +1,7 @@ """Module for handling IP Account operations and transactions.""" from dataclasses import asdict, is_dataclass +from typing import cast from ens.ens import Address, HexStr from typing_extensions import deprecated @@ -907,7 +908,7 @@ def mint_and_register_ip_and_make_derivative_with_license_tokens( self, spg_nft_contract: Address, license_token_ids: list[int], - max_rts: int, + max_rts: int = MAX_ROYALTY_TOKEN, recipient: Address | None = None, allow_duplicates: bool = True, ip_metadata: IPMetadataInput | None = None, @@ -918,7 +919,7 @@ def mint_and_register_ip_and_make_derivative_with_license_tokens( :param spg_nft_contract Address: The address of the `SPGNFT` collection. :param license_token_ids list[int]: The IDs of the license tokens to be burned for linking the IP to parent IPs. - :param max_rts int: The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). + :param max_rts int: The maximum number of royalty tokens that can be distributed to the external royalty policies (default: 100,000,000). :param recipient Address: [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 ip_metadata IPMetadataInput: [Optional] The desired metadata for the newly minted NFT and newly registered IP. @@ -1170,7 +1171,6 @@ def mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( response["tx_receipt"], ip_registered["ip_id"], ) - return RegistrationWithRoyaltyVaultResponse( tx_hash=response["tx_hash"], ip_id=ip_registered["ip_id"], @@ -1482,7 +1482,7 @@ def register_ip_asset( license_terms_data: list[LicenseTermsDataInput] | None = None, royalty_shares: list[RoyaltyShareInput] | None = None, ip_metadata: IPMetadataInput | None = None, - deadline: int | None = None, + deadline: int = 1000, tx_options: dict | None = None, ) -> RegisterIpAssetResponse: """ @@ -1679,8 +1679,8 @@ def register_derivative_ip_asset( license_token_ids: list[int] | None = None, royalty_shares: list[RoyaltyShareInput] | None = None, max_rts: int = MAX_ROYALTY_TOKEN, + deadline: int = 1000, ip_metadata: IPMetadataInput | None = None, - deadline: int | None = None, tx_options: dict | None = None, ) -> RegisterDerivativeIpAssetResponse: """ @@ -1731,7 +1731,7 @@ def register_derivative_ip_asset( if not has_deriv_data and not has_license_tokens: raise ValueError( - "Either deriv_data or license_token_ids must be provided." + "either deriv_data or license_token_ids must be provided." ) if nft.type == "minted": @@ -1767,7 +1767,7 @@ def _handle_minted_nft_derivative_registration( royalty_shares: list[RoyaltyShareInput] | None, max_rts: int, ip_metadata: IPMetadataInput | None, - deadline: int | None, + deadline: int, tx_options: dict | None, ) -> RegisterDerivativeIpAssetResponse: """ @@ -1780,7 +1780,7 @@ def _handle_minted_nft_derivative_registration( deriv_data=deriv_data, royalty_shares=royalty_shares, ip_metadata=ip_metadata, - deadline=deadline or 1000, + deadline=deadline, tx_options=tx_options, ) return RegisterDerivativeIpAssetResponse( @@ -1811,9 +1811,9 @@ def _handle_minted_nft_derivative_registration( token_result = self.register_ip_and_make_derivative_with_license_tokens( nft_contract=nft.nft_contract, token_id=nft.token_id, - license_token_ids=license_token_ids, # type: ignore + license_token_ids=cast(list[int], license_token_ids), max_rts=max_rts, - deadline=deadline or 1000, + deadline=deadline, ip_metadata=ip_metadata, tx_options=tx_options, ) @@ -1850,6 +1850,7 @@ def _handle_mint_nft_derivative_registration( tx_hash=royalty_result["tx_hash"], ip_id=royalty_result["ip_id"], token_id=royalty_result["token_id"], + royalty_vault=royalty_result["royalty_vault"], ) if deriv_data: diff --git a/src/story_protocol_python_sdk/utils/constants.py b/src/story_protocol_python_sdk/utils/constants.py index 835244c..ab94749 100644 --- a/src/story_protocol_python_sdk/utils/constants.py +++ b/src/story_protocol_python_sdk/utils/constants.py @@ -2,7 +2,7 @@ ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000" ZERO_FUNC = "0x00000000" DEFAULT_FUNCTION_SELECTOR = "0x00000000" -MAX_ROYALTY_TOKEN = 100000000 +MAX_ROYALTY_TOKEN = 100_000_000 ROYALTY_POLICY_LAP_ADDRESS = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" ROYALTY_POLICY_LRP_ADDRESS = "0x9156E603C949481883B1D3355C6F1132D191FC41" WIP_TOKEN_ADDRESS = "0x1514000000000000000000000000000000000000" diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 148d1ce..595198f 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -4,7 +4,12 @@ from ens.ens import HexStr from web3 import Web3 -from story_protocol_python_sdk import MintedNFT, MintNFT, RoyaltyShareInput +from story_protocol_python_sdk import ( + MAX_ROYALTY_TOKEN, + MintedNFT, + MintNFT, + RoyaltyShareInput, +) from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( IPAccountImplClient, ) @@ -117,6 +122,16 @@ def _mock(royalty_vault=ADDRESS): return _mock +@pytest.fixture +def mock_owner_of(ip_asset: IPAsset): + def _mock(owner=ACCOUNT_ADDRESS): + return patch.object( + ip_asset.license_token_client, "ownerOf", return_value=owner + ) + + return _mock + + class TestIPAssetRegister: def test_register_invalid_deadline_type( self, ip_asset, mock_get_ip_id, mock_is_registered @@ -603,16 +618,6 @@ def test_with_custom_value( assert not mock_build_transaction.call_args[0][4] # allowDuplicates -@pytest.fixture(scope="class") -def mock_owner_of(ip_asset: IPAsset): - def _mock(owner: str = ACCOUNT_ADDRESS): - return patch.object( - ip_asset.license_token_client, "ownerOf", return_value=owner - ) - - return _mock - - class TestRegisterIpAndMakeDerivativeWithLicenseTokens: def test_nft_already_registered( self, ip_asset: IPAsset, mock_get_ip_id, mock_is_registered @@ -2416,3 +2421,506 @@ def test_success_when_ip_metadata_provided_for_mint_nft( assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID assert result["token_id"] == 3 + + +class TestRegisterDerivativeIpAsset: + def test_throw_error_when_deriv_data_is_not_provided_and_royalty_shares_are_provided_for_minted_nft( + self, ip_asset: IPAsset + ): + with pytest.raises( + ValueError, + match="Failed to register derivative IP Asset: deriv_data must be provided when royalty_shares are provided.", + ): + ip_asset.register_derivative_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + ], + ) + + def test_throw_error_when_deriv_data_is_not_provided_and_royalty_shares_are_provided_for_mint_nft( + self, ip_asset: IPAsset + ): + with pytest.raises( + ValueError, + match="Failed to register derivative IP Asset: deriv_data must be provided when royalty_shares are provided.", + ): + ip_asset.register_derivative_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), + royalty_shares=[ + RoyaltyShareInput(recipient=ADDRESS, percentage=50.0), + ], + ) + + def test_success_when_deriv_data_and_royalty_shares_are_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_signature_related_methods, + mock_is_registered, + mock_get_royalty_vault_address_by_ip_id, + mock_license_registry_client, + mock_ip_account_impl_client, + ): + with ( + mock_get_ip_id(), + mock_is_registered(is_registered=False), + mock_parse_ip_registered_event(), + mock_signature_related_methods(), + mock_get_royalty_vault_address_by_ip_id(), + mock_license_registry_client(), + mock_ip_account_impl_client(), + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_registerIpAndMakeDerivativeAndDeployRoyaltyVault_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_royalty_tokens, + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + ], + ip_metadata=IP_METADATA, + deadline=100000, + ) + assert ( + mock_build_register_transaction.call_args[0][0] == ADDRESS + ) # nft_contract + assert mock_build_register_transaction.call_args[0][1] == 3 # token_id + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + assert mock_distribute_royalty_tokens.call_args[0][0] == IP_ID # ip_id + assert ( + mock_distribute_royalty_tokens.call_args[0][1] + == get_royalty_shares( + [ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + ] + )[ + "royalty_shares" + ] # royalty_shares + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["royalty_vault"] == ADDRESS + assert result["distribute_royalty_tokens_tx_hash"] == TX_HASH.hex() + + def test_success_when_deriv_data_and_royalty_shares_are_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_license_registry_client, + mock_parse_ip_registered_event, + mock_get_royalty_vault_address_by_ip_id, + ): + with ( + mock_license_registry_client(), + mock_get_royalty_vault_address_by_ip_id(), + mock_parse_ip_registered_event(), + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=ADDRESS, percentage=50.0), + ], + ) + assert ( + mock_build_register_transaction.call_args[0][0] == ADDRESS + ) # spg_nft_contract + assert ( + mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][4] + == get_royalty_shares( + [RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)] + )[ + "royalty_shares" + ] # royalty_shares + ) + assert mock_build_register_transaction.call_args[0][5] is True + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["royalty_vault"] == ADDRESS + + def test_success_when_deriv_data_royalty_shares_recipient_and_ip_metadata_are_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_license_registry_client, + mock_parse_ip_registered_event, + mock_get_royalty_vault_address_by_ip_id, + ): + with ( + mock_license_registry_client(), + mock_get_royalty_vault_address_by_ip_id(), + mock_parse_ip_registered_event(), + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + ip_asset.register_derivative_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS, recipient=ADDRESS), + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=ADDRESS, percentage=50.0), + ], + ip_metadata=IP_METADATA, + ) + assert ( + mock_build_register_transaction.call_args[0][1] == ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + + def test_throw_error_when_license_token_ids_and_deriv_data_are_not_provided_for_minted_nft( + self, ip_asset: IPAsset + ): + with pytest.raises( + ValueError, + match="Failed to register derivative IP Asset: either deriv_data or license_token_ids must be provided.", + ): + ip_asset.register_derivative_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + ) + + def test_throw_error_when_license_token_ids_and_deriv_data_are_not_provided_for_mint_nft( + self, ip_asset: IPAsset + ): + with pytest.raises( + ValueError, + match="Failed to register derivative IP Asset: either deriv_data or license_token_ids must be provided.", + ): + ip_asset.register_derivative_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), + ) + + def test_success_when_deriv_data_only_are_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_license_registry_client, + mock_signature_related_methods, + mock_is_registered, + mock_get_function_signature, + ): + with ( + mock_get_ip_id(), + mock_is_registered(is_registered=False), + mock_parse_ip_registered_event(), + mock_license_registry_client(), + mock_signature_related_methods(), + mock_get_function_signature(), + patch.object( + ip_asset.derivative_workflows_client, + "build_registerIpAndMakeDerivative_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + ) + assert ( + mock_build_register_transaction.call_args[0][0] == ADDRESS + ) # nft_contract + assert mock_build_register_transaction.call_args[0][1] == 3 # token_id + assert ( + mock_build_register_transaction.call_args[0][3] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + + def test_success_when_deriv_data_only_are_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_license_registry_client, + ): + with ( + mock_parse_ip_registered_event(), + mock_license_registry_client(), + patch.object( + ip_asset.derivative_workflows_client, + "build_mintAndRegisterIpAndMakeDerivative_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=ADDRESS, + ), + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + ) + assert ( + mock_build_register_transaction.call_args[0][0] == ADDRESS + ) # spg_nft_contract + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][3] == ACCOUNT_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][4] is True + ) # allow_duplicates + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + + def test_success_when_deriv_data_recipient_allow_duplicates_and_ip_metadata_are_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_license_registry_client, + ): + with ( + mock_parse_ip_registered_event(), + mock_license_registry_client(), + patch.object( + ip_asset.derivative_workflows_client, + "build_mintAndRegisterIpAndMakeDerivative_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + ip_asset.register_derivative_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=ADDRESS, + recipient=ZERO_ADDRESS, + allow_duplicates=False, + ), + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + ip_metadata=IP_METADATA, + ) + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][3] == ZERO_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][4] is False + ) # allow_duplicates + + def test_success_when_license_token_ids_only_are_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_license_registry_client, + mock_signature_related_methods, + mock_is_registered, + mock_get_function_signature, + mock_owner_of, + ): + with ( + mock_get_ip_id(), + mock_is_registered(is_registered=False), + mock_parse_ip_registered_event(), + mock_license_registry_client(), + mock_signature_related_methods(), + mock_get_function_signature(), + patch.object( + ip_asset.derivative_workflows_client, + "build_registerIpAndMakeDerivativeWithLicenseTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + mock_owner_of(), + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + license_token_ids=[1], + ) + assert ( + mock_build_register_transaction.call_args[0][0] == ADDRESS + ) # nft_contract + assert mock_build_register_transaction.call_args[0][1] == 3 # token_id + assert mock_build_register_transaction.call_args[0][2] == [ + 1 + ] # license_token_ids + assert ( + mock_build_register_transaction.call_args[0][3] == ZERO_ADDRESS + ) # royalty_context + assert ( + mock_build_register_transaction.call_args[0][4] == MAX_ROYALTY_TOKEN + ) # max_rts + assert ( + mock_build_register_transaction.call_args[0][5] + == IPMetadata.from_input().get_validated_data() + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + + def test_success_when_license_token_ids_ip_metadata_and_max_rts_are_provided_for_minted_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_get_ip_id, + mock_license_registry_client, + mock_signature_related_methods, + mock_is_registered, + mock_get_function_signature, + mock_owner_of, + ): + with ( + mock_get_ip_id(), + mock_is_registered(is_registered=False), + mock_parse_ip_registered_event(), + mock_license_registry_client(), + mock_signature_related_methods(), + mock_get_function_signature(), + patch.object( + ip_asset.derivative_workflows_client, + "build_registerIpAndMakeDerivativeWithLicenseTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + mock_owner_of(), + ): + ip_asset.register_derivative_ip_asset( + nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), + license_token_ids=[1], + max_rts=1000_000, + ip_metadata=IP_METADATA, + ) + assert ( + mock_build_register_transaction.call_args[0][4] == 1000_000 + ) # max_rts + assert ( + mock_build_register_transaction.call_args[0][5] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + + def test_success_when_license_token_ids_only_are_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_owner_of, + ): + with ( + mock_parse_ip_registered_event(), + mock_owner_of(), + patch.object( + ip_asset.derivative_workflows_client, + "build_mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), + license_token_ids=[1], + ) + assert mock_build_register_transaction.call_args[0][0] == ADDRESS + assert mock_build_register_transaction.call_args[0][1] == [ + 1 + ] # license_token_ids + assert ( + mock_build_register_transaction.call_args[0][2] == ZERO_ADDRESS + ) # royalty_context + assert ( + mock_build_register_transaction.call_args[0][3] == MAX_ROYALTY_TOKEN + ) # max_rts + assert ( + mock_build_register_transaction.call_args[0][4] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][5] == ACCOUNT_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][6] is True + ) # allow_duplicates + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + + def test_success_when_license_token_ids_ip_metadata_and_max_rts_are_provided_for_mint_nft( + self, + ip_asset: IPAsset, + mock_parse_ip_registered_event, + mock_owner_of, + ): + with ( + mock_parse_ip_registered_event(), + mock_owner_of(), + patch.object( + ip_asset.derivative_workflows_client, + "build_mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + result = ip_asset.register_derivative_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=ADDRESS, + recipient=ZERO_ADDRESS, + allow_duplicates=False, + ), + license_token_ids=[1], + ip_metadata=IP_METADATA, + max_rts=1000_000, + ) + assert ( + mock_build_register_transaction.call_args[0][3] == 1000_000 + ) # max_rts + assert ( + mock_build_register_transaction.call_args[0][4] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][5] == ZERO_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][6] is False + ) # allow_duplicates + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 From 1398e71708add283f6bf033d949b5f2bc0580529 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Dec 2025 16:36:02 +0800 Subject: [PATCH 05/12] feat: include token_id in IP asset registration response and update integration tests for consistency --- src/story_protocol_python_sdk/resources/IPAsset.py | 7 ++++++- tests/integration/test_integration_ip_asset.py | 10 ++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 0476cb1..7de6619 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -850,7 +850,11 @@ def register_derivative_ip( 0 ] - return {"tx_hash": response["tx_hash"], "ip_id": ip_registered["ip_id"]} + return { + "tx_hash": response["tx_hash"], + "ip_id": ip_registered["ip_id"], + "token_id": ip_registered["token_id"], + } except Exception as e: raise e @@ -1805,6 +1809,7 @@ def _handle_minted_nft_derivative_registration( return RegisterDerivativeIpAssetResponse( tx_hash=deriv_result["tx_hash"], ip_id=deriv_result["ip_id"], + token_id=deriv_result["token_id"], ) # Use license_token_ids diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index ff3daf4..ad0b946 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -1544,11 +1544,14 @@ def test_register_derivative_ip_asset_minted_with_deriv_data( max_revenue_share=100, ), ip_metadata=COMMON_IP_METADATA, - deadline=1000, + deadline=100000, ) 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 + ) def test_register_derivative_ip_asset_minted_with_deriv_data_and_royalty_shares( self, @@ -1583,7 +1586,7 @@ def test_register_derivative_ip_asset_minted_with_deriv_data_and_royalty_shares( ), royalty_shares=royalty_shares, ip_metadata=COMMON_IP_METADATA, - deadline=1000, + deadline=100000, ) assert isinstance(response["tx_hash"], str) and response["tx_hash"] @@ -1615,9 +1618,8 @@ def test_register_derivative_ip_asset_minted_with_license_token_ids( token_id=token_id, ), license_token_ids=[mint_and_approve_license_token[0]], - max_rts=100_000_000, ip_metadata=COMMON_IP_METADATA, - deadline=1000, + deadline=100000, ) assert isinstance(response["tx_hash"], str) and response["tx_hash"] From de9cd29f32b6e529848a03b0dce54f2ff5e1d77d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 4 Dec 2025 18:11:26 +0800 Subject: [PATCH 06/12] refactor: streamline integration tests by introducing utility functions for creating parent IP and license terms, and minting license tokens --- tests/integration/config/utils.py | 104 ++++++++++++++++++ tests/integration/conftest.py | 96 +--------------- .../integration/test_integration_ip_asset.py | 64 ++++++++--- 3 files changed, 154 insertions(+), 110 deletions(-) diff --git a/tests/integration/config/utils.py b/tests/integration/config/utils.py index efc343e..525d84b 100644 --- a/tests/integration/config/utils.py +++ b/tests/integration/config/utils.py @@ -4,12 +4,27 @@ import base58 from dotenv import load_dotenv +from eth_account.signers.local import LocalAccount from web3 import Web3 +from story_protocol_python_sdk import ( + ROYALTY_POLICY_LAP_ADDRESS, + LicenseTermsDataInput, + LicenseTermsInput, + LicensingConfig, + MintNFT, +) +from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( + DerivativeWorkflowsClient, +) +from story_protocol_python_sdk.abi.LicenseToken.LicenseToken_client import ( + LicenseTokenClient, +) from story_protocol_python_sdk.story_client import StoryClient load_dotenv() + # Mock ERC721 contract address MockERC721 = "0xa1119092ea911202E0a65B743a13AE28C5CF2f21" @@ -262,3 +277,92 @@ def setup_royalty_vault(story_client, parent_ip_id, account): ) return response + + +def mint_and_approve_license_token( + story_client: StoryClient, + parent_ip_and_license_terms: dict, + account: LocalAccount, +): + """ + Fixture to mint and approve license tokens for derivative workflow testing. + + :param story_client: The StoryClient instance. + :param parent_ip_and_license_terms: A dictionary containing the parent IP ID and license terms ID. + :return: A list of license token IDs. + """ + license_token_response = story_client.License.mint_license_tokens( + licensor_ip_id=parent_ip_and_license_terms["parent_ip_id"], + license_template=PIL_LICENSE_TEMPLATE, + license_terms_id=parent_ip_and_license_terms["license_terms_id"], + amount=2, + receiver=account.address, + max_revenue_share=100, + ) + license_token_ids = license_token_response["license_token_ids"] + + for license_token_id in license_token_ids: + approve( + erc20_contract_address=LicenseTokenClient( + story_client.web3 + ).contract.address, + web3=story_client.web3, + account=account, + spender_address=DerivativeWorkflowsClient( + story_client.web3 + ).contract.address, + amount=license_token_id, + ) + + return license_token_ids + + +def create_parent_ip_and_license_terms( + story_client: StoryClient, nft_collection, account: LocalAccount +): + """Fixture to provide the parent IP and license terms""" + response = story_client.IPAsset.register_ip_asset( + nft=MintNFT( + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + type="mint", + ), + license_terms_data=[ + LicenseTermsDataInput( + terms=LicenseTermsInput( + 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=LicensingConfig( + 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/conftest.py b/tests/integration/conftest.py index 363e6fe..800fe64 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,19 +1,8 @@ import pytest -from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( - DerivativeWorkflowsClient, -) -from story_protocol_python_sdk.abi.LicenseToken.LicenseToken_client import ( - LicenseTokenClient, -) 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, private_key, web3 -from tests.integration.config.utils import MockERC20, approve, get_story_client -from tests.integration.setup_for_integration import PIL_LICENSE_TEMPLATE +from tests.integration.config.utils import get_story_client @pytest.fixture(scope="session") @@ -46,89 +35,6 @@ def story_client_2() -> StoryClient: return story_client_2 -@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, - }, - } - ], - allow_duplicates=True, - ) - return { - "parent_ip_id": response["ip_id"], - "license_terms_id": response["license_terms_ids"][0], - } - - -@pytest.fixture(scope="module") -def mint_and_approve_license_token( - story_client: StoryClient, parent_ip_and_license_terms -): - """ - Fixture to mint and approve license tokens for derivative workflow testing. - - :param story_client: The StoryClient instance. - :param parent_ip_and_license_terms: A dictionary containing the parent IP ID and license terms ID. - :return: A list of license token IDs. - """ - license_token_response = story_client.License.mint_license_tokens( - licensor_ip_id=parent_ip_and_license_terms["parent_ip_id"], - license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=parent_ip_and_license_terms["license_terms_id"], - amount=2, - receiver=account.address, - max_revenue_share=100, - ) - license_token_ids = license_token_response["license_token_ids"] - - for license_token_id in license_token_ids: - approve( - erc20_contract_address=LicenseTokenClient( - story_client.web3 - ).contract.address, - web3=story_client.web3, - account=account, - spender_address=DerivativeWorkflowsClient( - story_client.web3 - ).contract.address, - amount=license_token_id, - ) - - return license_token_ids - - @pytest.fixture(scope="module") def nft_collection(story_client: StoryClient): tx_data = story_client.NFTClient.create_nft_collection( diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index ad0b946..74f2217 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -21,7 +21,11 @@ LicenseTokenClient, ) from tests.integration.config.test_config import account_2 -from tests.integration.config.utils import approve +from tests.integration.config.utils import ( + approve, + create_parent_ip_and_license_terms, + mint_and_approve_license_token, +) from .setup_for_integration import ( PIL_LICENSE_TEMPLATE, @@ -391,9 +395,12 @@ def test_mint_register_ip(self, story_client: StoryClient, nft_collection): ) def test_mint_and_register_ip_and_make_derivative( - self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms + self, story_client: StoryClient, nft_collection ): """Test minting NFT, registering IP and making derivative with custom derivative data and metadata""" + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) response = story_client.IPAsset.mint_and_register_ip_and_make_derivative( spg_nft_contract=nft_collection, deriv_data=DerivativeDataInput( @@ -416,10 +423,16 @@ def test_mint_and_register_ip_and_make_derivative_with_license_tokens( self, story_client: StoryClient, nft_collection, - mint_and_approve_license_token, ): """Test minting NFT, registering IP and making derivative using license tokens with custom metadata""" - license_token_ids = mint_and_approve_license_token + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) + license_token_ids = mint_and_approve_license_token( + story_client, + parent_ip_and_license_terms, + account, + ) response = story_client.IPAsset.mint_and_register_ip_and_make_derivative_with_license_tokens( spg_nft_contract=nft_collection, license_token_ids=[license_token_ids[1]], @@ -434,9 +447,14 @@ def test_mint_and_register_ip_and_make_derivative_with_license_tokens( assert isinstance(response["token_id"], int) def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( - self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms + self, + story_client: StoryClient, + nft_collection, ): """Test minting NFT, registering IP, making derivative and distributing royalty tokens with custom derivative data""" + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) response = story_client.IPAsset.mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( spg_nft_contract=nft_collection, deriv_data=DerivativeDataInput( @@ -461,9 +479,10 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( class TestSPGNFTOperations: - def test_register_derivative_ip( - self, story_client: StoryClient, parent_ip_and_license_terms, nft_collection - ): + def test_register_derivative_ip(self, story_client: StoryClient, nft_collection): + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) token_child_id = mint_by_spg( nft_collection, story_client.web3, story_client.account ) @@ -621,9 +640,12 @@ def test_register_ip_and_attach_pil_terms( def test_register_pil_terms_and_attach( self, story_client: StoryClient, - parent_ip_and_license_terms, + nft_collection, ): """Test registering PIL terms and attaching them to an existing IP with multiple license terms""" + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) response = story_client.IPAsset.register_pil_terms_and_attach( ip_id=parent_ip_and_license_terms["parent_ip_id"], license_terms_data=[ @@ -766,9 +788,12 @@ def test_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( ) def test_register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( - self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms + self, story_client: StoryClient, nft_collection ): """Test registering an existing NFT as derivative IP and distributing royalty tokens with all optional parameters""" + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) # Mint an NFT first token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) @@ -1604,20 +1629,24 @@ def test_register_derivative_ip_asset_minted_with_license_token_ids( self, story_client: StoryClient, nft_collection, - mint_and_approve_license_token, ): """Test derivative registration for already minted NFT with license_token_ids (uses register_ip_and_make_derivative_with_license_tokens internally) """ token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) - + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) + license_token_ids = mint_and_approve_license_token( + story_client, parent_ip_and_license_terms, account + ) response = story_client.IPAsset.register_derivative_ip_asset( nft=MintedNFT( type="minted", nft_contract=nft_collection, token_id=token_id, ), - license_token_ids=[mint_and_approve_license_token[0]], + license_token_ids=license_token_ids, ip_metadata=COMMON_IP_METADATA, deadline=100000, ) @@ -1696,11 +1725,16 @@ def test_register_derivative_ip_asset_mint_with_license_token_ids( self, story_client: StoryClient, nft_collection, - mint_and_approve_license_token, ): """Test derivative registration with minting new NFT and license_token_ids (uses mint_and_register_ip_and_make_derivative_with_license_tokens internally) """ + parent_ip_and_license_terms = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) + license_token_ids = mint_and_approve_license_token( + story_client, parent_ip_and_license_terms, account + ) response = story_client.IPAsset.register_derivative_ip_asset( nft=MintNFT( type="mint", @@ -1708,7 +1742,7 @@ def test_register_derivative_ip_asset_mint_with_license_token_ids( recipient=account.address, allow_duplicates=True, ), - license_token_ids=[mint_and_approve_license_token[1]], + license_token_ids=license_token_ids, ip_metadata=COMMON_IP_METADATA, ) From 52f505fc454f8e7a3ff1c5f24893842bf768205d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Dec 2025 11:16:55 +0800 Subject: [PATCH 07/12] Remove lint ignore --- src/story_protocol_python_sdk/resources/IPAsset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 7de6619..b332f7e 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1877,7 +1877,7 @@ def _handle_mint_nft_derivative_registration( token_result = ( self.mint_and_register_ip_and_make_derivative_with_license_tokens( spg_nft_contract=nft.spg_nft_contract, - license_token_ids=license_token_ids, # type: ignore + license_token_ids=cast(list[int], license_token_ids), max_rts=max_rts, recipient=nft.recipient, allow_duplicates=nft.allow_duplicates, From 86b8f7318782ed370ad397d48429d8d4fe844707 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Dec 2025 11:17:36 +0800 Subject: [PATCH 08/12] Fix the integration tests --- tests/integration/config/utils.py | 10 +++++----- tests/integration/test_integration_ip_asset.py | 5 ++--- tests/unit/resources/test_ip_asset.py | 4 ---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/integration/config/utils.py b/tests/integration/config/utils.py index 525d84b..51a9502 100644 --- a/tests/integration/config/utils.py +++ b/tests/integration/config/utils.py @@ -283,9 +283,9 @@ def mint_and_approve_license_token( story_client: StoryClient, parent_ip_and_license_terms: dict, account: LocalAccount, -): +) -> list[int]: """ - Fixture to mint and approve license tokens for derivative workflow testing. + Mint and approve license tokens for derivative workflow testing. :param story_client: The StoryClient instance. :param parent_ip_and_license_terms: A dictionary containing the parent IP ID and license terms ID. @@ -295,7 +295,7 @@ def mint_and_approve_license_token( licensor_ip_id=parent_ip_and_license_terms["parent_ip_id"], license_template=PIL_LICENSE_TEMPLATE, license_terms_id=parent_ip_and_license_terms["license_terms_id"], - amount=2, + amount=1, receiver=account.address, max_revenue_share=100, ) @@ -319,8 +319,8 @@ def mint_and_approve_license_token( def create_parent_ip_and_license_terms( story_client: StoryClient, nft_collection, account: LocalAccount -): - """Fixture to provide the parent IP and license terms""" +) -> dict[str, int]: + """Create a parent IP with license terms for testing.""" response = story_client.IPAsset.register_ip_asset( nft=MintNFT( spg_nft_contract=nft_collection, diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 74f2217..6ed8b1e 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -435,7 +435,7 @@ def test_mint_and_register_ip_and_make_derivative_with_license_tokens( ) response = story_client.IPAsset.mint_and_register_ip_and_make_derivative_with_license_tokens( spg_nft_contract=nft_collection, - license_token_ids=[license_token_ids[1]], + license_token_ids=license_token_ids, max_rts=100000000, ip_metadata=COMMON_IP_METADATA, recipient=account_2.address, @@ -1633,13 +1633,13 @@ def test_register_derivative_ip_asset_minted_with_license_token_ids( """Test derivative registration for already minted NFT with license_token_ids (uses register_ip_and_make_derivative_with_license_tokens internally) """ - token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) parent_ip_and_license_terms = create_parent_ip_and_license_terms( story_client, nft_collection, account ) license_token_ids = mint_and_approve_license_token( story_client, parent_ip_and_license_terms, account ) + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) response = story_client.IPAsset.register_derivative_ip_asset( nft=MintedNFT( type="minted", @@ -1739,7 +1739,6 @@ def test_register_derivative_ip_asset_mint_with_license_token_ids( nft=MintNFT( type="mint", spg_nft_contract=nft_collection, - recipient=account.address, allow_duplicates=True, ), license_token_ids=license_token_ids, diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 595198f..ff2f51b 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -2377,10 +2377,6 @@ def test_success_when_ip_metadata_not_provided_for_minted_nft( 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 From 3d24ee72739b7a5f6ae6b698ed779754613cdb8e Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Dec 2025 14:47:39 +0800 Subject: [PATCH 09/12] Fix the insufficient allowance error --- tests/integration/test_integration_dispute.py | 18 ++++++++++++++---- tests/integration/test_integration_royalty.py | 9 +++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_integration_dispute.py b/tests/integration/test_integration_dispute.py index fa3207a..39e11dc 100644 --- a/tests/integration/test_integration_dispute.py +++ b/tests/integration/test_integration_dispute.py @@ -2,6 +2,9 @@ import pytest +from story_protocol_python_sdk.abi.ArbitrationPolicyUMA.ArbitrationPolicyUMA_client import ( + ArbitrationPolicyUMAClient, +) from story_protocol_python_sdk.story_client import StoryClient from .setup_for_integration import account, generate_cid, web3 @@ -42,7 +45,11 @@ def target_ip_id(self, story_client: StoryClient, story_client_2: StoryClient): def dispute_id(self, story_client: StoryClient, target_ip_id): cid = generate_cid() bond_amount = 1000000000000000000 # 1 ETH in wei - + # Approve WIP tokens to the target IP + story_client.WIP.approve( + spender=ArbitrationPolicyUMAClient(web3).contract.address, + amount=bond_amount, + ) response = story_client.Dispute.raise_dispute( target_ip_id=target_ip_id, target_tag="IMPROPER_REGISTRATION", @@ -77,9 +84,12 @@ def test_counter_dispute( # Generate a CID for counter evidence counter_evidence_cid = generate_cid() - - story_client_2.WIP.deposit(amount=web3.to_wei(1, "ether")) # 1 IP - + amount = web3.to_wei(1, "ether") + story_client_2.WIP.deposit(amount=amount) # 1 IP + story_client_2.WIP.approve( + spender=target_ip_id, + amount=amount, + ) # Counter the dispute assertion with story_client_2 (the IP owner) response = story_client_2.Dispute.dispute_assertion( ip_id=target_ip_id, diff --git a/tests/integration/test_integration_royalty.py b/tests/integration/test_integration_royalty.py index 9bd06cd..6c48911 100644 --- a/tests/integration/test_integration_royalty.py +++ b/tests/integration/test_integration_royalty.py @@ -1,5 +1,8 @@ import pytest +from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( + DerivativeWorkflowsClient, +) from story_protocol_python_sdk.story_client import StoryClient from story_protocol_python_sdk.utils.constants import WIP_TOKEN_ADDRESS from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput @@ -198,6 +201,12 @@ def wrapper_derivative_with_wip(parent_ip_id, license_terms_id): # Approve SPG contract to spend WIP tokens story_client.WIP.approve(spender=spg_nft_contract, amount=amount) + derivative_workflows_address = DerivativeWorkflowsClient( + story_client.web3 + ).contract.address + story_client.WIP.approve( + spender=derivative_workflows_address, amount=amount + ) # Mint and register the derivative IP response = story_client.IPAsset.mint_and_register_ip_and_make_derivative( From 1292f4d5cfa23e3b0480676f8c0ac2ae2be4cba8 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Dec 2025 14:59:11 +0800 Subject: [PATCH 10/12] refactor: replace hardcoded deadline with constant in IPAsset registration methods --- src/story_protocol_python_sdk/resources/IPAsset.py | 5 +++-- src/story_protocol_python_sdk/utils/constants.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index b332f7e..79558ed 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -70,6 +70,7 @@ ) from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.constants import ( + DEADLINE, MAX_ROYALTY_TOKEN, ZERO_ADDRESS, ZERO_HASH, @@ -1486,7 +1487,7 @@ def register_ip_asset( license_terms_data: list[LicenseTermsDataInput] | None = None, royalty_shares: list[RoyaltyShareInput] | None = None, ip_metadata: IPMetadataInput | None = None, - deadline: int = 1000, + deadline: int = DEADLINE, tx_options: dict | None = None, ) -> RegisterIpAssetResponse: """ @@ -1683,7 +1684,7 @@ def register_derivative_ip_asset( license_token_ids: list[int] | None = None, royalty_shares: list[RoyaltyShareInput] | None = None, max_rts: int = MAX_ROYALTY_TOKEN, - deadline: int = 1000, + deadline: int = DEADLINE, ip_metadata: IPMetadataInput | None = None, tx_options: dict | None = None, ) -> RegisterDerivativeIpAssetResponse: diff --git a/src/story_protocol_python_sdk/utils/constants.py b/src/story_protocol_python_sdk/utils/constants.py index ab94749..1035c44 100644 --- a/src/story_protocol_python_sdk/utils/constants.py +++ b/src/story_protocol_python_sdk/utils/constants.py @@ -6,3 +6,5 @@ ROYALTY_POLICY_LAP_ADDRESS = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" ROYALTY_POLICY_LRP_ADDRESS = "0x9156E603C949481883B1D3355C6F1132D191FC41" WIP_TOKEN_ADDRESS = "0x1514000000000000000000000000000000000000" +# Default deadline for signature in seconds +DEADLINE = 1000 From 6ba11e5dfabd4c729a890e9f675c4f50e464abd2 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Dec 2025 15:17:56 +0800 Subject: [PATCH 11/12] test: enhance unit tests for IP asset registration by adding assertions for optional parameters and refining test cases --- tests/unit/resources/test_ip_asset.py | 237 ++++++++++++++++++++------ 1 file changed, 183 insertions(+), 54 deletions(-) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index ff2f51b..b752d15 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -2153,16 +2153,18 @@ def test_success_when_license_terms_data_and_royalty_shares_provided_for_minted_ 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][0] == ADDRESS + ) # nft_contract + assert mock_build_register_transaction.call_args[0][1] == 3 # token_id assert ( mock_build_register_transaction.call_args[0][2] == IPMetadata.from_input().get_validated_data() - ) - assert mock_distribute.call_args[0][0] == IP_ID + ) # ip_metadata + assert mock_distribute.call_args[0][0] == IP_ID # ip_id assert ( mock_distribute.call_args[0][1] == royalty_shares_obj["royalty_shares"] - ) + ) # royalty_shares assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID assert result["token_id"] == 3 @@ -2193,64 +2195,149 @@ def test_success_when_license_terms_data_and_royalty_shares_provided_for_mint_nf ) as mock_build_register_transaction, ): result = ip_asset.register_ip_asset( - nft=MintNFT( - type="mint", spg_nft_contract=ADDRESS, allow_duplicates=False - ), + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), 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][0] == ADDRESS + ) # spg_nft_contract + assert ( + mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + ) # recipient assert ( mock_build_register_transaction.call_args[0][2] - == IPMetadata.from_input(IP_METADATA).get_validated_data() - ) + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata 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 + ) # royalty_shares + assert ( + mock_build_register_transaction.call_args[0][5] is True + ) # allow_duplicates 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( + def test_success_when_license_terms_data_royalty_shares_and_all_optional_parameters_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_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.license_attachment_workflows_client, - "build_mintAndRegisterIpAndAttachPILTerms_transaction", + 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( + ip_asset.register_ip_asset( nft=MintNFT( - type="mint", spg_nft_contract=ADDRESS, allow_duplicates=False + type="mint", + spg_nft_contract=ADDRESS, + allow_duplicates=False, + recipient=ADDRESS, ), 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][1] == ADDRESS + ) # recipient assert ( mock_build_register_transaction.call_args[0][2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][5] is False + ) # allow_duplicates + + 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), + license_terms_data=LICENSE_TERMS_DATA, ) - assert mock_build_register_transaction.call_args[0][4] is False + assert ( + mock_build_register_transaction.call_args[0][0] == ADDRESS + ) # spg_nft_contract + assert ( + mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][4] is True + ) # allow_duplicates 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_and_all_optional_parameters_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, + ): + ip_asset.register_ip_asset( + nft=MintNFT( + type="mint", + spg_nft_contract=ADDRESS, + allow_duplicates=False, + recipient=ADDRESS, + ), + license_terms_data=LICENSE_TERMS_DATA, + ip_metadata=IP_METADATA, + ) + assert ( + mock_build_register_transaction.call_args[0][1] == ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][4] is False + ) # allow_duplicates + def test_success_when_license_terms_data_provided_for_minted_nft( self, ip_asset: IPAsset, @@ -2277,8 +2364,10 @@ def test_success_when_license_terms_data_provided_for_minted_nft( 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][0] == ADDRESS + ) # nft_contract + assert mock_build_register_transaction.call_args[0][1] == 3 # token_id assert ( mock_build_register_transaction.call_args[0][2] == IPMetadata.from_input(IP_METADATA).get_validated_data() @@ -2311,8 +2400,10 @@ def test_success_when_ip_metadata_provided_for_minted_nft( 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][0] == ADDRESS + ) # nft_contract + assert mock_build_register_transaction.call_args[0][1] == 3 # token_id assert ( mock_build_register_transaction.call_args[0][2] == IPMetadata.from_input(IP_METADATA).get_validated_data() @@ -2320,7 +2411,7 @@ 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( + def test_success_when_with_partial_ip_metadata_provided_for_minted_nft( self, ip_asset: IPAsset, mock_parse_ip_registered_event, @@ -2342,18 +2433,14 @@ def test_success_when_ip_metadata_provided_for_minted_nft_without_ip_metadata( return_value={"tx_hash": TX_HASH.hex()}, ) as mock_build_register_transaction, ): - result = ip_asset.register_ip_asset( + 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 + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input(partialIpMetadata).get_validated_data() + ) # ip_metadata def test_success_when_ip_metadata_not_provided_for_minted_nft( self, @@ -2374,16 +2461,15 @@ def test_success_when_ip_metadata_not_provided_for_minted_nft( return_value={"tx_hash": TX_HASH.hex()}, ) as mock_build_register_transaction, ): - result = ip_asset.register_ip_asset( + ip_asset.register_ip_asset( nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), ) - 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 + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata - def test_success_when_ip_metadata_provided_for_mint_nft( + def test_success_when_all_optional_parameters_provided_for_mint_nft( self, ip_asset: IPAsset, mock_parse_ip_registered_event, @@ -2404,20 +2490,63 @@ def test_success_when_ip_metadata_provided_for_mint_nft( ): result = ip_asset.register_ip_asset( nft=MintNFT( - type="mint", spg_nft_contract=ADDRESS, allow_duplicates=False + type="mint", + spg_nft_contract=ADDRESS, + allow_duplicates=False, + recipient=ADDRESS, ), + 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][0] == ADDRESS + ) # spg_nft_contract + assert ( + mock_build_register_transaction.call_args[0][1] == ADDRESS + ) # recipient 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 + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][3] is False + ) # allow_duplicates assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID assert result["token_id"] == 3 + def test_success_when_all_optional_parameters_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.registration_workflows_client, + "build_mintAndRegisterIp_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ) as mock_build_register_transaction, + ): + ip_asset.register_ip_asset( + nft=MintNFT(type="mint", spg_nft_contract=ADDRESS), + ) + assert ( + mock_build_register_transaction.call_args[0][1] == ACCOUNT_ADDRESS + ) # recipient + assert ( + mock_build_register_transaction.call_args[0][2] + == IPMetadata.from_input().get_validated_data() + ) # ip_metadata + assert ( + mock_build_register_transaction.call_args[0][3] is True + ) # allow_duplicates + class TestRegisterDerivativeIpAsset: def test_throw_error_when_deriv_data_is_not_provided_and_royalty_shares_are_provided_for_minted_nft( @@ -2566,7 +2695,7 @@ def test_success_when_deriv_data_and_royalty_shares_are_provided_for_mint_nft( assert result["token_id"] == 3 assert result["royalty_vault"] == ADDRESS - def test_success_when_deriv_data_royalty_shares_recipient_and_ip_metadata_are_provided_for_mint_nft( + def test_success_when_deriv_data_royalty_shares_all_optional_parameters_are_provided_for_mint_nft( self, ip_asset: IPAsset, mock_license_registry_client, @@ -2707,7 +2836,7 @@ def test_success_when_deriv_data_only_are_provided_for_mint_nft( assert result["ip_id"] == IP_ID assert result["token_id"] == 3 - def test_success_when_deriv_data_recipient_allow_duplicates_and_ip_metadata_are_provided_for_mint_nft( + def test_success_when_deriv_data_all_optional_parameters_are_provided_for_mint_nft( self, ip_asset: IPAsset, mock_parse_ip_registered_event, @@ -2796,7 +2925,7 @@ def test_success_when_license_token_ids_only_are_provided_for_minted_nft( assert result["ip_id"] == IP_ID assert result["token_id"] == 3 - def test_success_when_license_token_ids_ip_metadata_and_max_rts_are_provided_for_minted_nft( + def test_success_when_license_token_ids_all_optional_parameters_are_provided_for_minted_nft( self, ip_asset: IPAsset, mock_parse_ip_registered_event, @@ -2878,7 +3007,7 @@ def test_success_when_license_token_ids_only_are_provided_for_mint_nft( assert result["ip_id"] == IP_ID assert result["token_id"] == 3 - def test_success_when_license_token_ids_ip_metadata_and_max_rts_are_provided_for_mint_nft( + def test_success_when_license_token_ids_all_optional_parameters_are_provided_for_mint_nft( self, ip_asset: IPAsset, mock_parse_ip_registered_event, From 2c3efea272ac6a45e798aa763b410eba99be7a20 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 5 Dec 2025 15:23:45 +0800 Subject: [PATCH 12/12] refactor: remove redundant test for IP asset registration when metadata is not provided --- tests/unit/resources/test_ip_asset.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index b752d15..16ca846 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -2442,33 +2442,6 @@ def test_success_when_with_partial_ip_metadata_provided_for_minted_nft( == IPMetadata.from_input(partialIpMetadata).get_validated_data() ) # ip_metadata - 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, - ): - ip_asset.register_ip_asset( - nft=MintedNFT(type="minted", nft_contract=ADDRESS, token_id=3), - ) - assert ( - mock_build_register_transaction.call_args[0][2] - == IPMetadata.from_input().get_validated_data() - ) # ip_metadata - def test_success_when_all_optional_parameters_provided_for_mint_nft( self, ip_asset: IPAsset,