From 459d31548105251b3eb736adb974430c033d0822 Mon Sep 17 00:00:00 2001 From: Andrew Chung Date: Fri, 23 May 2025 20:35:17 -0700 Subject: [PATCH 1/2] Added mint_by_spg to integration test setup --- tests/integration/setup_for_integration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/setup_for_integration.py b/tests/integration/setup_for_integration.py index d77ff90..750ea54 100644 --- a/tests/integration/setup_for_integration.py +++ b/tests/integration/setup_for_integration.py @@ -30,7 +30,8 @@ setup_royalty_vault, EVEN_SPLIT_GROUP_POOL, ROYALTY_POLICY_LRP, - CORE_METADATA_MODULE + CORE_METADATA_MODULE, + mint_by_spg ) # Load environment variables @@ -91,5 +92,6 @@ def story_client_2(): 'private_key', 'private_key_2', 'EVEN_SPLIT_GROUP_POOL', - 'ROYALTY_POLICY_LRP' + 'ROYALTY_POLICY_LRP', + 'mint_by_spg' ] \ No newline at end of file From 67a3e2a3b8d335a68da30fe0e0b286b972cd1175 Mon Sep 17 00:00:00 2001 From: Andrew Chung Date: Fri, 23 May 2025 20:35:33 -0700 Subject: [PATCH 2/2] Added register_ip_and_attach_pil_terms() spg fn --- .../resources/IPAsset.py | 489 ++++++++++-------- .../integration/test_integration_ip_asset.py | 175 ++++++- 2 files changed, 443 insertions(+), 221 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index df4a66f..a86e7c9 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -218,76 +218,6 @@ def register_derivative( except Exception as e: raise ValueError("Failed to register derivative") from e - def _validate_max_rts(self, max_rts: int): - """ - Validates the maximum number of royalty tokens. - - :param max_rts int: The maximum number of royalty tokens - :raises ValueError: If max_rts is invalid - """ - if not isinstance(max_rts, int): - raise ValueError("The maxRts must be a number.") - if max_rts < 0 or max_rts > 100_000_000: - raise ValueError("The maxRts must be greater than 0 and less than 100,000,000.") - - def _validate_derivative_data(self, derivative_data: dict) -> dict: - """ - Validates the derivative data and returns processed internal data. - - :param derivative_data dict: The derivative data to validate - :return dict: The processed internal derivative data - :raises ValueError: If validation fails - """ - internal_data = { - 'childIpId': derivative_data['childIpId'], - 'parentIpIds': derivative_data['parentIpIds'], - 'licenseTermsIds': [int(id) for id in derivative_data['licenseTermsIds']], - 'licenseTemplate': (derivative_data.get('licenseTemplate') - if derivative_data.get('licenseTemplate') is not None - else self.pi_license_template_client.contract.address), - 'royaltyContext': ZERO_ADDRESS, - 'maxMintingFee': int(derivative_data.get('maxMintingFee', 0)), - 'maxRts': int(derivative_data.get('maxRts', 0)), - 'maxRevenueShare': int(derivative_data.get('maxRevenueShare', 0)) - } - - if not internal_data['parentIpIds']: - raise ValueError("The parent IP IDs must be provided.") - - if not internal_data['licenseTermsIds']: - raise ValueError("The license terms IDs must be provided.") - - if len(internal_data['parentIpIds']) != len(internal_data['licenseTermsIds']): - raise ValueError("The number of parent IP IDs must match the number of license terms IDs.") - - if internal_data['maxMintingFee'] < 0: - raise ValueError("The maxMintingFee must be greater than 0.") - - self._validate_max_rts(internal_data['maxRts']) - - for parent_id, terms_id in zip(internal_data['parentIpIds'], internal_data['licenseTermsIds']): - if not self._is_registered(parent_id): - raise ValueError(f"The parent IP with id {parent_id} is not registered.") - - if not self.license_registry_client.hasIpAttachedLicenseTerms( - parent_id, internal_data['licenseTemplate'], terms_id - ): - raise ValueError( - f"License terms id {terms_id} must be attached to the parent ipId " - f"{parent_id} before registering derivative." - ) - - royalty_percent = self.license_registry_client.getRoyaltyPercent( - parent_id, internal_data['licenseTemplate'], terms_id - ) - if internal_data['maxRevenueShare'] != 0 and royalty_percent > internal_data['maxRevenueShare']: - raise ValueError( - f"The royalty percent for the parent IP with id {parent_id} is greater " - f"than the maximum revenue share {internal_data['maxRevenueShare']}." - ) - - return internal_data - def register_derivative_with_license_tokens( self, child_ip_id: str, @@ -338,28 +268,6 @@ def register_derivative_with_license_tokens( except Exception as e: raise ValueError(f"Failed to register derivative with license tokens: {str(e)}") - def _validate_license_token_ids(self, license_token_ids: list) -> list: - """ - Validates the license token IDs and checks ownership. - - :param license_token_ids list: The IDs of the license tokens to validate - :return list: The validated and converted license token IDs - :raises ValueError: If validation fails - """ - if not license_token_ids: - raise ValueError("License token IDs must be provided.") - - # Convert all IDs to integers - license_token_ids = [int(id) for id in license_token_ids] - - # Validate ownership of each token - for token_id in license_token_ids: - token_owner = self.license_token_client.ownerOf(token_id) - if not token_owner or token_owner.lower() != self.account.address.lower(): - raise ValueError(f"License token id {token_id} must be owned by the caller.") - - return license_token_ids - def mint_and_register_ip_asset_with_pil_terms( self, spg_nft_contract: str, @@ -577,129 +485,178 @@ def mint_and_register_ip( except Exception as e: raise ValueError(f"Failed to mint and register IP: {str(e)}") - # def registerIpAndAttachPilTerms(self, nft_contract: str, token_id: int, license_terms_data: dict, ip_metadata: dict = None, deadline: int = None, tx_options: dict = None) -> dict: - # """ - # Register a given NFT as an IP and attach Programmable IP License Terms. - # - # :param nft_contract str: The address of the NFT collection. - # :param token_id int: The ID of the NFT. - # :param license_terms_data dict: The PIL terms and licensing configuration data to be attached to the IP. - # :param terms dict: The PIL terms to be used for the licensing. - # :param transferable bool: Indicates whether the license is transferable or not. - # :param royalty_policy str: The address of the royalty policy contract which required to StoryProtocol in advance. - # :param minting_fee int: The fee to be paid when minting a license. - # :param expiration int: The expiration period of the license. - # :param commercial_use bool: Indicates whether the work can be used commercially or not. - # :param commercial_attribution bool: Whether attribution is required when reproducing the work commercially or not. - # :param commercializer_checker str: Commercializers that are allowed to commercially exploit the work. - # :param commercializer_checker_data str: The data to be passed to the commercializer checker contract. - # :param commercial_rev_share int: Percentage of revenue that must be shared with the licensor. - # :param commercial_rev_ceiling int: The maximum revenue that can be generated from the commercial use of the work. - # :param derivatives_allowed bool: Indicates whether the licensee can create derivatives of his work or not. - # :param derivatives_attribution bool: Indicates whether attribution is required for derivatives of the work or not. - # :param derivatives_approval bool: Indicates whether the licensor must approve derivatives of the work before they can be linked. - # :param derivatives_reciprocal bool: Indicates whether the licensee must license derivatives under the same terms. - # :param derivative_rev_ceiling int: The maximum revenue that can be generated from the derivative use of the work. - # :param currency str: The ERC20 token to be used to pay the minting fee. - # :param uri str: The URI of the license terms. - # :param licensing_config dict: The PIL terms and licensing configuration data to attach to the IP. - # :param is_set bool: Whether the configuration is set or not. - # :param minting_fee int: The minting fee to be paid when minting license tokens. - # :param licensing_hook str: The hook contract address for the licensing module. - # :param hook_data str: The data to be used by the licensing hook. - # :param commercial_rev_share int: The commercial revenue share percentage. - # :param disabled bool: Whether the licensing is disabled or not. - # :param expect_minimum_group_reward_share int: The minimum percentage of the group's reward share. - # :param expect_group_reward_pool str: The address of the expected group reward pool. - # :param ip_metadata dict: [Optional] The metadata for the newly registered IP. - # :param ip_metadata_uri str: [Optional] The URI of the metadata for the IP. - # :param ip_metadata_hash str: [Optional] The hash of the metadata for the IP. - # :param nft_metadata_uri str: [Optional] The URI of the metadata for the NFT. - # :param nft_metadata_hash str: [Optional] The hash of the metadata for the IP NFT. - # :param deadline int: [Optional] The deadline for the signature in milliseconds. - # :param tx_options dict: [Optional] The transaction options. - # :return dict: A dictionary with the transaction hash, license terms ID, and IP ID. - # """ - # try: - # ip_id = self._get_ip_id(nft_contract, token_id) - # if self._is_registered(ip_id): - # raise ValueError(f"The NFT with id {token_id} is already registered as IP.") + def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, license_terms_data: dict, ip_metadata: dict = None, deadline: int = None, tx_options: dict = None) -> dict: + """ + Register a given NFT as an IP and attach Programmable IP License Terms. + + :param nft_contract str: The address of the NFT collection. + :param token_id int: The ID of the NFT. + :param license_terms_data dict: The PIL terms and licensing configuration data to be attached to the IP. + :param terms dict: The PIL terms to be used for the licensing. + :param transferable bool: Indicates whether the license is transferable or not. + :param royalty_policy str: The address of the royalty policy contract which required to StoryProtocol in advance. + :param minting_fee int: The fee to be paid when minting a license. + :param expiration int: The expiration period of the license. + :param commercial_use bool: Indicates whether the work can be used commercially or not. + :param commercial_attribution bool: Whether attribution is required when reproducing the work commercially or not. + :param commercializer_checker str: Commercializers that are allowed to commercially exploit the work. + :param commercializer_checker_data str: The data to be passed to the commercializer checker contract. + :param commercial_rev_share int: Percentage of revenue that must be shared with the licensor. + :param commercial_rev_ceiling int: The maximum revenue that can be generated from the commercial use of the work. + :param derivatives_allowed bool: Indicates whether the licensee can create derivatives of his work or not. + :param derivatives_attribution bool: Indicates whether attribution is required for derivatives of the work or not. + :param derivatives_approval bool: Indicates whether the licensor must approve derivatives of the work before they can be linked. + :param derivatives_reciprocal bool: Indicates whether the licensee must license derivatives under the same terms. + :param derivative_rev_ceiling int: The maximum revenue that can be generated from the derivative use of the work. + :param currency str: The ERC20 token to be used to pay the minting fee. + :param uri str: The URI of the license terms. + :param licensing_config dict: The PIL terms and licensing configuration data to attach to the IP. + :param is_set bool: Whether the configuration is set or not. + :param minting_fee int: The minting fee to be paid when minting license tokens. + :param licensing_hook str: The hook contract address for the licensing module. + :param hook_data str: The data to be used by the licensing hook. + :param commercial_rev_share int: The commercial revenue share percentage. + :param disabled bool: Whether the licensing is disabled or not. + :param expect_minimum_group_reward_share int: The minimum percentage of the group's reward share. + :param expect_group_reward_pool str: The address of the expected group reward pool. + :param ip_metadata dict: [Optional] The metadata for the newly registered IP. + :param ip_metadata_uri str: [Optional] The URI of the metadata for the IP. + :param ip_metadata_hash str: [Optional] The hash of the metadata for the IP. + :param nft_metadata_uri str: [Optional] The URI of the metadata for the NFT. + :param nft_metadata_hash str: [Optional] The hash of the metadata for the IP NFT. + :param deadline int: [Optional] The deadline for the signature in milliseconds. + :param tx_options dict: [Optional] The transaction options. + :return dict: A dictionary with the transaction hash, license terms ID, and IP ID. + """ + try: + ip_id = self._get_ip_id(nft_contract, token_id) + if self._is_registered(ip_id): + raise ValueError(f"The NFT with id {token_id} is already registered as IP.") - # license_terms, validated_licensing_config = self.license_terms_util.validate_license_terms_data(license_terms_data) + license_terms = [] + for term in license_terms_data: + validated_term = self.license_terms_util.validate_license_terms( + term['terms'] + ) + validated_licensing_config = ( + self.license_terms_util.validate_licensing_config( + term['licensing_config'] + ) + ) - # calculated_deadline = self._get_deadline(deadline=deadline) + camelcase_term = { + 'transferable': term['terms']['transferable'], + 'royaltyPolicy': term['terms']['royalty_policy'], + 'defaultMintingFee': term['terms']['default_minting_fee'], + 'expiration': term['terms']['expiration'], + 'commercialUse': term['terms']['commercial_use'], + 'commercialAttribution': term['terms']['commercial_attribution'], + 'commercializerChecker': term['terms']['commercializer_checker'], + 'commercializerCheckerData': term['terms']['commercializer_checker_data'], + 'commercialRevShare': term['terms']['commercial_rev_share'], + 'commercialRevCeiling': term['terms']['commercial_rev_ceiling'], + 'derivativesAllowed': term['terms']['derivatives_allowed'], + 'derivativesAttribution': term['terms']['derivatives_attribution'], + 'derivativesApproval': term['terms']['derivatives_approval'], + 'derivativesReciprocal': term['terms']['derivatives_reciprocal'], + 'derivativeRevCeiling': term['terms']['derivative_rev_ceiling'], + 'currency': term['terms']['currency'], + 'uri': term['terms']['uri'] + } - # # Get permission signature for all required permissions - # signature = self._get_permission_signature( - # ip_id=ip_id, - # deadline=calculated_deadline, - # permissions=[ - # { - # 'signer': self.license_attachment_workflows_client.contract.address, - # 'to': self.core_metadata_module_client.contract.address, - # 'permission': 1, # ALLOW - # 'func': "setAll(address,string,bytes32,bytes32)" - # }, - # { - # 'signer': self.license_attachment_workflows_client.contract.address, - # 'to': self.licensing_module_client.contract.address, - # 'permission': 1, # ALLOW - # 'func': "attachLicenseTerms(address,address,uint256)" - # }, - # { - # 'signer': self.license_attachment_workflows_client.contract.address, - # 'to': self.licensing_module_client.contract.address, - # 'permission': 1, # ALLOW - # 'func': "setLicensingConfig(address,address,uint256)" - # } - # ] - # ) + camelcase_config = { + 'isSet': validated_licensing_config['is_set'], + 'mintingFee': validated_licensing_config['minting_fee'], + 'hookData': validated_licensing_config['hook_data'], + 'licensingHook': validated_licensing_config['licensing_hook'], + 'commercialRevShare': validated_licensing_config['commercial_rev_share'], + 'disabled': validated_licensing_config['disabled'], + 'expectMinimumGroupRewardShare': validated_licensing_config['expect_minimum_group_reward_share'], + 'expectGroupRewardPool': validated_licensing_config['expect_group_reward_pool'] + } - # metadata = { - # 'ipMetadataURI': "", - # 'ipMetadataHash': ZERO_HASH, - # 'nftMetadataURI': "", - # 'nftMetadataHash': ZERO_HASH, - # } + license_terms.append({ + 'terms': camelcase_term, + 'licensingConfig': camelcase_config + }) - # if ip_metadata: - # metadata.update({ - # 'ipMetadataURI': ip_metadata.get('ip_metadata_uri', ""), - # 'ipMetadataHash': ip_metadata.get('ip_metadata_hash', ZERO_HASH), - # 'nftMetadataURI': ip_metadata.get('nft_metadata_uri', ""), - # 'nftMetadataHash': ip_metadata.get('nft_metadata_hash', ZERO_HASH), - # }) + calculated_deadline = self.sign_util.get_deadline(deadline=deadline) - # response = build_and_send_transaction( - # self.web3, - # self.account, - # self.license_attachment_workflows_client.build_registerIpAndAttachPILTerms_transaction, - # nft_contract, - # token_id, - # metadata, - # license_terms, - # { - # 'signer': self.web3.to_checksum_address(self.account.address), - # 'deadline': calculated_deadline, - # 'signature': signature - # }, - # tx_options=tx_options - # ) + # Get permission signature for all required permissions + signature_response = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=self.web3.to_bytes(hexstr=ZERO_HASH), + permissions=[ + { + 'ipId': ip_id, + 'signer': self.license_attachment_workflows_client.contract.address, + 'to': self.core_metadata_module_client.contract.address, + 'permission': 1, # ALLOW + 'func': "setAll(address,string,bytes32,bytes32)" + }, + { + 'ipId': ip_id, + 'signer': self.license_attachment_workflows_client.contract.address, + 'to': self.licensing_module_client.contract.address, + 'permission': 1, # ALLOW + 'func': "attachLicenseTerms(address,address,uint256)" + }, + { + 'ipId': ip_id, + 'signer': self.license_attachment_workflows_client.contract.address, + 'to': self.licensing_module_client.contract.address, + 'permission': 1, # ALLOW + 'func': "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))" + } + ] + ) - # ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) - # license_terms_id = self._parse_tx_license_terms_attached_event(response['tx_receipt']) + metadata = { + 'ipMetadataURI': "", + 'ipMetadataHash': ZERO_HASH, + 'nftMetadataURI': "", + 'nftMetadataHash': ZERO_HASH, + } - # return { - # 'txHash': response['txHash'], - # 'ipId': ip_registered['ipId'], - # 'licenseTermsId': license_terms_id, - # 'tokenId': ip_registered['tokenId'] - # } - # - # except Exception as e: - # raise e + if ip_metadata: + metadata.update({ + 'ipMetadataURI': ip_metadata.get('ip_metadata_uri', ""), + 'ipMetadataHash': ip_metadata.get('ip_metadata_hash', ZERO_HASH), + 'nftMetadataURI': ip_metadata.get('nft_metadata_uri', ""), + 'nftMetadataHash': ip_metadata.get('nft_metadata_hash', ZERO_HASH), + }) - # def registerDerivativeIp( + response = build_and_send_transaction( + self.web3, + self.account, + self.license_attachment_workflows_client.build_registerIpAndAttachPILTerms_transaction, + nft_contract, + token_id, + metadata, + license_terms, + { + 'signer': self.web3.to_checksum_address(self.account.address), + 'deadline': calculated_deadline, + 'signature': self.web3.to_bytes(hexstr=signature_response['signature']) + }, + tx_options=tx_options + ) + + ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) + license_terms_ids = self._parse_tx_license_terms_attached_event(response['tx_receipt']) + + return { + 'tx_hash': response['tx_hash'], + 'ip_id': ip_registered['ip_id'], + 'license_terms_ids': license_terms_ids, + 'token_id': ip_registered['token_id'] + } + + except Exception as e: + raise e + + # def register_derivative_ip( # self, # nft_contract: str, # token_id: int, @@ -711,7 +668,7 @@ def mint_and_register_ip( # """ # Register the given NFT as a derivative IP with metadata without using # license tokens. - # + # :param nft_contract str: The address of the NFT collection. # :param token_id int: The ID of the NFT. # :param deriv_data dict: The derivative data for registerDerivative. @@ -732,14 +689,14 @@ def mint_and_register_ip( # raise ValueError( # f"The NFT with id {token_id} is already registered as IP." # ) - # + # if len(deriv_data['parentIpIds']) != len(deriv_data['licenseTermsIds']): # raise ValueError( # "Parent IP IDs and license terms IDs must match in quantity." # ) # if len(deriv_data['parentIpIds']) not in [1, 2]: # raise ValueError("There can only be 1 or 2 parent IP IDs.") - # + # for parent_ip_id, license_terms_id in zip( # deriv_data['parentIpIds'], # deriv_data['licenseTermsIds'] @@ -754,7 +711,7 @@ def mint_and_register_ip( # f"the parent ipId {parent_ip_id} before registering " # f"derivative." # ) - # + # calculated_deadline = self._get_deadline(deadline=deadline) # sig_register_signature = self._get_signature( # ip_id, @@ -763,7 +720,7 @@ def mint_and_register_ip( # "registerDerivative(address,address[],uint256[],address,bytes)", # 2 # ) - # + # req_object = { # 'nftContract': nft_contract, # 'tokenId': token_id, @@ -792,14 +749,14 @@ def mint_and_register_ip( # 'signature': ZERO_HASH, # }, # } - # + # if metadata: # req_object['metadata'].update({ # 'metadataURI': metadata.get('metadataURI', ""), # 'metadataHash': metadata.get('metadataHash', ZERO_HASH), # 'nftMetadataHash': metadata.get('nftMetadataHash', ZERO_HASH), # }) - # + # signature = self._get_signature( # ip_id, # self.core_metadata_module_client.contract.address, @@ -807,13 +764,13 @@ def mint_and_register_ip( # "setAll(address,string,bytes32,bytes32)", # 1 # ) - # + # req_object['sigMetadata'] = { # 'signer': self.web3.to_checksum_address(self.account.address), # 'deadline': calculated_deadline, # 'signature': signature, # } - # + # response = build_and_send_transaction( # self.web3, # self.account, @@ -826,17 +783,109 @@ def mint_and_register_ip( # req_object['sigRegister'], # tx_options=tx_options # ) - # + # ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) - # + # return { - # 'txHash': response['txHash'], - # 'ipId': ip_registered['ipId'] + # 'tx_hash': response['tx_hash'], + # 'ip_id': ip_registered['ip_id'] # } - # + # except Exception as e: # raise e + def _validate_max_rts(self, max_rts: int): + """ + Validates the maximum number of royalty tokens. + + :param max_rts int: The maximum number of royalty tokens + :raises ValueError: If max_rts is invalid + """ + if not isinstance(max_rts, int): + raise ValueError("The maxRts must be a number.") + if max_rts < 0 or max_rts > 100_000_000: + raise ValueError("The maxRts must be greater than 0 and less than 100,000,000.") + + def _validate_derivative_data(self, derivative_data: dict) -> dict: + """ + Validates the derivative data and returns processed internal data. + + :param derivative_data dict: The derivative data to validate + :return dict: The processed internal derivative data + :raises ValueError: If validation fails + """ + internal_data = { + 'childIpId': derivative_data['childIpId'], + 'parentIpIds': derivative_data['parentIpIds'], + 'licenseTermsIds': [int(id) for id in derivative_data['licenseTermsIds']], + 'licenseTemplate': (derivative_data.get('licenseTemplate') + if derivative_data.get('licenseTemplate') is not None + else self.pi_license_template_client.contract.address), + 'royaltyContext': ZERO_ADDRESS, + 'maxMintingFee': int(derivative_data.get('maxMintingFee', 0)), + 'maxRts': int(derivative_data.get('maxRts', 0)), + 'maxRevenueShare': int(derivative_data.get('maxRevenueShare', 0)) + } + + if not internal_data['parentIpIds']: + raise ValueError("The parent IP IDs must be provided.") + + if not internal_data['licenseTermsIds']: + raise ValueError("The license terms IDs must be provided.") + + if len(internal_data['parentIpIds']) != len(internal_data['licenseTermsIds']): + raise ValueError("The number of parent IP IDs must match the number of license terms IDs.") + + if internal_data['maxMintingFee'] < 0: + raise ValueError("The maxMintingFee must be greater than 0.") + + self._validate_max_rts(internal_data['maxRts']) + + for parent_id, terms_id in zip(internal_data['parentIpIds'], internal_data['licenseTermsIds']): + if not self._is_registered(parent_id): + raise ValueError(f"The parent IP with id {parent_id} is not registered.") + + if not self.license_registry_client.hasIpAttachedLicenseTerms( + parent_id, internal_data['licenseTemplate'], terms_id + ): + raise ValueError( + f"License terms id {terms_id} must be attached to the parent ipId " + f"{parent_id} before registering derivative." + ) + + royalty_percent = self.license_registry_client.getRoyaltyPercent( + parent_id, internal_data['licenseTemplate'], terms_id + ) + if internal_data['maxRevenueShare'] != 0 and royalty_percent > internal_data['maxRevenueShare']: + raise ValueError( + f"The royalty percent for the parent IP with id {parent_id} is greater " + f"than the maximum revenue share {internal_data['maxRevenueShare']}." + ) + + return internal_data + + def _validate_license_token_ids(self, license_token_ids: list) -> list: + """ + Validates the license token IDs and checks ownership. + + :param license_token_ids list: The IDs of the license tokens to validate + :return list: The validated and converted license token IDs + :raises ValueError: If validation fails + """ + if not license_token_ids: + raise ValueError("License token IDs must be provided.") + + # Convert all IDs to integers + license_token_ids = [int(id) for id in license_token_ids] + + # Validate ownership of each token + for token_id in license_token_ids: + token_owner = self.license_token_client.ownerOf(token_id) + if not token_owner or token_owner.lower() != self.account.address.lower(): + raise ValueError(f"License token id {token_id} must be owned by the caller.") + + return license_token_ids + def _get_ip_id(self, token_contract: str, token_id: int) -> str: """ Get the IP ID for a given token. diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 9599782..01b791b 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -12,7 +12,9 @@ MockERC20, ZERO_ADDRESS, ROYALTY_POLICY, - PIL_LICENSE_TEMPLATE + PIL_LICENSE_TEMPLATE, + WIP_TOKEN_ADDRESS, + mint_by_spg ) class TestIPAssetRegistration: @@ -227,3 +229,174 @@ def test_mint_register_ip(self, story_client, nft_collection): spg_nft_contract=nft_collection, ip_metadata=metadata ) + +class TestSPGNFTOperations: + @pytest.fixture(scope="module") + def nft_collection(self, story_client): + tx_data = story_client.NFTClient.create_nft_collection( + name="test-collection", + symbol="TEST", + max_supply=100, + is_public_minting=True, + mint_open=True, + contract_uri="test-uri", + mint_fee_recipient=account.address, + ) + return tx_data['nft_contract'] + + @pytest.fixture(scope="module") + def parent_ip_and_license_terms(self, story_client, nft_collection): + response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( + spg_nft_contract=nft_collection, + terms=[{ + 'terms': { + 'transferable': True, + 'royalty_policy': ROYALTY_POLICY, + 'default_minting_fee': 0, + 'expiration': 0, + 'commercial_use': True, + 'commercial_attribution': False, + 'commercializer_checker': ZERO_ADDRESS, + 'commercializer_checker_data': ZERO_ADDRESS, + 'commercial_rev_share': 90, + 'commercial_rev_ceiling': 0, + 'derivatives_allowed': True, + 'derivatives_attribution': True, + 'derivatives_approval': False, + 'derivatives_reciprocal': True, + 'derivative_rev_ceiling': 0, + 'currency': MockERC20, + 'uri': "" + }, + 'licensing_config': { + 'is_set': True, + 'minting_fee': 0, + 'hook_data': ZERO_ADDRESS, + 'licensing_hook': ZERO_ADDRESS, + 'commercial_rev_share': 0, + 'disabled': False, + 'expect_minimum_group_reward_share': 0, + 'expect_group_reward_pool': ZERO_ADDRESS + } + }] + ) + + return { + 'parent_ip_id': response['ip_id'], + 'license_terms_id': response['license_terms_ids'][0] + } + # def test_register_ip_asset_with_metadata(self, story_client, nft_collection): + # token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account, "test-metadata") + + # response = story_client.IPAsset.register( + # nft_contract=nft_collection, + # token_id=token_id, + # ip_metadata={ + # 'ip_metadata_uri': "test-uri", + # 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash")), + # 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) + # }, + # deadline=1000 + # ) + + # assert 'ip_id' in response + # assert isinstance(response['ip_id'], str) + # assert response['ip_id'] != '' + + # def test_register_derivative_ip(self, story_client, parent_ip_id, license_terms_id): + # token_child_id = mint_by_spg(MockERC721, story_client.web3, story_client.account) + + # result = story_client.IPAsset.register_derivative_ip( + # nft_contract=MockERC721, + # token_id=token_child_id, + # deriv_data={ + # 'parentIpIds': [parent_ip_id], + # 'licenseTermsIds': [license_terms_id], + # 'maxMintingFee': 0, + # 'maxRts': 5 * 10**6, + # 'maxRevenueShare': 0 + # }, + # deadline=1000, + # tx_options={'waitForTransaction': True} + # ) + + # assert isinstance(result['tx_hash'], str) and result['tx_hash'] + # assert isinstance(result['ip_id'], str) and result['ip_id'] + + def test_register_ip_and_attach_pil_terms(self, story_client, nft_collection, parent_ip_and_license_terms): + token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) + + # Register IP and attach PIL terms + result = story_client.IPAsset.register_ip_and_attach_pil_terms( + nft_contract=nft_collection, + token_id=token_id, + deadline=1000, + license_terms_data=[ + { + 'terms': { + 'transferable': True, + 'royalty_policy': ZERO_ADDRESS, + 'default_minting_fee': 0, + 'expiration': 0, + 'commercial_use': False, + 'commercial_attribution': False, + 'commercializer_checker': ZERO_ADDRESS, + 'commercializer_checker_data': ZERO_ADDRESS, + 'commercial_rev_share': 0, + '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': '', + }, + 'licensing_config': { + 'is_set': True, + 'minting_fee': 0, + 'licensing_hook': ZERO_ADDRESS, + 'hook_data': ZERO_ADDRESS, + 'commercial_rev_share': 0, + 'disabled': False, + 'expect_minimum_group_reward_share': 0, + 'expect_group_reward_pool': ZERO_ADDRESS, + } + }, + { + 'terms': { + '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_ADDRESS, + 'commercial_rev_share': 0, + '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 case', + }, + 'licensing_config': { + 'is_set': True, + 'minting_fee': 10000, + 'licensing_hook': ZERO_ADDRESS, + 'hook_data': ZERO_ADDRESS, + 'commercial_rev_share': 0, + 'disabled': False, + 'expect_minimum_group_reward_share': 0, + 'expect_group_reward_pool': ZERO_ADDRESS, + } + } + ] + ) + + assert isinstance(result['tx_hash'], str) and result['tx_hash'] + assert isinstance(result['ip_id'], str) and result['ip_id'] + assert isinstance(result['license_terms_ids'], list) and result['license_terms_ids']