From 756f807e52f297101c19fa2f7f81322a1162f69d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 9 Jan 2026 16:12:46 +0800 Subject: [PATCH 01/52] feat: add multicall functionality to DerivativeWorkflows and RoyaltyTokenDistributionWorkflows clients, and update SPGNFTImpl client with publicMinting method --- .../abi/DerivativeWorkflows/DerivativeWorkflows_client.py | 6 ++++++ .../RoyaltyTokenDistributionWorkflows_client.py | 6 ++++++ .../abi/SPGNFTImpl/SPGNFTImpl_client.py | 3 +++ src/story_protocol_python_sdk/scripts/config.json | 8 +++++--- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py b/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py index 5881aca0..dd065977 100644 --- a/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py @@ -101,6 +101,12 @@ def build_mintAndRegisterIpAndMakeDerivativeWithLicenseTokens_transaction( ).build_transaction(tx_params) ) + def multicall(self, data): + return self.contract.functions.multicall(data).transact() + + def build_multicall_transaction(self, data, tx_params): + return self.contract.functions.multicall(data).build_transaction(tx_params) + def registerIpAndMakeDerivative( self, nftContract, tokenId, derivData, ipMetadata, sigMetadataAndRegister ): diff --git a/src/story_protocol_python_sdk/abi/RoyaltyTokenDistributionWorkflows/RoyaltyTokenDistributionWorkflows_client.py b/src/story_protocol_python_sdk/abi/RoyaltyTokenDistributionWorkflows/RoyaltyTokenDistributionWorkflows_client.py index 651e3bbf..a33e4c69 100644 --- a/src/story_protocol_python_sdk/abi/RoyaltyTokenDistributionWorkflows/RoyaltyTokenDistributionWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/RoyaltyTokenDistributionWorkflows/RoyaltyTokenDistributionWorkflows_client.py @@ -126,6 +126,12 @@ def build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transacti tx_params ) + def multicall(self, data): + return self.contract.functions.multicall(data).transact() + + def build_multicall_transaction(self, data, tx_params): + return self.contract.functions.multicall(data).build_transaction(tx_params) + def registerIpAndAttachPILTermsAndDeployRoyaltyVault( self, nftContract, diff --git a/src/story_protocol_python_sdk/abi/SPGNFTImpl/SPGNFTImpl_client.py b/src/story_protocol_python_sdk/abi/SPGNFTImpl/SPGNFTImpl_client.py index 4490654e..e29db284 100644 --- a/src/story_protocol_python_sdk/abi/SPGNFTImpl/SPGNFTImpl_client.py +++ b/src/story_protocol_python_sdk/abi/SPGNFTImpl/SPGNFTImpl_client.py @@ -19,3 +19,6 @@ def mintFee(self): def mintFeeToken(self): return self.contract.functions.mintFeeToken().call() + + def publicMinting(self): + return self.contract.functions.publicMinting().call() diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index 91df2b58..14fefe31 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -163,7 +163,8 @@ "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", "registerIpAndAttachPILTermsAndDeployRoyaltyVault", "distributeRoyaltyTokens", - "registerIpAndMakeDerivativeAndDeployRoyaltyVault" + "registerIpAndMakeDerivativeAndDeployRoyaltyVault", + "multicall" ] }, { @@ -235,7 +236,7 @@ { "contract_name": "SPGNFTImpl", "contract_address": "0xc09e3788Fdfbd3dd8CDaa2aa481B52CcFAb74a42", - "functions": ["mintFeeToken", "mintFee"] + "functions": ["mintFeeToken", "mintFee", "publicMinting"] }, { "contract_name": "DerivativeWorkflows", @@ -244,7 +245,8 @@ "registerIpAndMakeDerivative", "mintAndRegisterIpAndMakeDerivative", "mintAndRegisterIpAndMakeDerivativeWithLicenseTokens", - "registerIpAndMakeDerivativeWithLicenseTokens" + "registerIpAndMakeDerivativeWithLicenseTokens", + "multicall" ] }, { From 69779a78ce5aea133c4063623bcabf3c985f78cf Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 9 Jan 2026 17:40:55 +0800 Subject: [PATCH 02/52] feat: add batch registration types for optimized workflows in IPAsset --- src/story_protocol_python_sdk/__init__.py | 11 ++ .../types/resource/IPAsset.py | 101 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 95a383af..ef932f81 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -16,8 +16,12 @@ from .types.resource.IPAsset import ( BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, + BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, + BatchRegistrationResult, + IpRegistrationWorkflowRequest, LicenseTermsDataInput, LinkDerivativeResponse, + MintAndRegisterRequest, MintedNFT, MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, @@ -26,6 +30,7 @@ RegisteredIP, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, + RegisterRegistrationRequest, RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, @@ -82,6 +87,12 @@ "RegisterIpAssetResponse", "RegisterDerivativeIpAssetResponse", "LinkDerivativeResponse", + # Types for batch_register_ip_assets_with_optimized_workflows + "MintAndRegisterRequest", + "RegisterRegistrationRequest", + "IpRegistrationWorkflowRequest", + "BatchRegistrationResult", + "BatchRegisterIpAssetsWithOptimizedWorkflowsResponse", # Constants "ZERO_ADDRESS", "ZERO_HASH", diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index d698a84b..4bd58207 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -4,6 +4,8 @@ from ens.ens import Address, HexStr from story_protocol_python_sdk.types.resource.License import LicenseTermsInput +from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput +from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput from story_protocol_python_sdk.utils.ip_metadata import IPMetadataInput from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfig @@ -237,3 +239,102 @@ class LinkDerivativeResponse(TypedDict): """ tx_hash: HexStr + + +# ============================================================================= +# Batch Registration Types for batch_register_ip_assets_with_optimized_workflows +# ============================================================================= + + +@dataclass +class MintAndRegisterRequest: + """ + Request for mint and register IP operations. + + Used for: + - mintAndRegisterIpAssetWithPilTerms + - mintAndRegisterIpAndMakeDerivative + - mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens + - mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens + + Attributes: + spg_nft_contract: The address of the SPG NFT contract. + recipient: [Optional] The address to receive the NFT. Defaults to caller's wallet address. + allow_duplicates: [Optional] Set to true to allow minting an NFT with a duplicate metadata hash. (default: True) + ip_metadata: [Optional] The metadata for the newly minted NFT and registered IP. + license_terms_data: [Optional] The license terms data to attach. Required if not using deriv_data. + deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using license_terms_data. + royalty_shares: [Optional] The royalty shares for distributing royalty tokens. + """ + + spg_nft_contract: Address + recipient: Address | None = None + allow_duplicates: bool = True + ip_metadata: IPMetadataInput | None = None + license_terms_data: list[LicenseTermsDataInput] | None = None + deriv_data: DerivativeDataInput | None = None + royalty_shares: list[RoyaltyShareInput] | None = None + + +@dataclass +class RegisterRegistrationRequest: + """ + Request for register IP operations (already minted NFT). + + Used for: + - registerIpAndAttachPilTerms + - registerIpAndMakeDerivative (registerDerivativeIp) + - registerIpAndAttachPilTermsAndDeployRoyaltyVault + - registerIpAndMakeDerivativeAndDeployRoyaltyVault + + Attributes: + nft_contract: The address of the NFT contract. + token_id: The token ID of the NFT. + ip_metadata: [Optional] The metadata for the registered IP. + deadline: [Optional] The deadline for the signature in seconds. (default: 1000) + license_terms_data: [Optional] The license terms data to attach. Required if not using deriv_data. + deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using license_terms_data. + royalty_shares: [Optional] The royalty shares for distributing royalty tokens. + """ + + nft_contract: Address + token_id: int + ip_metadata: IPMetadataInput | None = None + deadline: int | None = None + license_terms_data: list[LicenseTermsDataInput] | None = None + deriv_data: DerivativeDataInput | None = None + royalty_shares: list[RoyaltyShareInput] | None = None + + +# Union type for all registration requests +IpRegistrationWorkflowRequest = MintAndRegisterRequest | RegisterRegistrationRequest + + +class BatchRegistrationResult(TypedDict, total=False): + """ + Result of a single batch registration transaction. + + Attributes: + tx_hash: The transaction hash. + registered_ips: List of registered IP assets (ip_id, token_id). + license_terms_ids: [Optional] The IDs of the license terms attached (applies to all IPs in this batch). + ip_royalty_vaults: [Optional] List of (ip_id, ip_royalty_vault) tuples for deployed royalty vaults. + """ + + tx_hash: HexStr + registered_ips: list[RegisteredIP] + license_terms_ids: list[int] + ip_royalty_vaults: list[tuple[Address, Address]] + + +class BatchRegisterIpAssetsWithOptimizedWorkflowsResponse(TypedDict, total=False): + """ + Response for batch register IP assets with optimized workflows. + + Attributes: + registration_results: List of batch registration results. + distribute_royalty_tokens_tx_hashes: [Optional] Transaction hashes for royalty token distribution. + """ + + registration_results: list[BatchRegistrationResult] + distribute_royalty_tokens_tx_hashes: list[HexStr] From 702efad05e42cf7aa33b83db878024917d9b6dac Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 9 Jan 2026 17:57:21 +0800 Subject: [PATCH 03/52] feat: add registration utilities for SPG NFT public minting check --- .../utils/registration_utils.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/story_protocol_python_sdk/utils/registration_utils.py diff --git a/src/story_protocol_python_sdk/utils/registration_utils.py b/src/story_protocol_python_sdk/utils/registration_utils.py new file mode 100644 index 00000000..5518eb47 --- /dev/null +++ b/src/story_protocol_python_sdk/utils/registration_utils.py @@ -0,0 +1,23 @@ +"""Registration utilities for IP asset operations.""" + +from ens.ens import Address +from web3 import Web3 + +from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient + + +def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: + """ + Check if SPG NFT contract has public minting enabled. + + Args: + spg_nft_contract: The address of the SPG NFT contract. + web3: Web3 instance. + + Returns: + True if public minting is enabled, False otherwise. + """ + spg_client = SPGNFTImplClient( + web3, contract_address=Web3.to_checksum_address(spg_nft_contract) + ) + return spg_client.publicMinting() From 330cc6f0cd2a887d6960c64706a204d0da09e029 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 12 Jan 2026 11:31:32 +0800 Subject: [PATCH 04/52] feat: add aggregate3 functionality to Multicall3Client and enhance IPAsset with batch registration methods for optimized workflows --- .../abi/Multicall3/Multicall3_client.py | 6 + .../resources/IPAsset.py | 712 +++++++++++++++++- .../scripts/config.json | 2 +- 3 files changed, 718 insertions(+), 2 deletions(-) diff --git a/src/story_protocol_python_sdk/abi/Multicall3/Multicall3_client.py b/src/story_protocol_python_sdk/abi/Multicall3/Multicall3_client.py index fb28b71a..7e9ab2ca 100644 --- a/src/story_protocol_python_sdk/abi/Multicall3/Multicall3_client.py +++ b/src/story_protocol_python_sdk/abi/Multicall3/Multicall3_client.py @@ -29,6 +29,12 @@ def __init__(self, web3: Web3): abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) + def aggregate3(self, calls): + return self.contract.functions.aggregate3(calls).transact() + + def build_aggregate3_transaction(self, calls, tx_params): + return self.contract.functions.aggregate3(calls).build_transaction(tx_params) + def aggregate3Value(self, calls): return self.contract.functions.aggregate3Value(calls).transact() diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 2b0e7935..69727505 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1,7 +1,7 @@ """Module for handling IP Account operations and transactions.""" from dataclasses import asdict, is_dataclass, replace -from typing import cast +from typing import Any, cast from ens.ens import Address, HexStr from typing_extensions import deprecated @@ -58,8 +58,11 @@ from story_protocol_python_sdk.types.resource.IPAsset import ( BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, + BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, + BatchRegistrationResult, LicenseTermsDataInput, LinkDerivativeResponse, + MintAndRegisterRequest, MintedNFT, MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, @@ -68,6 +71,7 @@ RegisteredIP, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, + RegisterRegistrationRequest, RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, @@ -93,6 +97,7 @@ ) from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData from story_protocol_python_sdk.utils.pil_flavor import PILFlavor +from story_protocol_python_sdk.utils.registration_utils import get_public_minting from story_protocol_python_sdk.utils.royalty import get_royalty_shares from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -2122,6 +2127,58 @@ def _distribute_royalty_tokens( except Exception as e: raise ValueError(f"Failed to distribute royalty tokens: {str(e)}") from e + def batch_register_ip_assets_with_optimized_workflows( + self, + requests: list[MintAndRegisterRequest] | list[RegisterRegistrationRequest], + tx_options: dict | None = None, + ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: + """ + Batch register IP assets with optimized workflows. + + This method supports batching multiple registration operations into optimized transactions. + It uses different multicall strategies based on the request type and contract configuration: + + - For MintAndRegisterRequest: + - When spg_nft_contract has public minting disabled: Uses SPG's native multicall + - When spg_nft_contract has public minting enabled: Uses multicall3 + - Exception: Methods with royalty distribution always use SPG's native multicall + + - For RegisterRegistrationRequest: + - Always uses SPG's native multicall (signatures require msg.sender preservation) + + Note: Royalty token distribution is executed as a separate step after registration + because the ipRoyaltyVault address is only known after the registration transaction. + + :param requests list[MintAndRegisterRequest] | list[RegisterRegistrationRequest]: List of registration requests. + All requests must be of the same type (either all MintAndRegisterRequest or all RegisterRegistrationRequest). + :param tx_options dict: [Optional] Transaction options. + :return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: Response with registration results and distribution tx hashes. + :raises ValueError: If requests list is empty or contains mixed request types. + """ + try: + if not requests: + raise ValueError("Requests list cannot be empty.") + + # Validate all requests are the same type + first_type = type(requests[0]) + if not all(isinstance(r, first_type) for r in requests): + raise ValueError( + "All requests must be of the same type " + "(either all MintAndRegisterRequest or all RegisterRegistrationRequest)." + ) + + if isinstance(requests[0], MintAndRegisterRequest): + return self._batch_mint_and_register( + cast(list[MintAndRegisterRequest], requests), tx_options + ) + else: + return self._batch_register( + cast(list[RegisterRegistrationRequest], requests), tx_options + ) + + except Exception as e: + raise ValueError(f"Failed to batch register IP assets: {str(e)}") from e + def _get_ip_id(self, token_contract: str, token_id: int) -> str: """ Get the IP ID for a given token. @@ -2288,3 +2345,656 @@ def _validate_license_terms_data( } ) return validated_license_terms_data + + def _parse_all_ip_royalty_vault_deployed_events( + self, tx_receipt: dict + ) -> list[tuple[Address, Address]]: + """ + Parse all IpRoyaltyVaultDeployed events from a transaction receipt. + + :param tx_receipt dict: The transaction receipt. + :return list[tuple[Address, Address]]: List of (ip_id, ip_royalty_vault) tuples. + """ + event_signature = Web3.keccak( + text="IpRoyaltyVaultDeployed(address,address)" + ).hex() + results: list[tuple[Address, Address]] = [] + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + event_result = self.royalty_module_client.contract.events.IpRoyaltyVaultDeployed.process_log( + log + ) + results.append( + ( + event_result["args"]["ipId"], + event_result["args"]["ipRoyaltyVault"], + ) + ) + return results + + def _batch_mint_and_register( + self, + requests: list[MintAndRegisterRequest], + tx_options: dict | None = None, + ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: + """ + Handle batch mint and register requests. + + Groups requests by workflow type and uses appropriate multicall strategy. + """ + registration_results: list[BatchRegistrationResult] = [] + distribute_royalty_tokens_tx_hashes: list[str] = [] + + # Group requests by workflow type for multicall + # Key: (workflow_type, use_multicall3) + grouped_requests: dict[tuple[str, bool], list[tuple[int, bytes]]] = {} + + for idx, request in enumerate(requests): + spg_nft_contract = validate_address(request.spg_nft_contract) + has_royalty_shares = ( + request.royalty_shares is not None and len(request.royalty_shares) > 0 + ) + has_license_terms = ( + request.license_terms_data is not None + and len(request.license_terms_data) > 0 + ) + has_deriv_data = request.deriv_data is not None + + # Validate request: must have license_terms or deriv_data, but not both + if has_license_terms and has_deriv_data: + raise ValueError( + f"Request {idx}: Cannot have both license_terms_data and deriv_data." + ) + if has_royalty_shares and not (has_license_terms or has_deriv_data): + raise ValueError( + f"Request {idx}: royalty_shares requires license_terms_data or deriv_data." + ) + + # Determine multicall strategy + # - Royalty distribution workflows always use SPG's multicall + # - Other methods use multicall3 if publicMinting is enabled + use_multicall3 = False + if has_royalty_shares: + # Royalty distribution workflows always use SPG's native multicall + use_multicall3 = False + else: + # Check if publicMinting is enabled for this SPG NFT contract + use_multicall3 = get_public_minting(spg_nft_contract, self.web3) + + # Encode the transaction data based on request parameters + if has_license_terms and has_royalty_shares: + # mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens + # Type assertions - already validated by has_* checks above + assert request.license_terms_data is not None + assert request.royalty_shares is not None + license_terms = self._validate_license_terms_data( + request.license_terms_data + ) + validated_royalty_shares = get_royalty_shares(request.royalty_shares)[ + "royalty_shares" + ] + encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", + args=[ + spg_nft_contract, + self._validate_recipient(request.recipient), + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + license_terms, + validated_royalty_shares, + request.allow_duplicates, + ], + ) + workflow_key = ("royalty_distribution_pil_terms", use_multicall3) + + elif has_deriv_data and has_royalty_shares: + # mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens + # Type assertions - already validated by has_* checks above + assert request.deriv_data is not None + assert request.royalty_shares is not None + validated_deriv_data = DerivativeData.from_input( + web3=self.web3, input_data=request.deriv_data + ).get_validated_data() + validated_royalty_shares = get_royalty_shares(request.royalty_shares)[ + "royalty_shares" + ] + encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", + args=[ + spg_nft_contract, + self._validate_recipient(request.recipient), + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + validated_deriv_data, + validated_royalty_shares, + request.allow_duplicates, + ], + ) + workflow_key = ("royalty_distribution_derivative", use_multicall3) + + elif has_license_terms: + # mintAndRegisterIpAndAttachPILTerms + # Type assertion - already validated by has_license_terms check above + assert request.license_terms_data is not None + license_terms = self._validate_license_terms_data( + request.license_terms_data + ) + encoded_data = ( + self.license_attachment_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndAttachPILTerms", + args=[ + spg_nft_contract, + self._validate_recipient(request.recipient), + IPMetadata.from_input( + request.ip_metadata + ).get_validated_data(), + license_terms, + request.allow_duplicates, + ], + ) + ) + workflow_key = ("license_attachment", use_multicall3) + + elif has_deriv_data: + # mintAndRegisterIpAndMakeDerivative + # Type assertion - already validated by has_deriv_data check above + assert request.deriv_data is not None + validated_deriv_data = DerivativeData.from_input( + web3=self.web3, input_data=request.deriv_data + ).get_validated_data() + encoded_data = self.derivative_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndMakeDerivative", + args=[ + spg_nft_contract, + validated_deriv_data, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + self._validate_recipient(request.recipient), + request.allow_duplicates, + ], + ) + workflow_key = ("derivative", use_multicall3) + + else: + # mintAndRegisterIp (basic registration) + encoded_data = self.registration_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIp", + args=[ + spg_nft_contract, + self._validate_recipient(request.recipient), + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + request.allow_duplicates, + ], + ) + workflow_key = ("registration", use_multicall3) + + if workflow_key not in grouped_requests: + grouped_requests[workflow_key] = [] + grouped_requests[workflow_key].append((idx, encoded_data)) + + # Execute grouped requests using appropriate multicall strategy + for ( + workflow_type, + use_multicall3, + ), indexed_data_list in grouped_requests.items(): + encoded_data_list = [data for _, data in indexed_data_list] + + # Select the appropriate workflow client and execute multicall + workflow_client: Any + if workflow_type.startswith("royalty_distribution"): + workflow_client = self.royalty_token_distribution_workflows_client + elif workflow_type == "license_attachment": + workflow_client = self.license_attachment_workflows_client + elif workflow_type == "derivative": + workflow_client = self.derivative_workflows_client + else: + workflow_client = self.registration_workflows_client + + if use_multicall3: + # Use multicall3 for batching + calls = [ + { + "target": workflow_client.contract.address, + "allowFailure": False, + "callData": data, + } + for data in encoded_data_list + ] + response = build_and_send_transaction( + self.web3, + self.account, + self.multicall3_client.build_aggregate3_transaction, + calls, + tx_options=tx_options, + ) + else: + # Use workflow's native multicall + response = build_and_send_transaction( + self.web3, + self.account, + workflow_client.build_multicall_transaction, + encoded_data_list, + tx_options=tx_options, + ) + + # Parse events from the transaction receipt + registered_ips = self._parse_tx_ip_registered_event(response["tx_receipt"]) + license_terms_ids = self._parse_tx_license_terms_attached_event( + response["tx_receipt"] + ) + ip_royalty_vaults = self._parse_all_ip_royalty_vault_deployed_events( + response["tx_receipt"] + ) + + registration_results.append( + BatchRegistrationResult( + tx_hash=response["tx_hash"], + registered_ips=registered_ips, + license_terms_ids=license_terms_ids, + ip_royalty_vaults=ip_royalty_vaults, + ) + ) + + return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( + registration_results=registration_results, + distribute_royalty_tokens_tx_hashes=distribute_royalty_tokens_tx_hashes, + ) + + def _batch_register( + self, + requests: list[RegisterRegistrationRequest], + tx_options: dict | None = None, + ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: + """ + Handle batch register requests for already minted NFTs. + + Uses SPG's native multicall for all operations (signatures require msg.sender preservation). + Royalty token distribution is executed separately after registration. + """ + registration_results: list[BatchRegistrationResult] = [] + distribute_royalty_tokens_tx_hashes: list[str] = [] + pending_royalty_distributions: list[ + tuple[Address, list[dict], Address, int, int] + ] = [] + + # Group requests by workflow type + # Key: workflow_type + grouped_requests: dict[ + str, list[tuple[int, bytes, RegisterRegistrationRequest]] + ] = {} + + for idx, request in enumerate(requests): + nft_contract = validate_address(request.nft_contract) + ip_id = self._get_ip_id(nft_contract, request.token_id) + + if self._is_registered(ip_id): + raise ValueError( + f"Request {idx}: The NFT with token_id {request.token_id} is already registered as IP." + ) + + has_royalty_shares = ( + request.royalty_shares is not None and len(request.royalty_shares) > 0 + ) + has_license_terms = ( + request.license_terms_data is not None + and len(request.license_terms_data) > 0 + ) + has_deriv_data = request.deriv_data is not None + + # Validate request + if has_license_terms and has_deriv_data: + raise ValueError( + f"Request {idx}: Cannot have both license_terms_data and deriv_data." + ) + if has_royalty_shares and not (has_license_terms or has_deriv_data): + raise ValueError( + f"Request {idx}: royalty_shares requires license_terms_data or deriv_data." + ) + + calculated_deadline = self.sign_util.get_deadline(deadline=request.deadline) + + # Build signature and encode transaction data based on request parameters + if has_license_terms and has_royalty_shares: + # registerIpAndAttachPILTermsAndDeployRoyaltyVault + distributeRoyaltyTokens (later) + # Type assertions - already validated by has_* checks above + assert request.license_terms_data is not None + assert request.royalty_shares is not None + license_terms = self._validate_license_terms_data( + request.license_terms_data + ) + royalty_shares_obj = get_royalty_shares(request.royalty_shares) + + signature_response = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": self.royalty_token_distribution_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": self.royalty_token_distribution_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "attachLicenseTerms(address,address,uint256)", + }, + { + "ipId": ip_id, + "signer": self.royalty_token_distribution_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ], + ) + + encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + license_terms, + { + "signer": self.web3.to_checksum_address( + self.account.address + ), + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + ], + ) + workflow_key = "royalty_distribution_pil_terms" + + # Store pending royalty distribution info + pending_royalty_distributions.append( + ( + ip_id, + royalty_shares_obj["royalty_shares"], + None, # royalty_vault will be filled after registration + royalty_shares_obj["total_amount"], + calculated_deadline, + ) + ) + + elif has_deriv_data and has_royalty_shares: + # registerIpAndMakeDerivativeAndDeployRoyaltyVault + distributeRoyaltyTokens (later) + # Type assertions - already validated by has_* checks above + assert request.deriv_data is not None + assert request.royalty_shares is not None + validated_deriv_data = DerivativeData.from_input( + web3=self.web3, input_data=request.deriv_data + ).get_validated_data() + royalty_shares_obj = get_royalty_shares(request.royalty_shares) + + signature_response = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": self.royalty_token_distribution_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": self.royalty_token_distribution_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", + }, + ], + ) + + encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + validated_deriv_data, + { + "signer": self.web3.to_checksum_address( + self.account.address + ), + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + ], + ) + workflow_key = "royalty_distribution_derivative" + + # Store pending royalty distribution info + pending_royalty_distributions.append( + ( + ip_id, + royalty_shares_obj["royalty_shares"], + None, # royalty_vault will be filled after registration + royalty_shares_obj["total_amount"], + calculated_deadline, + ) + ) + + elif has_license_terms: + # registerIpAndAttachPILTerms + # Type assertion - already validated by has_license_terms check above + assert request.license_terms_data is not None + license_terms = self._validate_license_terms_data( + request.license_terms_data + ) + + signature_response = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": self.license_attachment_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": AccessPermission.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": AccessPermission.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": AccessPermission.ALLOW, + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ], + ) + + encoded_data = ( + self.license_attachment_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndAttachPILTerms", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input( + request.ip_metadata + ).get_validated_data(), + license_terms, + { + "signer": self.web3.to_checksum_address( + self.account.address + ), + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + ], + ) + ) + workflow_key = "license_attachment" + + elif has_deriv_data: + # registerIpAndMakeDerivative + # Type assertion - already validated by has_deriv_data check above + assert request.deriv_data is not None + validated_deriv_data = DerivativeData.from_input( + web3=self.web3, input_data=request.deriv_data + ).get_validated_data() + + signature_response = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": self.derivative_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": self.derivative_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", + }, + ], + ) + + encoded_data = self.derivative_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndMakeDerivative", + args=[ + nft_contract, + request.token_id, + validated_deriv_data, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + { + "signer": self.web3.to_checksum_address( + self.account.address + ), + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + ], + ) + workflow_key = "derivative" + + else: + # registerIp (basic registration with metadata) + signature_response = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": self.registration_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + ], + ) + + encoded_data = self.registration_workflows_client.contract.encode_abi( + abi_element_identifier="registerIp", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + { + "signer": self.web3.to_checksum_address( + self.account.address + ), + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + ], + ) + workflow_key = "registration" + + if workflow_key not in grouped_requests: + grouped_requests[workflow_key] = [] + grouped_requests[workflow_key].append((idx, encoded_data, request)) + + # Execute grouped requests using SPG's native multicall + royalty_vault_map: dict[Address, Address] = {} + + for workflow_type, indexed_data_list in grouped_requests.items(): + encoded_data_list = [data for _, data, _ in indexed_data_list] + + # Select the appropriate workflow client + workflow_client: Any + if workflow_type.startswith("royalty_distribution"): + workflow_client = self.royalty_token_distribution_workflows_client + elif workflow_type == "license_attachment": + workflow_client = self.license_attachment_workflows_client + elif workflow_type == "derivative": + workflow_client = self.derivative_workflows_client + else: + workflow_client = self.registration_workflows_client + + # Use workflow's native multicall + response = build_and_send_transaction( + self.web3, + self.account, + workflow_client.build_multicall_transaction, + encoded_data_list, + tx_options=tx_options, + ) + + # Parse events from the transaction receipt + registered_ips = self._parse_tx_ip_registered_event(response["tx_receipt"]) + license_terms_ids = self._parse_tx_license_terms_attached_event( + response["tx_receipt"] + ) + ip_royalty_vaults = self._parse_all_ip_royalty_vault_deployed_events( + response["tx_receipt"] + ) + + # Build royalty vault map for distribution + for ip_id, vault in ip_royalty_vaults: + royalty_vault_map[ip_id] = vault + + registration_results.append( + BatchRegistrationResult( + tx_hash=response["tx_hash"], + registered_ips=registered_ips, + license_terms_ids=license_terms_ids, + ip_royalty_vaults=ip_royalty_vaults, + ) + ) + + # Execute pending royalty distributions + for ( + ip_id, + royalty_shares, + _, + total_amount, + deadline, + ) in pending_royalty_distributions: + if ip_id in royalty_vault_map: + royalty_vault = royalty_vault_map[ip_id] + # Cast royalty_shares to match method signature (it's actually list[dict] from get_royalty_shares) + distribute_tx_hash = self._distribute_royalty_tokens( + ip_id=ip_id, + royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), + royalty_vault=royalty_vault, + total_amount=total_amount, + tx_options=tx_options, + deadline=deadline, + ) + distribute_royalty_tokens_tx_hashes.append(distribute_tx_hash) + + return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( + registration_results=registration_results, + distribute_royalty_tokens_tx_hashes=distribute_royalty_tokens_tx_hashes, + ) diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index 14fefe31..b8ea2801 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -252,7 +252,7 @@ { "contract_name": "Multicall3", "contract_address": "0xcA11bde05977b3631167028862bE2a173976CA11", - "functions": ["aggregate3Value"] + "functions": ["aggregate3Value", "aggregate3"] }, { "contract_name": "WrappedIP", From e96c6c9894c7906816347231a037e881c59f5620 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 12 Jan 2026 15:12:06 +0800 Subject: [PATCH 05/52] refactor: replace _validate_license_terms_data method with validate_license_terms_data utility for improved validation in IPAsset --- .../resources/IPAsset.py | 89 ++++--------------- .../utils/registration_utils.py | 73 +++++++++++++++ 2 files changed, 90 insertions(+), 72 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 69727505..e6221c35 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1,6 +1,5 @@ """Module for handling IP Account operations and transactions.""" -from dataclasses import asdict, is_dataclass, replace from typing import Any, cast from ens.ens import Address, HexStr @@ -76,7 +75,6 @@ RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, ) -from story_protocol_python_sdk.types.resource.License import LicenseTermsInput from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.constants import ( DEADLINE, @@ -95,15 +93,14 @@ get_ip_metadata_dict, is_initial_ip_metadata, ) -from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData -from story_protocol_python_sdk.utils.pil_flavor import PILFlavor -from story_protocol_python_sdk.utils.registration_utils import get_public_minting +from story_protocol_python_sdk.utils.registration_utils import ( + get_public_minting, + validate_license_terms_data, +) from story_protocol_python_sdk.utils.royalty import get_royalty_shares from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction -from story_protocol_python_sdk.utils.util import convert_dict_keys_to_camel_case from story_protocol_python_sdk.utils.validation import ( - get_revenue_share, validate_address, validate_max_rts, ) @@ -540,7 +537,7 @@ def mint_and_register_ip_asset_with_pil_terms( raise ValueError( f"The NFT contract address {spg_nft_contract} is not valid." ) - license_terms = self._validate_license_terms_data(terms) + license_terms = validate_license_terms_data(terms, self.web3) metadata = { "ipMetadataURI": "", "ipMetadataHash": ZERO_HASH, @@ -766,7 +763,7 @@ def register_ip_and_attach_pil_terms( raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) - license_terms = self._validate_license_terms_data(license_terms_data) + license_terms = validate_license_terms_data(license_terms_data, self.web3) calculated_deadline = self.sign_util.get_deadline(deadline=deadline) # Get permission signature for all required permissions @@ -1170,7 +1167,7 @@ def mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( validated_royalty_shares = get_royalty_shares(royalty_shares)[ "royalty_shares" ] - license_terms = self._validate_license_terms_data(license_terms_data) + license_terms = validate_license_terms_data(license_terms_data, self.web3) response = build_and_send_transaction( self.web3, @@ -1407,7 +1404,7 @@ def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( f"The NFT with id {token_id} is already registered as IP." ) - license_terms = self._validate_license_terms_data(license_terms_data) + license_terms = validate_license_terms_data(license_terms_data, self.web3) calculated_deadline = self.sign_util.get_deadline(deadline=deadline) royalty_shares_obj = get_royalty_shares(royalty_shares) signature_response = self.sign_util.get_permission_signature( @@ -1510,7 +1507,7 @@ def register_pil_terms_and_attach( calculated_deadline = self.sign_util.get_deadline(deadline=deadline) ip_account_impl_client = IPAccountImplClient(self.web3, ip_id) state = ip_account_impl_client.state() - license_terms = self._validate_license_terms_data(license_terms_data) + license_terms = validate_license_terms_data(license_terms_data, self.web3) signature_response = self.sign_util.get_permission_signature( ip_id=ip_id, deadline=calculated_deadline, @@ -2294,58 +2291,6 @@ def _validate_recipient(self, recipient: Address | None) -> Address: return self.account.address return validate_address(recipient) - def _validate_license_terms_data( - self, license_terms_data: list[LicenseTermsDataInput] | list[dict] - ) -> list: - """ - Validate the license terms data. - - :param license_terms_data `list[LicenseTermsDataInput]` or `list[dict]`: The license terms data to validate. - :return list: The validated license terms data. - """ - - validated_license_terms_data = [] - for term in license_terms_data: - if is_dataclass(term): - terms_dict = asdict(term.terms) - licensing_config_dict = term.licensing_config - else: - terms_dict = term["terms"] - licensing_config_dict = term["licensing_config"] - - license_terms = PILFlavor.validate_license_terms( - LicenseTermsInput(**terms_dict) - ) - license_terms = replace( - license_terms, - commercial_rev_share=get_revenue_share( - license_terms.commercial_rev_share - ), - ) - if license_terms.royalty_policy != ZERO_ADDRESS: - is_whitelisted = self.royalty_module_client.isWhitelistedRoyaltyPolicy( - license_terms.royalty_policy - ) - if not is_whitelisted: - raise ValueError("The royalty_policy is not whitelisted.") - - if license_terms.currency != ZERO_ADDRESS: - is_whitelisted = self.royalty_module_client.isWhitelistedRoyaltyToken( - license_terms.currency - ) - if not is_whitelisted: - raise ValueError("The currency is not whitelisted.") - - validated_license_terms_data.append( - { - "terms": convert_dict_keys_to_camel_case(asdict(license_terms)), - "licensingConfig": LicensingConfigData.validate_license_config( - self.module_registry_client, licensing_config_dict - ), - } - ) - return validated_license_terms_data - def _parse_all_ip_royalty_vault_deployed_events( self, tx_receipt: dict ) -> list[tuple[Address, Address]]: @@ -2427,8 +2372,8 @@ def _batch_mint_and_register( # Type assertions - already validated by has_* checks above assert request.license_terms_data is not None assert request.royalty_shares is not None - license_terms = self._validate_license_terms_data( - request.license_terms_data + license_terms = validate_license_terms_data( + request.license_terms_data, self.web3 ) validated_royalty_shares = get_royalty_shares(request.royalty_shares)[ "royalty_shares" @@ -2474,8 +2419,8 @@ def _batch_mint_and_register( # mintAndRegisterIpAndAttachPILTerms # Type assertion - already validated by has_license_terms check above assert request.license_terms_data is not None - license_terms = self._validate_license_terms_data( - request.license_terms_data + license_terms = validate_license_terms_data( + request.license_terms_data, self.web3 ) encoded_data = ( self.license_attachment_workflows_client.contract.encode_abi( @@ -2656,8 +2601,8 @@ def _batch_register( # Type assertions - already validated by has_* checks above assert request.license_terms_data is not None assert request.royalty_shares is not None - license_terms = self._validate_license_terms_data( - request.license_terms_data + license_terms = validate_license_terms_data( + request.license_terms_data, self.web3 ) royalty_shares_obj = get_royalty_shares(request.royalty_shares) @@ -2784,8 +2729,8 @@ def _batch_register( # registerIpAndAttachPILTerms # Type assertion - already validated by has_license_terms check above assert request.license_terms_data is not None - license_terms = self._validate_license_terms_data( - request.license_terms_data + license_terms = validate_license_terms_data( + request.license_terms_data, self.web3 ) signature_response = self.sign_util.get_permission_signature( diff --git a/src/story_protocol_python_sdk/utils/registration_utils.py b/src/story_protocol_python_sdk/utils/registration_utils.py index 5518eb47..e3fbd699 100644 --- a/src/story_protocol_python_sdk/utils/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration_utils.py @@ -1,9 +1,24 @@ """Registration utilities for IP asset operations.""" +from dataclasses import asdict, is_dataclass, replace + from ens.ens import Address from web3 import Web3 +from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ( + ModuleRegistryClient, +) +from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( + RoyaltyModuleClient, +) from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient +from story_protocol_python_sdk.types.resource.IPAsset import LicenseTermsDataInput +from story_protocol_python_sdk.types.resource.License import LicenseTermsInput +from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS +from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData +from story_protocol_python_sdk.utils.pil_flavor import PILFlavor +from story_protocol_python_sdk.utils.util import convert_dict_keys_to_camel_case +from story_protocol_python_sdk.utils.validation import get_revenue_share def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: @@ -21,3 +36,61 @@ def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: web3, contract_address=Web3.to_checksum_address(spg_nft_contract) ) return spg_client.publicMinting() + + +def validate_license_terms_data( + license_terms_data: list[LicenseTermsDataInput] | list[dict], + web3: Web3, +) -> list: + """ + Validate the license terms data. + + Args: + license_terms_data: The license terms data to validate. + web3: Web3 instance. + + Returns: + The validated license terms data. + """ + royalty_module_client = RoyaltyModuleClient(web3) + module_registry_client = ModuleRegistryClient(web3) + + validated_license_terms_data = [] + for term in license_terms_data: + if is_dataclass(term): + terms_dict = asdict(term.terms) + licensing_config_dict = term.licensing_config + else: + terms_dict = term["terms"] + licensing_config_dict = term["licensing_config"] + + license_terms = PILFlavor.validate_license_terms( + LicenseTermsInput(**terms_dict) + ) + license_terms = replace( + license_terms, + commercial_rev_share=get_revenue_share(license_terms.commercial_rev_share), + ) + if license_terms.royalty_policy != ZERO_ADDRESS: + is_whitelisted = royalty_module_client.isWhitelistedRoyaltyPolicy( + license_terms.royalty_policy + ) + if not is_whitelisted: + raise ValueError("The royalty_policy is not whitelisted.") + + if license_terms.currency != ZERO_ADDRESS: + is_whitelisted = royalty_module_client.isWhitelistedRoyaltyToken( + license_terms.currency + ) + if not is_whitelisted: + raise ValueError("The currency is not whitelisted.") + + validated_license_terms_data.append( + { + "terms": convert_dict_keys_to_camel_case(asdict(license_terms)), + "licensingConfig": LicensingConfigData.validate_license_config( + module_registry_client, licensing_config_dict + ), + } + ) + return validated_license_terms_data From 5ab1c9c264864192061005587b7f39eed80d3870 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 12 Jan 2026 15:21:04 +0800 Subject: [PATCH 06/52] refactor: remove deprecated batch registration methods and unused imports in IPAsset for cleaner codebase --- .../resources/IPAsset.py | 686 +----------------- 1 file changed, 1 insertion(+), 685 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index e6221c35..c98fde54 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1,6 +1,6 @@ """Module for handling IP Account operations and transactions.""" -from typing import Any, cast +from typing import cast from ens.ens import Address, HexStr from typing_extensions import deprecated @@ -57,11 +57,8 @@ from story_protocol_python_sdk.types.resource.IPAsset import ( BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, - BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, - BatchRegistrationResult, LicenseTermsDataInput, LinkDerivativeResponse, - MintAndRegisterRequest, MintedNFT, MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, @@ -70,7 +67,6 @@ RegisteredIP, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, - RegisterRegistrationRequest, RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, @@ -94,7 +90,6 @@ is_initial_ip_metadata, ) from story_protocol_python_sdk.utils.registration_utils import ( - get_public_minting, validate_license_terms_data, ) from story_protocol_python_sdk.utils.royalty import get_royalty_shares @@ -2124,58 +2119,6 @@ def _distribute_royalty_tokens( except Exception as e: raise ValueError(f"Failed to distribute royalty tokens: {str(e)}") from e - def batch_register_ip_assets_with_optimized_workflows( - self, - requests: list[MintAndRegisterRequest] | list[RegisterRegistrationRequest], - tx_options: dict | None = None, - ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: - """ - Batch register IP assets with optimized workflows. - - This method supports batching multiple registration operations into optimized transactions. - It uses different multicall strategies based on the request type and contract configuration: - - - For MintAndRegisterRequest: - - When spg_nft_contract has public minting disabled: Uses SPG's native multicall - - When spg_nft_contract has public minting enabled: Uses multicall3 - - Exception: Methods with royalty distribution always use SPG's native multicall - - - For RegisterRegistrationRequest: - - Always uses SPG's native multicall (signatures require msg.sender preservation) - - Note: Royalty token distribution is executed as a separate step after registration - because the ipRoyaltyVault address is only known after the registration transaction. - - :param requests list[MintAndRegisterRequest] | list[RegisterRegistrationRequest]: List of registration requests. - All requests must be of the same type (either all MintAndRegisterRequest or all RegisterRegistrationRequest). - :param tx_options dict: [Optional] Transaction options. - :return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: Response with registration results and distribution tx hashes. - :raises ValueError: If requests list is empty or contains mixed request types. - """ - try: - if not requests: - raise ValueError("Requests list cannot be empty.") - - # Validate all requests are the same type - first_type = type(requests[0]) - if not all(isinstance(r, first_type) for r in requests): - raise ValueError( - "All requests must be of the same type " - "(either all MintAndRegisterRequest or all RegisterRegistrationRequest)." - ) - - if isinstance(requests[0], MintAndRegisterRequest): - return self._batch_mint_and_register( - cast(list[MintAndRegisterRequest], requests), tx_options - ) - else: - return self._batch_register( - cast(list[RegisterRegistrationRequest], requests), tx_options - ) - - except Exception as e: - raise ValueError(f"Failed to batch register IP assets: {str(e)}") from e - def _get_ip_id(self, token_contract: str, token_id: int) -> str: """ Get the IP ID for a given token. @@ -2316,630 +2259,3 @@ def _parse_all_ip_royalty_vault_deployed_events( ) ) return results - - def _batch_mint_and_register( - self, - requests: list[MintAndRegisterRequest], - tx_options: dict | None = None, - ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: - """ - Handle batch mint and register requests. - - Groups requests by workflow type and uses appropriate multicall strategy. - """ - registration_results: list[BatchRegistrationResult] = [] - distribute_royalty_tokens_tx_hashes: list[str] = [] - - # Group requests by workflow type for multicall - # Key: (workflow_type, use_multicall3) - grouped_requests: dict[tuple[str, bool], list[tuple[int, bytes]]] = {} - - for idx, request in enumerate(requests): - spg_nft_contract = validate_address(request.spg_nft_contract) - has_royalty_shares = ( - request.royalty_shares is not None and len(request.royalty_shares) > 0 - ) - has_license_terms = ( - request.license_terms_data is not None - and len(request.license_terms_data) > 0 - ) - has_deriv_data = request.deriv_data is not None - - # Validate request: must have license_terms or deriv_data, but not both - if has_license_terms and has_deriv_data: - raise ValueError( - f"Request {idx}: Cannot have both license_terms_data and deriv_data." - ) - if has_royalty_shares and not (has_license_terms or has_deriv_data): - raise ValueError( - f"Request {idx}: royalty_shares requires license_terms_data or deriv_data." - ) - - # Determine multicall strategy - # - Royalty distribution workflows always use SPG's multicall - # - Other methods use multicall3 if publicMinting is enabled - use_multicall3 = False - if has_royalty_shares: - # Royalty distribution workflows always use SPG's native multicall - use_multicall3 = False - else: - # Check if publicMinting is enabled for this SPG NFT contract - use_multicall3 = get_public_minting(spg_nft_contract, self.web3) - - # Encode the transaction data based on request parameters - if has_license_terms and has_royalty_shares: - # mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens - # Type assertions - already validated by has_* checks above - assert request.license_terms_data is not None - assert request.royalty_shares is not None - license_terms = validate_license_terms_data( - request.license_terms_data, self.web3 - ) - validated_royalty_shares = get_royalty_shares(request.royalty_shares)[ - "royalty_shares" - ] - encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", - args=[ - spg_nft_contract, - self._validate_recipient(request.recipient), - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - license_terms, - validated_royalty_shares, - request.allow_duplicates, - ], - ) - workflow_key = ("royalty_distribution_pil_terms", use_multicall3) - - elif has_deriv_data and has_royalty_shares: - # mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens - # Type assertions - already validated by has_* checks above - assert request.deriv_data is not None - assert request.royalty_shares is not None - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=request.deriv_data - ).get_validated_data() - validated_royalty_shares = get_royalty_shares(request.royalty_shares)[ - "royalty_shares" - ] - encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", - args=[ - spg_nft_contract, - self._validate_recipient(request.recipient), - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - validated_deriv_data, - validated_royalty_shares, - request.allow_duplicates, - ], - ) - workflow_key = ("royalty_distribution_derivative", use_multicall3) - - elif has_license_terms: - # mintAndRegisterIpAndAttachPILTerms - # Type assertion - already validated by has_license_terms check above - assert request.license_terms_data is not None - license_terms = validate_license_terms_data( - request.license_terms_data, self.web3 - ) - encoded_data = ( - self.license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndAttachPILTerms", - args=[ - spg_nft_contract, - self._validate_recipient(request.recipient), - IPMetadata.from_input( - request.ip_metadata - ).get_validated_data(), - license_terms, - request.allow_duplicates, - ], - ) - ) - workflow_key = ("license_attachment", use_multicall3) - - elif has_deriv_data: - # mintAndRegisterIpAndMakeDerivative - # Type assertion - already validated by has_deriv_data check above - assert request.deriv_data is not None - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=request.deriv_data - ).get_validated_data() - encoded_data = self.derivative_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndMakeDerivative", - args=[ - spg_nft_contract, - validated_deriv_data, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - self._validate_recipient(request.recipient), - request.allow_duplicates, - ], - ) - workflow_key = ("derivative", use_multicall3) - - else: - # mintAndRegisterIp (basic registration) - encoded_data = self.registration_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIp", - args=[ - spg_nft_contract, - self._validate_recipient(request.recipient), - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - request.allow_duplicates, - ], - ) - workflow_key = ("registration", use_multicall3) - - if workflow_key not in grouped_requests: - grouped_requests[workflow_key] = [] - grouped_requests[workflow_key].append((idx, encoded_data)) - - # Execute grouped requests using appropriate multicall strategy - for ( - workflow_type, - use_multicall3, - ), indexed_data_list in grouped_requests.items(): - encoded_data_list = [data for _, data in indexed_data_list] - - # Select the appropriate workflow client and execute multicall - workflow_client: Any - if workflow_type.startswith("royalty_distribution"): - workflow_client = self.royalty_token_distribution_workflows_client - elif workflow_type == "license_attachment": - workflow_client = self.license_attachment_workflows_client - elif workflow_type == "derivative": - workflow_client = self.derivative_workflows_client - else: - workflow_client = self.registration_workflows_client - - if use_multicall3: - # Use multicall3 for batching - calls = [ - { - "target": workflow_client.contract.address, - "allowFailure": False, - "callData": data, - } - for data in encoded_data_list - ] - response = build_and_send_transaction( - self.web3, - self.account, - self.multicall3_client.build_aggregate3_transaction, - calls, - tx_options=tx_options, - ) - else: - # Use workflow's native multicall - response = build_and_send_transaction( - self.web3, - self.account, - workflow_client.build_multicall_transaction, - encoded_data_list, - tx_options=tx_options, - ) - - # Parse events from the transaction receipt - registered_ips = self._parse_tx_ip_registered_event(response["tx_receipt"]) - license_terms_ids = self._parse_tx_license_terms_attached_event( - response["tx_receipt"] - ) - ip_royalty_vaults = self._parse_all_ip_royalty_vault_deployed_events( - response["tx_receipt"] - ) - - registration_results.append( - BatchRegistrationResult( - tx_hash=response["tx_hash"], - registered_ips=registered_ips, - license_terms_ids=license_terms_ids, - ip_royalty_vaults=ip_royalty_vaults, - ) - ) - - return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( - registration_results=registration_results, - distribute_royalty_tokens_tx_hashes=distribute_royalty_tokens_tx_hashes, - ) - - def _batch_register( - self, - requests: list[RegisterRegistrationRequest], - tx_options: dict | None = None, - ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: - """ - Handle batch register requests for already minted NFTs. - - Uses SPG's native multicall for all operations (signatures require msg.sender preservation). - Royalty token distribution is executed separately after registration. - """ - registration_results: list[BatchRegistrationResult] = [] - distribute_royalty_tokens_tx_hashes: list[str] = [] - pending_royalty_distributions: list[ - tuple[Address, list[dict], Address, int, int] - ] = [] - - # Group requests by workflow type - # Key: workflow_type - grouped_requests: dict[ - str, list[tuple[int, bytes, RegisterRegistrationRequest]] - ] = {} - - for idx, request in enumerate(requests): - nft_contract = validate_address(request.nft_contract) - ip_id = self._get_ip_id(nft_contract, request.token_id) - - if self._is_registered(ip_id): - raise ValueError( - f"Request {idx}: The NFT with token_id {request.token_id} is already registered as IP." - ) - - has_royalty_shares = ( - request.royalty_shares is not None and len(request.royalty_shares) > 0 - ) - has_license_terms = ( - request.license_terms_data is not None - and len(request.license_terms_data) > 0 - ) - has_deriv_data = request.deriv_data is not None - - # Validate request - if has_license_terms and has_deriv_data: - raise ValueError( - f"Request {idx}: Cannot have both license_terms_data and deriv_data." - ) - if has_royalty_shares and not (has_license_terms or has_deriv_data): - raise ValueError( - f"Request {idx}: royalty_shares requires license_terms_data or deriv_data." - ) - - calculated_deadline = self.sign_util.get_deadline(deadline=request.deadline) - - # Build signature and encode transaction data based on request parameters - if has_license_terms and has_royalty_shares: - # registerIpAndAttachPILTermsAndDeployRoyaltyVault + distributeRoyaltyTokens (later) - # Type assertions - already validated by has_* checks above - assert request.license_terms_data is not None - assert request.royalty_shares is not None - license_terms = validate_license_terms_data( - request.license_terms_data, self.web3 - ) - royalty_shares_obj = get_royalty_shares(request.royalty_shares) - - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "attachLicenseTerms(address,address,uint256)", - }, - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", - }, - ], - ) - - encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", - args=[ - nft_contract, - request.token_id, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - license_terms, - { - "signer": self.web3.to_checksum_address( - self.account.address - ), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, - ], - ) - workflow_key = "royalty_distribution_pil_terms" - - # Store pending royalty distribution info - pending_royalty_distributions.append( - ( - ip_id, - royalty_shares_obj["royalty_shares"], - None, # royalty_vault will be filled after registration - royalty_shares_obj["total_amount"], - calculated_deadline, - ) - ) - - elif has_deriv_data and has_royalty_shares: - # registerIpAndMakeDerivativeAndDeployRoyaltyVault + distributeRoyaltyTokens (later) - # Type assertions - already validated by has_* checks above - assert request.deriv_data is not None - assert request.royalty_shares is not None - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=request.deriv_data - ).get_validated_data() - royalty_shares_obj = get_royalty_shares(request.royalty_shares) - - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", - }, - ], - ) - - encoded_data = self.royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", - args=[ - nft_contract, - request.token_id, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - validated_deriv_data, - { - "signer": self.web3.to_checksum_address( - self.account.address - ), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, - ], - ) - workflow_key = "royalty_distribution_derivative" - - # Store pending royalty distribution info - pending_royalty_distributions.append( - ( - ip_id, - royalty_shares_obj["royalty_shares"], - None, # royalty_vault will be filled after registration - royalty_shares_obj["total_amount"], - calculated_deadline, - ) - ) - - elif has_license_terms: - # registerIpAndAttachPILTerms - # Type assertion - already validated by has_license_terms check above - assert request.license_terms_data is not None - license_terms = validate_license_terms_data( - request.license_terms_data, self.web3 - ) - - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.license_attachment_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.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": AccessPermission.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": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", - }, - ], - ) - - encoded_data = ( - self.license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndAttachPILTerms", - args=[ - nft_contract, - request.token_id, - IPMetadata.from_input( - request.ip_metadata - ).get_validated_data(), - license_terms, - { - "signer": self.web3.to_checksum_address( - self.account.address - ), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, - ], - ) - ) - workflow_key = "license_attachment" - - elif has_deriv_data: - # registerIpAndMakeDerivative - # Type assertion - already validated by has_deriv_data check above - assert request.deriv_data is not None - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=request.deriv_data - ).get_validated_data() - - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.derivative_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": self.derivative_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", - }, - ], - ) - - encoded_data = self.derivative_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndMakeDerivative", - args=[ - nft_contract, - request.token_id, - validated_deriv_data, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - { - "signer": self.web3.to_checksum_address( - self.account.address - ), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, - ], - ) - workflow_key = "derivative" - - else: - # registerIp (basic registration with metadata) - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.registration_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - ], - ) - - encoded_data = self.registration_workflows_client.contract.encode_abi( - abi_element_identifier="registerIp", - args=[ - nft_contract, - request.token_id, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), - { - "signer": self.web3.to_checksum_address( - self.account.address - ), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, - ], - ) - workflow_key = "registration" - - if workflow_key not in grouped_requests: - grouped_requests[workflow_key] = [] - grouped_requests[workflow_key].append((idx, encoded_data, request)) - - # Execute grouped requests using SPG's native multicall - royalty_vault_map: dict[Address, Address] = {} - - for workflow_type, indexed_data_list in grouped_requests.items(): - encoded_data_list = [data for _, data, _ in indexed_data_list] - - # Select the appropriate workflow client - workflow_client: Any - if workflow_type.startswith("royalty_distribution"): - workflow_client = self.royalty_token_distribution_workflows_client - elif workflow_type == "license_attachment": - workflow_client = self.license_attachment_workflows_client - elif workflow_type == "derivative": - workflow_client = self.derivative_workflows_client - else: - workflow_client = self.registration_workflows_client - - # Use workflow's native multicall - response = build_and_send_transaction( - self.web3, - self.account, - workflow_client.build_multicall_transaction, - encoded_data_list, - tx_options=tx_options, - ) - - # Parse events from the transaction receipt - registered_ips = self._parse_tx_ip_registered_event(response["tx_receipt"]) - license_terms_ids = self._parse_tx_license_terms_attached_event( - response["tx_receipt"] - ) - ip_royalty_vaults = self._parse_all_ip_royalty_vault_deployed_events( - response["tx_receipt"] - ) - - # Build royalty vault map for distribution - for ip_id, vault in ip_royalty_vaults: - royalty_vault_map[ip_id] = vault - - registration_results.append( - BatchRegistrationResult( - tx_hash=response["tx_hash"], - registered_ips=registered_ips, - license_terms_ids=license_terms_ids, - ip_royalty_vaults=ip_royalty_vaults, - ) - ) - - # Execute pending royalty distributions - for ( - ip_id, - royalty_shares, - _, - total_amount, - deadline, - ) in pending_royalty_distributions: - if ip_id in royalty_vault_map: - royalty_vault = royalty_vault_map[ip_id] - # Cast royalty_shares to match method signature (it's actually list[dict] from get_royalty_shares) - distribute_tx_hash = self._distribute_royalty_tokens( - ip_id=ip_id, - royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), - royalty_vault=royalty_vault, - total_amount=total_amount, - tx_options=tx_options, - deadline=deadline, - ) - distribute_royalty_tokens_tx_hashes.append(distribute_tx_hash) - - return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( - registration_results=registration_results, - distribute_royalty_tokens_tx_hashes=distribute_royalty_tokens_tx_hashes, - ) From 97bb30c5c8643c0950b2ba246092899752200c24 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 12 Jan 2026 15:27:17 +0800 Subject: [PATCH 07/52] refactor: update mock fixtures in test_ip_asset.py to use MagicMock for improved clarity and maintainability --- tests/unit/resources/test_ip_asset.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index e1a6467e..2f682c01 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -200,11 +200,10 @@ def test_register_with_metadata( @pytest.fixture(scope="class") -def mock_is_whitelisted_royalty_policy(ip_asset): +def mock_is_whitelisted_royalty_policy(): def _mock(is_whitelisted: bool = True): - return patch.object( - ip_asset.royalty_module_client, - "isWhitelistedRoyaltyPolicy", + return patch( + "story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client.RoyaltyModuleClient.isWhitelistedRoyaltyPolicy", return_value=is_whitelisted, ) @@ -212,11 +211,10 @@ def _mock(is_whitelisted: bool = True): @pytest.fixture(scope="class") -def mock_is_whitelisted_royalty_token(ip_asset): +def mock_is_whitelisted_royalty_token(): def _mock(is_whitelisted: bool = True): - return patch.object( - ip_asset.royalty_module_client, - "isWhitelistedRoyaltyToken", + return patch( + "story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client.RoyaltyModuleClient.isWhitelistedRoyaltyToken", return_value=is_whitelisted, ) From ff34469db35d2ba7de15155fc928af08055d830f Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 12 Jan 2026 16:22:14 +0800 Subject: [PATCH 08/52] test: add unit test for test_registration_utils --- .../utils/registration_utils.py | 9 +- tests/unit/fixtures/data.py | 22 +++ tests/unit/utils/test_registration_utils.py | 168 ++++++++++++++++++ 3 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 tests/unit/utils/test_registration_utils.py diff --git a/src/story_protocol_python_sdk/utils/registration_utils.py b/src/story_protocol_python_sdk/utils/registration_utils.py index e3fbd699..31f547be 100644 --- a/src/story_protocol_python_sdk/utils/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration_utils.py @@ -18,7 +18,10 @@ from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData from story_protocol_python_sdk.utils.pil_flavor import PILFlavor from story_protocol_python_sdk.utils.util import convert_dict_keys_to_camel_case -from story_protocol_python_sdk.utils.validation import get_revenue_share +from story_protocol_python_sdk.utils.validation import ( + get_revenue_share, + validate_address, +) def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: @@ -33,7 +36,7 @@ def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: True if public minting is enabled, False otherwise. """ spg_client = SPGNFTImplClient( - web3, contract_address=Web3.to_checksum_address(spg_nft_contract) + web3, contract_address=validate_address(spg_nft_contract) ) return spg_client.publicMinting() @@ -41,7 +44,7 @@ def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: def validate_license_terms_data( license_terms_data: list[LicenseTermsDataInput] | list[dict], web3: Web3, -) -> list: +) -> list[dict]: """ Validate the license terms data. diff --git a/tests/unit/fixtures/data.py b/tests/unit/fixtures/data.py index 136ded96..efd15fc6 100644 --- a/tests/unit/fixtures/data.py +++ b/tests/unit/fixtures/data.py @@ -1,3 +1,5 @@ +from dataclasses import asdict, replace + from ens.ens import HexStr from story_protocol_python_sdk import ( @@ -6,6 +8,7 @@ LicenseTermsInput, ) from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH +from story_protocol_python_sdk.utils.util import convert_dict_keys_to_camel_case CHAIN_ID = 1315 ADDRESS = "0x1234567890123456789012345678901234567890" @@ -78,6 +81,25 @@ }, ) ] + +# camel case version of LICENSE_TERMS_DATA +LICENSE_TERMS_DATA_CAMEL_CASE = { + "terms": convert_dict_keys_to_camel_case( + asdict(replace(LICENSE_TERMS_DATA[0].terms, commercial_rev_share=10 * 10**6)) + ), + "licensingConfig": { + "isSet": True, + "mintingFee": 10, + "licensingHook": ADDRESS, + "hookData": ZERO_HASH, + "commercialRevShare": 10 * 10**6, + "disabled": False, + "expectMinimumGroupRewardShare": 0, + "expectGroupRewardPool": ZERO_ADDRESS, + }, +} + + IP_METADATA = IPMetadataInput( ip_metadata_uri="https://example.com/ip-metadata.json", ip_metadata_hash=HexStr("0x" + "a" * 64), diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py new file mode 100644 index 00000000..d40423bb --- /dev/null +++ b/tests/unit/utils/test_registration_utils.py @@ -0,0 +1,168 @@ +from dataclasses import asdict, replace +from unittest.mock import MagicMock, patch + +import pytest + +from story_protocol_python_sdk.utils.registration_utils import ( + get_public_minting, + validate_license_terms_data, +) +from tests.unit.fixtures.data import ( + ADDRESS, + LICENSE_TERMS_DATA, + LICENSE_TERMS_DATA_CAMEL_CASE, +) + + +@pytest.fixture +def mock_spg_nft_client(): + """Mock SPGNFTImplClient.""" + + def _mock(public_minting: bool = True): + return patch( + "story_protocol_python_sdk.utils.registration_utils.SPGNFTImplClient", + return_value=MagicMock( + publicMinting=MagicMock(return_value=public_minting) + ), + ) + + return _mock + + +@pytest.fixture +def mock_royalty_module_client(): + """Mock RoyaltyModuleClient.""" + + def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True): + return patch( + "story_protocol_python_sdk.utils.registration_utils.RoyaltyModuleClient", + return_value=MagicMock( + isWhitelistedRoyaltyPolicy=MagicMock( + return_value=is_whitelisted_policy + ), + isWhitelistedRoyaltyToken=MagicMock(return_value=is_whitelisted_token), + ), + ) + + return _mock + + +@pytest.fixture +def mock_module_registry_client(): + """Mock ModuleRegistryClient.""" + return patch( + "story_protocol_python_sdk.utils.registration_utils.ModuleRegistryClient", + return_value=MagicMock(), + ) + + +class TestGetPublicMinting: + def test_returns_true_when_public_minting_enabled( + self, mock_web3, mock_spg_nft_client + ): + with mock_spg_nft_client(public_minting=True): + result = get_public_minting(ADDRESS, mock_web3) + assert result is True + + def test_returns_false_when_public_minting_disabled( + self, mock_web3, mock_spg_nft_client + ): + with mock_spg_nft_client(public_minting=False): + result = get_public_minting(ADDRESS, mock_web3) + assert result is False + + def test_throws_error_when_spg_nft_contract_invalid(self, mock_web3): + with pytest.raises(Exception): + get_public_minting("invalid_address", mock_web3) + + +class TestValidateLicenseTermsData: + def test_validates_license_terms_with_dataclass_input( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(), + mock_module_registry_client, + ): + result = validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) + assert isinstance(result, list) + assert len(result) == len(LICENSE_TERMS_DATA) + assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE + + def test_validates_license_terms_with_dict_input( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(), + mock_module_registry_client, + ): + result = validate_license_terms_data( + [ + { + "terms": asdict(LICENSE_TERMS_DATA[0].terms), + "licensing_config": LICENSE_TERMS_DATA[0].licensing_config, + } + ], + mock_web3, + ) + assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE + + def test_throws_error_when_royalty_policy_not_whitelisted( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(is_whitelisted_policy=False), + mock_module_registry_client, + pytest.raises(ValueError, match="The royalty_policy is not whitelisted."), + ): + validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) + + def test_throws_error_when_currency_not_whitelisted( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(is_whitelisted_token=False), + mock_module_registry_client, + pytest.raises(ValueError, match="The currency is not whitelisted."), + ): + validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) + + def test_validates_multiple_license_terms( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + # Use LICENSE_TERMS_DATA twice to test multiple terms + license_terms_data = LICENSE_TERMS_DATA + [ + replace( + LICENSE_TERMS_DATA[0], + terms=replace(LICENSE_TERMS_DATA[0].terms, commercial_rev_share=20), + ) + ] + + with ( + mock_royalty_module_client(), + mock_module_registry_client, + ): + result = validate_license_terms_data(license_terms_data, mock_web3) + assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE + assert result[1] == { + "terms": { + **LICENSE_TERMS_DATA_CAMEL_CASE["terms"], + "commercialRevShare": 20 * 10**6, + }, + "licensingConfig": LICENSE_TERMS_DATA_CAMEL_CASE["licensingConfig"], + } From 8635ef6c79067e7b87609b67eb53bc3d6229bf06 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 12 Jan 2026 18:17:16 +0800 Subject: [PATCH 09/52] feat: add transform_registration_request utility for processing registration requests with multicall support --- .../utils/transform_registration_request.py | 441 ++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 src/story_protocol_python_sdk/utils/transform_registration_request.py diff --git a/src/story_protocol_python_sdk/utils/transform_registration_request.py b/src/story_protocol_python_sdk/utils/transform_registration_request.py new file mode 100644 index 00000000..d98efcea --- /dev/null +++ b/src/story_protocol_python_sdk/utils/transform_registration_request.py @@ -0,0 +1,441 @@ +"""Transform registration request utilities.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ens.ens import Address, HexStr +from typing_extensions import cast +from web3 import Web3 + +from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import ( + CoreMetadataModuleClient, +) +from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( + DerivativeWorkflowsClient, +) +from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( + IPAssetRegistryClient, +) +from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import ( + LicenseAttachmentWorkflowsClient, +) +from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import ( + LicensingModuleClient, +) +from story_protocol_python_sdk.abi.RoyaltyTokenDistributionWorkflows.RoyaltyTokenDistributionWorkflows_client import ( + RoyaltyTokenDistributionWorkflowsClient, +) +from story_protocol_python_sdk.types.common import AccessPermission +from story_protocol_python_sdk.types.resource.IPAsset import ( + EncodedTxData, + ExtraData, + MintAndRegisterRequest, + RegisterRegistrationRequest, + TransformedRegistrationRequest, +) +from story_protocol_python_sdk.utils.constants import ZERO_HASH +from story_protocol_python_sdk.utils.derivative_data import DerivativeData +from story_protocol_python_sdk.utils.ip_metadata import IPMetadata +from story_protocol_python_sdk.utils.registration_utils import ( + get_public_minting, + validate_license_terms_data, +) +from story_protocol_python_sdk.utils.royalty import get_royalty_shares +from story_protocol_python_sdk.utils.sign import Sign +from story_protocol_python_sdk.utils.validation import validate_address + +if TYPE_CHECKING: + pass + + +def transform_registration_request( + request: MintAndRegisterRequest | RegisterRegistrationRequest, + web3: Web3, + wallet_address: Address, + chain_id: int, +) -> TransformedRegistrationRequest: + """ + Transform a registration request into encoded transaction data with multicall info. + + This is the main entry point for processing registration requests. It: + 1. Validates all input parameters + 2. Generates required signatures (for register* methods) + 3. Encodes the transaction data + 4. Determines whether to use multicall3 or SPG's native multicall + + Args: + request: The registration request (MintAndRegisterRequest or RegisterRegistrationRequest) + ip_asset: The IPAsset instance for validation and encoding + + Returns: + TransformedRegistrationRequest with encoded data and multicall strategy + + Raises: + ValueError: If the request is invalid + """ + # Check request type by attribute presence (following TypeScript SDK pattern) + if hasattr(request, "spg_nft_contract"): + return _handle_mint_and_register_request( + cast(MintAndRegisterRequest, request), web3, wallet_address + ) + elif hasattr(request, "nft_contract") and hasattr(request, "token_id"): + return _handle_register_request(request, web3, wallet_address, chain_id) + else: + raise ValueError("Invalid registration request type") + + +def _handle_mint_and_register_request( + request: MintAndRegisterRequest, + web3: Web3, + wallet_address: Address, +) -> TransformedRegistrationRequest: + """ + Handle mintAndRegister* workflow requests. + + Supports: + - mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens + - mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens + - mintAndRegisterIpAndAttachPILTerms + - mintAndRegisterIpAndMakeDerivative + + Multicall strategy: + - Public minting enabled: Uses multicall3 + - Public minting disabled: Uses SPG's native multicall + - Exception: Royalty distribution methods always use SPG's native multicall + """ + spg_nft_contract = validate_address(request.spg_nft_contract) + is_public_minting = get_public_minting(spg_nft_contract, web3) + recipient = ( + validate_address(request.recipient) if request.recipient else wallet_address + ) + license_terms_data = ( + validate_license_terms_data(request.license_terms_data, web3) + if request.license_terms_data + else None + ) + deriv_data = ( + DerivativeData.from_input( + web3=web3, input_data=request.deriv_data + ).get_validated_data() + if request.deriv_data + else None + ) + royalty_shares = ( + get_royalty_shares(request.royalty_shares)["royalty_shares"] + if request.royalty_shares + else None + ) + + # Validate request + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + royalty_token_distribution_workflows_address = ( + royalty_token_distribution_workflows_client.contract.address + ) + # Build encoded data based on request type + if license_terms_data and royalty_shares: + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", + args=[ + spg_nft_contract, + recipient, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + license_terms_data, + royalty_shares, + # TODO Need to base on request type to determine if allow_duplicates is true or false, due to history reasons + # We also need unified the allow_duplicates logic for all request types, due to history reasons. But it can cause breaking changes. + request.allow_duplicates, + ], + ) + + return TransformedRegistrationRequest( + # TODO: not sure if need the property + encoded_tx_data=EncodedTxData( + to=royalty_token_distribution_workflows_address, data=encoded_data + ), + is_use_multicall3=is_public_minting, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=None, + ) + + elif deriv_data and royalty_shares: + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", + args=[ + spg_nft_contract, + recipient, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + deriv_data, + royalty_shares, + request.allow_duplicates, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=royalty_token_distribution_workflows_address, data=encoded_data + ), + is_use_multicall3=is_public_minting, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=None, + ) + + elif license_terms_data: + license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) + license_attachment_workflows_address = ( + license_attachment_workflows_client.contract.address + ) + encoded_data = license_attachment_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndAttachPILTerms", + args=[ + spg_nft_contract, + recipient, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + license_terms_data, + request.allow_duplicates, + ], + ) + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=license_attachment_workflows_address, data=encoded_data + ), + is_use_multicall3=is_public_minting, + workflow_address=license_attachment_workflows_address, + extra_data=None, + ) + + elif deriv_data: + derivative_workflows_client = DerivativeWorkflowsClient(web3) + derivative_workflows_address = derivative_workflows_client.contract.address + encoded_data = derivative_workflows_client.contract.encode_abi( + abi_element_identifier="mintAndRegisterIpAndMakeDerivative", + args=[ + spg_nft_contract, + deriv_data, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + recipient, + request.allow_duplicates, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=derivative_workflows_address, data=encoded_data + ), + is_use_multicall3=is_public_minting, + workflow_address=derivative_workflows_address, + extra_data=None, + ) + + else: + raise ValueError("Invalid mint and register request type") + + +def _handle_register_request( + request: RegisterRegistrationRequest, + web3: Web3, + wallet_address: Address, + chain_id: int, +) -> TransformedRegistrationRequest: + """ + Handle register* workflow requests (already minted NFTs). + + Supports: + - registerIpAndAttachPILTermsAndDeployRoyaltyVault + - registerIpAndMakeDerivativeAndDeployRoyaltyVault + - registerIpAndAttachPILTerms + - registerIpAndMakeDerivative + + Note: register* methods always use SPG's native multicall because + signatures require msg.sender preservation. + """ + ip_asset_registry_client = IPAssetRegistryClient(web3) + ip_id = ip_asset_registry_client.ipId( + chain_id, request.nft_contract, request.token_id + ) + if not ip_asset_registry_client.isRegistered(ip_id): + raise ValueError(f"The NFT with id {request.token_id} is not registered as IP.") + + nft_contract = validate_address(request.nft_contract) + sign_util = Sign(web3=web3, chain_id=chain_id, account=wallet_address) + core_metadata_module_client = CoreMetadataModuleClient(web3) + licensing_module_client = LicensingModuleClient(web3) + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + royalty_token_distribution_workflows_address = ( + royalty_token_distribution_workflows_client.contract.address + ) + license_terms_data = ( + validate_license_terms_data(request.license_terms_data, web3) + if request.license_terms_data + else None + ) + deriv_data = ( + DerivativeData.from_input( + web3=web3, input_data=request.deriv_data + ).get_validated_data() + if request.deriv_data + else None + ) + royalty_shares = ( + get_royalty_shares(request.royalty_shares)["royalty_shares"] + if request.royalty_shares + else None + ) + deadline = sign_util.get_deadline(deadline=request.deadline) + if license_terms_data and royalty_shares: + signature_response = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=deadline, + state=web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": royalty_token_distribution_workflows_address, + "to": core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": royalty_token_distribution_workflows_address, + "to": licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "attachLicenseTerms(address,address,uint256)", + }, + { + "ipId": ip_id, + "signer": royalty_token_distribution_workflows_address, + "to": licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ], + ) + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + license_terms_data, + { + "signer": web3.to_checksum_address(wallet_address), + "deadline": deadline, + "signature": signature_response["signature"], + }, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=royalty_token_distribution_workflows_address, data=encoded_data + ), + is_use_multicall3=False, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=ExtraData( + royalty_shares=royalty_shares, + deadline=request.deadline, + ), + ) + + elif deriv_data and royalty_shares: + signature_response = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=deadline, + state=web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + permissions=[ + { + "ipId": ip_id, + "signer": royalty_token_distribution_workflows_address, + "to": core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": royalty_token_distribution_workflows_address, + "to": licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", + }, + ], + ) + + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + deriv_data, + { + "signer": wallet_address, + "deadline": deadline, + "signature": signature_response["signature"], + }, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=royalty_token_distribution_workflows_address, data=encoded_data + ), + is_use_multicall3=False, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=ExtraData( + royalty_shares=royalty_shares, + deadline=deadline, + ), + ) + + elif license_terms_data: + license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) + license_attachment_workflows_address = ( + license_attachment_workflows_client.contract.address + ) + encoded_data = license_attachment_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndAttachPILTerms", + args=[ + nft_contract, + request.token_id, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + license_terms_data, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=license_attachment_workflows_address, data=encoded_data + ), + is_use_multicall3=False, + workflow_address=license_attachment_workflows_address, + extra_data=None, + ) + + elif deriv_data: + derivative_workflows_client = DerivativeWorkflowsClient(web3) + derivative_workflows_address = derivative_workflows_client.contract.address + encoded_data = derivative_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndMakeDerivative", + args=[ + nft_contract, + deriv_data, + IPMetadata.from_input(request.ip_metadata).get_validated_data(), + wallet_address, + ], + ) + return TransformedRegistrationRequest( + encoded_tx_data=EncodedTxData( + to=derivative_workflows_address, data=encoded_data + ), + is_use_multicall3=False, + workflow_address=derivative_workflows_address, + extra_data=None, + ) + + else: + raise ValueError("Invalid mint and register request type") From 984243bb39f7c81664e68869db56640f606ae01f Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 13 Jan 2026 14:11:53 +0800 Subject: [PATCH 10/52] feat: introduce registration utilities for IP asset operations, including public minting checks and license terms validation --- .../registration_utils.py | 0 .../transform_registration_request.py | 145 ++++++++++++++---- 2 files changed, 117 insertions(+), 28 deletions(-) rename src/story_protocol_python_sdk/utils/{ => registrationUtils}/registration_utils.py (100%) rename src/story_protocol_python_sdk/utils/{ => registrationUtils}/transform_registration_request.py (74%) diff --git a/src/story_protocol_python_sdk/utils/registration_utils.py b/src/story_protocol_python_sdk/utils/registrationUtils/registration_utils.py similarity index 100% rename from src/story_protocol_python_sdk/utils/registration_utils.py rename to src/story_protocol_python_sdk/utils/registrationUtils/registration_utils.py diff --git a/src/story_protocol_python_sdk/utils/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py similarity index 74% rename from src/story_protocol_python_sdk/utils/transform_registration_request.py rename to src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py index d98efcea..445addd7 100644 --- a/src/story_protocol_python_sdk/utils/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from ens.ens import Address, HexStr from typing_extensions import cast from web3 import Web3 @@ -37,7 +35,7 @@ from story_protocol_python_sdk.utils.constants import ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeData from story_protocol_python_sdk.utils.ip_metadata import IPMetadata -from story_protocol_python_sdk.utils.registration_utils import ( +from story_protocol_python_sdk.utils.registrationUtils.registration_utils import ( get_public_minting, validate_license_terms_data, ) @@ -45,8 +43,29 @@ from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.validation import validate_address -if TYPE_CHECKING: - pass + +def get_allow_duplicates(allow_duplicates: bool, request_type: str) -> bool: + """ + Get the allow duplicates value based on the request type. + Due to history reasons, we need to use different allow duplicates values for different request types. + In the future, we need to unified the allow duplicates logic for all request types, but it can cause breaking changes. + Args: + allow_duplicates: The allow duplicates value. + request_type: The request type. + Returns: + The allow duplicates value. + """ + ALLOW_DUPLICATES_MAP = { + "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens": True, + "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens": True, + "mintAndRegisterIpAndAttachPILTerms": False, + "mintAndRegisterIpAndMakeDerivative": True, + } + return ( + allow_duplicates + if allow_duplicates is not None + else ALLOW_DUPLICATES_MAP[request_type] + ) def transform_registration_request( @@ -102,7 +121,6 @@ def _handle_mint_and_register_request( Multicall strategy: - Public minting enabled: Uses multicall3 - Public minting disabled: Uses SPG's native multicall - - Exception: Royalty distribution methods always use SPG's native multicall """ spg_nft_contract = validate_address(request.spg_nft_contract) is_public_minting = get_public_minting(spg_nft_contract, web3) @@ -134,19 +152,24 @@ def _handle_mint_and_register_request( royalty_token_distribution_workflows_address = ( royalty_token_distribution_workflows_client.contract.address ) + metadata = IPMetadata.from_input(request.ip_metadata).get_validated_data() # Build encoded data based on request type if license_terms_data and royalty_shares: + abi_element_identifier = ( + "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" + ) encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", + abi_element_identifier=abi_element_identifier, args=[ spg_nft_contract, recipient, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, license_terms_data, royalty_shares, - # TODO Need to base on request type to determine if allow_duplicates is true or false, due to history reasons - # We also need unified the allow_duplicates logic for all request types, due to history reasons. But it can cause breaking changes. - request.allow_duplicates, + get_allow_duplicates( + request.allow_duplicates, + abi_element_identifier, + ), ], ) @@ -161,15 +184,18 @@ def _handle_mint_and_register_request( ) elif deriv_data and royalty_shares: + abi_element_identifier = ( + "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" + ) encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", + abi_element_identifier=abi_element_identifier, args=[ spg_nft_contract, recipient, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, deriv_data, royalty_shares, - request.allow_duplicates, + get_allow_duplicates(request.allow_duplicates, abi_element_identifier), ], ) @@ -187,14 +213,15 @@ def _handle_mint_and_register_request( license_attachment_workflows_address = ( license_attachment_workflows_client.contract.address ) + abi_element_identifier = "mintAndRegisterIpAndAttachPILTerms" encoded_data = license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndAttachPILTerms", + abi_element_identifier=abi_element_identifier, args=[ spg_nft_contract, recipient, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, license_terms_data, - request.allow_duplicates, + get_allow_duplicates(request.allow_duplicates, abi_element_identifier), ], ) return TransformedRegistrationRequest( @@ -209,14 +236,15 @@ def _handle_mint_and_register_request( elif deriv_data: derivative_workflows_client = DerivativeWorkflowsClient(web3) derivative_workflows_address = derivative_workflows_client.contract.address + abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" encoded_data = derivative_workflows_client.contract.encode_abi( - abi_element_identifier="mintAndRegisterIpAndMakeDerivative", + abi_element_identifier=abi_element_identifier, args=[ spg_nft_contract, deriv_data, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, recipient, - request.allow_duplicates, + get_allow_duplicates(request.allow_duplicates, abi_element_identifier), ], ) @@ -285,12 +313,14 @@ def _handle_register_request( if request.royalty_shares else None ) + state = web3.to_bytes(hexstr=HexStr(ZERO_HASH)) + metadata = IPMetadata.from_input(request.ip_metadata).get_validated_data() deadline = sign_util.get_deadline(deadline=request.deadline) if license_terms_data and royalty_shares: - signature_response = sign_util.get_permission_signature( + signature_data = sign_util.get_permission_signature( ip_id=ip_id, deadline=deadline, - state=web3.to_bytes(hexstr=HexStr(ZERO_HASH)), + state=state, permissions=[ { "ipId": ip_id, @@ -320,12 +350,12 @@ def _handle_register_request( args=[ nft_contract, request.token_id, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, license_terms_data, { - "signer": web3.to_checksum_address(wallet_address), + "signer": wallet_address, "deadline": deadline, - "signature": signature_response["signature"], + "signature": signature_data["signature"], }, ], ) @@ -370,7 +400,7 @@ def _handle_register_request( args=[ nft_contract, request.token_id, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, deriv_data, { "signer": wallet_address, @@ -397,13 +427,46 @@ def _handle_register_request( license_attachment_workflows_address = ( license_attachment_workflows_client.contract.address ) + signature_data = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=deadline, + state=state, + permissions=[ + { + "ipId": ip_id, + "signer": license_attachment_workflows_address, + "to": core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": license_attachment_workflows_address, + "to": licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "attachLicenseTerms(address,address,uint256)", + }, + { + "ipId": ip_id, + "signer": license_attachment_workflows_address, + "to": licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ], + ) encoded_data = license_attachment_workflows_client.contract.encode_abi( abi_element_identifier="registerIpAndAttachPILTerms", args=[ nft_contract, request.token_id, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, license_terms_data, + { + "signer": wallet_address, + "deadline": deadline, + "signature": signature_data["signature"], + }, ], ) @@ -419,13 +482,39 @@ def _handle_register_request( elif deriv_data: derivative_workflows_client = DerivativeWorkflowsClient(web3) derivative_workflows_address = derivative_workflows_client.contract.address + signature_data = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=deadline, + state=state, + permissions=[ + { + "ipId": ip_id, + "signer": derivative_workflows_address, + "to": core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": derivative_workflows_address, + "to": licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", + }, + ], + ) encoded_data = derivative_workflows_client.contract.encode_abi( abi_element_identifier="registerIpAndMakeDerivative", args=[ nft_contract, deriv_data, - IPMetadata.from_input(request.ip_metadata).get_validated_data(), + metadata, wallet_address, + { + "signer": wallet_address, + "deadline": deadline, + "signature": signature_data["signature"], + }, ], ) return TransformedRegistrationRequest( From 293c9c284e1f650731656751d249c89ea8e1ba37 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 13 Jan 2026 14:16:17 +0800 Subject: [PATCH 11/52] feat: enhance registration request handling by introducing ExtraData and updating TransformedRegistrationRequest structure --- .../transform_registration_request.py | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py index 445addd7..47fa575d 100644 --- a/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py @@ -26,7 +26,6 @@ ) from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.types.resource.IPAsset import ( - EncodedTxData, ExtraData, MintAndRegisterRequest, RegisterRegistrationRequest, @@ -174,10 +173,7 @@ def _handle_mint_and_register_request( ) return TransformedRegistrationRequest( - # TODO: not sure if need the property - encoded_tx_data=EncodedTxData( - to=royalty_token_distribution_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, extra_data=None, @@ -200,9 +196,7 @@ def _handle_mint_and_register_request( ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=royalty_token_distribution_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, extra_data=None, @@ -225,9 +219,7 @@ def _handle_mint_and_register_request( ], ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=license_attachment_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=license_attachment_workflows_address, extra_data=None, @@ -249,9 +241,7 @@ def _handle_mint_and_register_request( ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=derivative_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=derivative_workflows_address, extra_data=None, @@ -361,9 +351,7 @@ def _handle_register_request( ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=royalty_token_distribution_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_address, extra_data=ExtraData( @@ -411,9 +399,7 @@ def _handle_register_request( ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=royalty_token_distribution_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_address, extra_data=ExtraData( @@ -471,9 +457,7 @@ def _handle_register_request( ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=license_attachment_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=license_attachment_workflows_address, extra_data=None, @@ -518,9 +502,7 @@ def _handle_register_request( ], ) return TransformedRegistrationRequest( - encoded_tx_data=EncodedTxData( - to=derivative_workflows_address, data=encoded_data - ), + encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=derivative_workflows_address, extra_data=None, From 587bcc7fb5956e2269f94ca425e14edafa70519c Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 14 Jan 2026 10:16:10 +0800 Subject: [PATCH 12/52] fix: update import paths for registration utilities and enhance test coverage for registration request transformations --- .../resources/IPAsset.py | 2 +- .../types/resource/IPAsset.py | 38 + .../registration_utils.py | 0 .../transform_registration_request.py | 4 +- tests/unit/utils/test_registration_utils.py | 8 +- .../test_transform_registration_request.py | 839 ++++++++++++++++++ 6 files changed, 884 insertions(+), 7 deletions(-) rename src/story_protocol_python_sdk/utils/{registrationUtils => registration}/registration_utils.py (100%) rename src/story_protocol_python_sdk/utils/{registrationUtils => registration}/transform_registration_request.py (99%) create mode 100644 tests/unit/utils/test_transform_registration_request.py diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index c98fde54..28ca5b85 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -89,7 +89,7 @@ get_ip_metadata_dict, is_initial_ip_metadata, ) -from story_protocol_python_sdk.utils.registration_utils import ( +from story_protocol_python_sdk.utils.registration.registration_utils import ( validate_license_terms_data, ) from story_protocol_python_sdk.utils.royalty import get_royalty_shares diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 4bd58207..12c90449 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -338,3 +338,41 @@ class BatchRegisterIpAssetsWithOptimizedWorkflowsResponse(TypedDict, total=False registration_results: list[BatchRegistrationResult] distribute_royalty_tokens_tx_hashes: list[HexStr] + + +# ============================================================================= +# Transform Registration Request Types +# ============================================================================= +class ExtraData(TypedDict, total=False): + """ + Extra data for post-processing after registration. + + Attributes: + royalty_shares: [Optional] The royalty shares for distribution. + deadline: [Optional] The deadline for the signature. + max_license_tokens: [Optional] Maximum license tokens for each license term. + license_terms_data: [Optional] The license terms data. + """ + + royalty_shares: list[RoyaltyShareInput] + deadline: int | None + max_license_tokens: list[int | None] + license_terms_data: list[LicenseTermsDataInput] + + +@dataclass +class TransformedRegistrationRequest: + """ + Transformed registration request with encoded data and multicall info. + + Attributes: + encoded_tx_data: The encoded transaction data. + is_use_multicall3: Whether to use multicall3 or SPG's native multicall. + workflow_address: The workflow contract address. + extra_data: [Optional] Extra data for post-processing. + """ + + encoded_tx_data: bytes + is_use_multicall3: bool + workflow_address: Address + extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/utils/registrationUtils/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py similarity index 100% rename from src/story_protocol_python_sdk/utils/registrationUtils/registration_utils.py rename to src/story_protocol_python_sdk/utils/registration/registration_utils.py diff --git a/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py similarity index 99% rename from src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py rename to src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 47fa575d..7f42254a 100644 --- a/src/story_protocol_python_sdk/utils/registrationUtils/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -34,7 +34,7 @@ from story_protocol_python_sdk.utils.constants import ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeData from story_protocol_python_sdk.utils.ip_metadata import IPMetadata -from story_protocol_python_sdk.utils.registrationUtils.registration_utils import ( +from story_protocol_python_sdk.utils.registration.registration_utils import ( get_public_minting, validate_license_terms_data, ) @@ -43,7 +43,7 @@ from story_protocol_python_sdk.utils.validation import validate_address -def get_allow_duplicates(allow_duplicates: bool, request_type: str) -> bool: +def get_allow_duplicates(allow_duplicates: bool | None, request_type: str) -> bool: """ Get the allow duplicates value based on the request type. Due to history reasons, we need to use different allow duplicates values for different request types. diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index d40423bb..5cc87db0 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -3,7 +3,7 @@ import pytest -from story_protocol_python_sdk.utils.registration_utils import ( +from story_protocol_python_sdk.utils.registration.registration_utils import ( get_public_minting, validate_license_terms_data, ) @@ -20,7 +20,7 @@ def mock_spg_nft_client(): def _mock(public_minting: bool = True): return patch( - "story_protocol_python_sdk.utils.registration_utils.SPGNFTImplClient", + "story_protocol_python_sdk.utils.registration.registration_utils.SPGNFTImplClient", return_value=MagicMock( publicMinting=MagicMock(return_value=public_minting) ), @@ -35,7 +35,7 @@ def mock_royalty_module_client(): def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True): return patch( - "story_protocol_python_sdk.utils.registration_utils.RoyaltyModuleClient", + "story_protocol_python_sdk.utils.registration.registration_utils.RoyaltyModuleClient", return_value=MagicMock( isWhitelistedRoyaltyPolicy=MagicMock( return_value=is_whitelisted_policy @@ -51,7 +51,7 @@ def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True) def mock_module_registry_client(): """Mock ModuleRegistryClient.""" return patch( - "story_protocol_python_sdk.utils.registration_utils.ModuleRegistryClient", + "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", return_value=MagicMock(), ) diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py new file mode 100644 index 00000000..e802a1dc --- /dev/null +++ b/tests/unit/utils/test_transform_registration_request.py @@ -0,0 +1,839 @@ +from unittest.mock import MagicMock, patch + +import pytest +from typing_extensions import cast + +from story_protocol_python_sdk import ( + DerivativeDataInput, + MintAndRegisterRequest, + RegisterRegistrationRequest, + RoyaltyShareInput, +) +from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( + DerivativeWorkflowsClient, +) +from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import ( + LicenseAttachmentWorkflowsClient, +) +from story_protocol_python_sdk.abi.RoyaltyTokenDistributionWorkflows.RoyaltyTokenDistributionWorkflows_client import ( + RoyaltyTokenDistributionWorkflowsClient, +) +from story_protocol_python_sdk.utils.ip_metadata import IPMetadata +from story_protocol_python_sdk.utils.registration.transform_registration_request import ( + get_allow_duplicates, + transform_registration_request, +) +from tests.unit.fixtures.data import ( + ACCOUNT_ADDRESS, + ADDRESS, + CHAIN_ID, + IP_ID, + IP_METADATA, + LICENSE_TERMS_DATA, + LICENSE_TERMS_DATA_CAMEL_CASE, +) + + +@pytest.fixture +def mock_get_public_minting(): + """Mock get_public_minting function.""" + + def _mock(public_minting: bool = True): + return patch( + "story_protocol_python_sdk.utils.registration.registration_utils.SPGNFTImplClient", + return_value=MagicMock( + publicMinting=MagicMock(return_value=public_minting) + ), + ) + + return _mock + + +@pytest.fixture +def mock_royalty_module_client(): + """Mock RoyaltyModuleClient for validate_license_terms_data.""" + + def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True): + return patch( + "story_protocol_python_sdk.utils.registration.registration_utils.RoyaltyModuleClient", + return_value=MagicMock( + isWhitelistedRoyaltyPolicy=MagicMock( + return_value=is_whitelisted_policy + ), + isWhitelistedRoyaltyToken=MagicMock(return_value=is_whitelisted_token), + ), + ) + + return _mock + + +@pytest.fixture +def mock_module_registry_client(): + """Mock ModuleRegistryClient for validate_license_terms_data.""" + return patch( + "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", + return_value=MagicMock(), + ) + + +@pytest.fixture +def mock_pi_license_template_client(): + """Mock PILicenseTemplateClient for DerivativeData.""" + + def _mock(): + mock_instance = MagicMock() + mock_instance.contract = MagicMock() + mock_instance.contract.address = ADDRESS + return patch( + "story_protocol_python_sdk.utils.derivative_data.PILicenseTemplateClient", + return_value=mock_instance, + ) + + return _mock + + +@pytest.fixture +def mock_derivative_ip_asset_registry_client(): + """Mock IPAssetRegistryClient for DerivativeData.""" + + def _mock(is_registered: bool = True): + return patch( + "story_protocol_python_sdk.utils.derivative_data.IPAssetRegistryClient", + return_value=MagicMock(isRegistered=MagicMock(return_value=is_registered)), + ) + + return _mock + + +@pytest.fixture +def mock_workflow_clients(mock_web3): + """Mock workflow clients (RoyaltyTokenDistributionWorkflowsClient, LicenseAttachmentWorkflowsClient, DerivativeWorkflowsClient). + + Returns real client instances so encode_abi can produce real encoding results. + """ + + def _mock(): + + # Create real client instances with mock_web3 + royalty_token_distribution_client = RoyaltyTokenDistributionWorkflowsClient( + mock_web3 + ) + license_attachment_client = LicenseAttachmentWorkflowsClient(mock_web3) + derivative_workflows_client = DerivativeWorkflowsClient(mock_web3) + royalty_token_distribution_client.contract.address = ( + "royalty_token_distribution_client_address" + ) + license_attachment_client.contract.address = "license_attachment_client_address" + derivative_workflows_client.contract.address = ( + "derivative_workflows_client_address" + ) + patches = [ + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyTokenDistributionWorkflowsClient", + return_value=royalty_token_distribution_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.LicenseAttachmentWorkflowsClient", + return_value=license_attachment_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.DerivativeWorkflowsClient", + return_value=derivative_workflows_client, + ), + ] + return { + "patches": patches, + "royalty_token_distribution_client": royalty_token_distribution_client, + "license_attachment_client": license_attachment_client, + "derivative_workflows_client": derivative_workflows_client, + } + + return _mock + + +@pytest.fixture +def mock_ip_asset_registry_client(): + """Mock IPAssetRegistryClient.""" + + def _mock(is_registered: bool = True, ip_id: str = IP_ID): + mock_client = MagicMock() + mock_client.ipId = MagicMock(return_value=ip_id) + mock_client.isRegistered = MagicMock(return_value=is_registered) + return patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.IPAssetRegistryClient", + return_value=mock_client, + ) + + return _mock + + +@pytest.fixture +def mock_sign_util(): + """Mock Sign utility.""" + + def _mock(deadline: int = 1000, signature: bytes = b"signature"): + mock_sign = MagicMock() + mock_sign.get_deadline = MagicMock(return_value=deadline) + mock_sign.get_permission_signature = MagicMock( + return_value={"signature": signature} + ) + return patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.Sign", + return_value=mock_sign, + ) + + return _mock + + +@pytest.fixture +def mock_module_clients(): + """Mock CoreMetadataModuleClient and LicensingModuleClient.""" + + def _mock(): + mock_core_metadata_contract = MagicMock() + mock_core_metadata_contract.address = ADDRESS + mock_core_metadata_client = MagicMock() + mock_core_metadata_client.contract = mock_core_metadata_contract + + mock_licensing_contract = MagicMock() + mock_licensing_contract.address = ADDRESS + mock_licensing_client = MagicMock() + mock_licensing_client.contract = mock_licensing_contract + + patches = [ + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.CoreMetadataModuleClient", + return_value=mock_core_metadata_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.LicensingModuleClient", + return_value=mock_licensing_client, + ), + ] + return patches + + return _mock + + +class TestGetAllowDuplicates: + def test_returns_default_for_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + self, + ): + result = get_allow_duplicates( + None, "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" + ) + assert result is True + + def test_returns_default_for_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( + self, + ): + result = get_allow_duplicates( + None, "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" + ) + assert result is True + + def test_returns_default_for_mint_and_register_ip_and_attach_pil_terms(self): + result = get_allow_duplicates(None, "mintAndRegisterIpAndAttachPILTerms") + assert result is False + + def test_returns_default_for_mint_and_register_ip_and_make_derivative(self): + result = get_allow_duplicates(None, "mintAndRegisterIpAndMakeDerivative") + assert result is True + + def test_returns_provided_value_when_not_none(self): + result = get_allow_duplicates(False, "mintAndRegisterIpAndAttachPILTerms") + assert result is False + + result = get_allow_duplicates(True, "mintAndRegisterIpAndAttachPILTerms") + assert result is True + + +class TestTransformRegistrationRequest: + def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_present( + self, + mock_web3, + mock_get_public_minting, + mock_royalty_module_client, + mock_module_registry_client, + mock_workflow_clients, + ): + request = MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + ip_metadata=IP_METADATA, + license_terms_data=LICENSE_TERMS_DATA, + ) + workflow_mocks = mock_workflow_clients() + patches = workflow_mocks["patches"] + license_attachment_client = workflow_mocks["license_attachment_client"] + with ( + mock_get_public_minting(), + mock_royalty_module_client(), + mock_module_registry_client, + patches[0], + patches[1], + patches[2], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + # Assert real encoding result (not mock value) + license_attachment_client.contract.encode_abi.assert_called_once() + call_args = license_attachment_client.contract.encode_abi.call_args + assert call_args[1]["abi_element_identifier"] == ( + "mintAndRegisterIpAndAttachPILTerms" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # spg_nft_contract + assert args[1] == ACCOUNT_ADDRESS # recipient + assert ( + args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data + assert args[4] is False # allow_duplicates (default for this method) + assert result.workflow_address == "license_attachment_client_address" + assert result.is_use_multicall3 is True + assert result.extra_data is None + + def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_id_present( + self, + mock_web3, + mock_ip_asset_registry_client, + mock_sign_util, + mock_module_clients, + mock_royalty_module_client, + mock_module_registry_client, + mock_workflow_clients, + ): + request = RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + license_terms_data=LICENSE_TERMS_DATA, + ) + workflow_mocks = mock_workflow_clients() + workflow_patches = workflow_mocks["patches"] + module_patches = mock_module_clients() + license_attachment_client = workflow_mocks["license_attachment_client"] + with ( + mock_ip_asset_registry_client(), + mock_sign_util(), + mock_royalty_module_client(), + mock_module_registry_client, + workflow_patches[0], + workflow_patches[1], + workflow_patches[2], + module_patches[0], + module_patches[1], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + # Assert real encoding result (not mock value) + license_attachment_client.contract.encode_abi.assert_called_once() + assert result.workflow_address == "license_attachment_client_address" + assert result.is_use_multicall3 is False + call_args = license_attachment_client.contract.encode_abi.call_args + assert call_args[1]["abi_element_identifier"] == ( + "registerIpAndAttachPILTerms" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # nft_contract + assert args[1] == 1 # token_id + assert args[2] == IPMetadata.from_input().get_validated_data() # metadata + assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data + assert args[4]["signer"] == ACCOUNT_ADDRESS # signature data + assert args[4]["deadline"] == 1000 + assert args[4]["signature"] == b"signature" + assert result.extra_data is None + assert result.is_use_multicall3 is False + + def test_raises_error_for_invalid_request_type( + self, mock_web3, mock_ip_asset_registry_client + ): + with mock_ip_asset_registry_client(): + with pytest.raises( + ValueError, match="Invalid mint and register request type" + ): + transform_registration_request( + RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + ), + mock_web3, + ACCOUNT_ADDRESS, + CHAIN_ID, + ) + + def test_raises_error_for_invalid_registration_request_type(self, mock_web3): + """Test that ValueError is raised when request doesn't match any known type.""" + with pytest.raises(ValueError, match="Invalid registration request type"): + transform_registration_request( + None, # type: ignore[arg-type] + mock_web3, + ACCOUNT_ADDRESS, + CHAIN_ID, + ) + + +class TestHandleMintAndRegisterRequest: + def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + self, + mock_web3, + mock_get_public_minting, + mock_royalty_module_client, + mock_module_registry_client, + mock_workflow_clients, + ): + request = MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + recipient=ADDRESS, + ip_metadata=IP_METADATA, + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], + ) + workflow_mocks = mock_workflow_clients() + patches = workflow_mocks["patches"] + royalty_token_distribution_client = workflow_mocks[ + "royalty_token_distribution_client" + ] + with ( + mock_get_public_minting(public_minting=True), + mock_royalty_module_client(), + mock_module_registry_client, + patches[0], + patches[1], + patches[2], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + royalty_token_distribution_client.contract.encode_abi.assert_called_once() + call_args = royalty_token_distribution_client.contract.encode_abi.call_args + assert result.is_use_multicall3 is True + assert ( + result.workflow_address == "royalty_token_distribution_client_address" + ) + assert result.extra_data is None + # Verify encode_abi was called with correct method and arguments + assert call_args[1]["abi_element_identifier"] == ( + "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # spg_nft_contract + assert args[1] == ADDRESS # recipient + assert ( + args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + # license_terms_data is validated, so we check it's not None + assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data + # royalty_shares is processed, so we check it's a list with correct structure + assert args[4][0]["recipient"] == ADDRESS + assert args[4][0]["percentage"] == 50 * 10**6 + assert args[5] is True # allow_duplicates (default for this method) + + def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( + self, + mock_web3, + mock_get_public_minting, + mock_pi_license_template_client, + mock_derivative_ip_asset_registry_client, + mock_license_registry_client, + mock_workflow_clients, + ): + request = MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + ip_metadata=IP_METADATA, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], license_terms_ids=[1] + ), + royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], + ) + workflow_mocks = mock_workflow_clients() + patches = workflow_mocks["patches"] + royalty_token_distribution_client = workflow_mocks[ + "royalty_token_distribution_client" + ] + with ( + mock_get_public_minting(public_minting=False), + mock_pi_license_template_client(), + mock_derivative_ip_asset_registry_client(), + mock_license_registry_client(), + patches[0], + patches[1], + patches[2], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + royalty_token_distribution_client.contract.encode_abi.assert_called_once() + call_args = royalty_token_distribution_client.contract.encode_abi.call_args + assert result.is_use_multicall3 is False + assert ( + result.workflow_address == "royalty_token_distribution_client_address" + ) + assert result.extra_data is None + # Verify encode_abi was called with correct method and arguments + assert call_args[1]["abi_element_identifier"] == ( + "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # spg_nft_contract + assert args[1] == ACCOUNT_ADDRESS # recipient + assert ( + args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + assert args[4][0]["recipient"] == ADDRESS # royalty_shares + assert args[4][0]["percentage"] == 50 * 10**6 # royalty_shares + assert args[5] is True # allow_duplicates (default for this method) + + def test_mint_and_register_ip_and_make_derivative( + self, + mock_web3, + mock_get_public_minting, + mock_pi_license_template_client, + mock_derivative_ip_asset_registry_client, + mock_license_registry_client, + mock_workflow_clients, + ): + request = MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + recipient=ACCOUNT_ADDRESS, + ip_metadata=IP_METADATA, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], license_terms_ids=[1] + ), + allow_duplicates=False, + ) + workflow_mocks = mock_workflow_clients() + patches = workflow_mocks["patches"] + derivative_workflows_client = workflow_mocks["derivative_workflows_client"] + with ( + mock_get_public_minting(public_minting=True), + mock_pi_license_template_client(), + mock_derivative_ip_asset_registry_client(), + mock_license_registry_client(), + patches[0], + patches[1], + patches[2], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + # Assert real encoding result (not mock value) + derivative_workflows_client.contract.encode_abi.assert_called_once() + call_args = derivative_workflows_client.contract.encode_abi.call_args + assert result.is_use_multicall3 is True + assert result.workflow_address == "derivative_workflows_client_address" + assert result.extra_data is None + assert call_args[1]["abi_element_identifier"] == ( + "mintAndRegisterIpAndMakeDerivative" + ) + assert call_args[1]["args"][0] == ADDRESS # spg_nft_contract + assert ( + call_args[1]["args"][2] + == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + assert call_args[1]["args"][3] == ACCOUNT_ADDRESS # recipient + assert ( + call_args[1]["args"][4] is False + ) # allow_duplicates (default for this method) + + def test_raises_error_for_invalid_mint_and_register_request_type( + self, + mock_web3, + mock_get_public_minting, + mock_workflow_clients, + ): + request = MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + ip_metadata=IP_METADATA, + ) + workflow_mocks = mock_workflow_clients() + patches = workflow_mocks["patches"] + with ( + mock_get_public_minting(), + patches[0], + patches[1], + patches[2], + ): + with pytest.raises( + ValueError, match="Invalid mint and register request type" + ): + transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + +class TestHandleRegisterRequest: + def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( + self, + mock_web3, + mock_ip_asset_registry_client, + mock_sign_util, + mock_module_clients, + mock_royalty_module_client, + mock_module_registry_client, + mock_workflow_clients, + ): + request = RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + ip_metadata=IP_METADATA, + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], + deadline=2000, + ) + workflow_mocks = mock_workflow_clients() + workflow_patches = workflow_mocks["patches"] + royalty_token_distribution_client = workflow_mocks[ + "royalty_token_distribution_client" + ] + module_patches = mock_module_clients() + with ( + mock_ip_asset_registry_client(), + mock_sign_util(deadline=2000), + mock_royalty_module_client(), + mock_module_registry_client, + workflow_patches[0], + workflow_patches[1], + workflow_patches[2], + module_patches[0], + module_patches[1], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + royalty_token_distribution_client.contract.encode_abi.assert_called_once() + call_args = royalty_token_distribution_client.contract.encode_abi.call_args + assert call_args[1]["abi_element_identifier"] == ( + "registerIpAndAttachPILTermsAndDeployRoyaltyVault" + ) + args = call_args[1]["args"] + assert args[0] == ADDRESS # nft_contract + assert args[1] == 1 # token_id + assert ( + args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data + assert args[4]["signer"] == ACCOUNT_ADDRESS # signature data + assert args[4]["deadline"] == 2000 + assert args[4]["signature"] == b"signature" + assert result.is_use_multicall3 is False + assert ( + result.workflow_address == "royalty_token_distribution_client_address" + ) + assert result.extra_data is not None + royalty_shares = result.extra_data["royalty_shares"] + assert len(royalty_shares) == 1 + royalty_share_dict = cast(list[dict[str, str | int]], royalty_shares)[0] + assert royalty_share_dict["recipient"] == ADDRESS + assert royalty_share_dict["percentage"] == 50 * 10**6 + assert result.extra_data["deadline"] == 2000 + + def test_register_ip_and_make_derivative_and_deploy_royalty_vault( + self, + mock_web3, + mock_ip_asset_registry_client, + mock_sign_util, + mock_module_clients, + mock_pi_license_template_client, + mock_derivative_ip_asset_registry_client, + mock_license_registry_client, + mock_workflow_clients, + ): + request = RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], license_terms_ids=[1] + ), + royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], + ) + workflow_mocks = mock_workflow_clients() + workflow_patches = workflow_mocks["patches"] + royalty_token_distribution_client = workflow_mocks[ + "royalty_token_distribution_client" + ] + module_patches = mock_module_clients() + with ( + mock_ip_asset_registry_client(), + mock_sign_util(), + mock_pi_license_template_client(), + mock_derivative_ip_asset_registry_client(), + mock_license_registry_client(), + workflow_patches[0], + workflow_patches[1], + workflow_patches[2], + module_patches[0], + module_patches[1], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + # Verify encode_abi was called with correct method and arguments + royalty_token_distribution_client.contract.encode_abi.assert_called_once() + call_args = royalty_token_distribution_client.contract.encode_abi.call_args + assert result.is_use_multicall3 is False + assert ( + result.workflow_address == "royalty_token_distribution_client_address" + ) + assert result.extra_data is not None + royalty_shares = result.extra_data["royalty_shares"] + assert len(royalty_shares) == 1 + royalty_share_dict = cast(list[dict[str, str | int]], royalty_shares)[0] + assert royalty_share_dict["recipient"] == ADDRESS + assert royalty_share_dict["percentage"] == 50 * 10**6 + assert call_args[1]["abi_element_identifier"] == ( + "registerIpAndMakeDerivativeAndDeployRoyaltyVault" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # nft_contract + assert args[1] == 1 # token_id + assert args[2] == IPMetadata.from_input().get_validated_data() # metadata + assert args[4]["signer"] == ACCOUNT_ADDRESS + assert args[4]["deadline"] == 1000 + assert args[4]["signature"] == b"signature" + + def test_register_ip_and_attach_pil_terms( + self, + mock_web3, + mock_ip_asset_registry_client, + mock_sign_util, + mock_module_clients, + mock_royalty_module_client, + mock_module_registry_client, + mock_workflow_clients, + ): + request = RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + ip_metadata=IP_METADATA, + license_terms_data=LICENSE_TERMS_DATA, + ) + workflow_mocks = mock_workflow_clients() + workflow_patches = workflow_mocks["patches"] + license_attachment_client = workflow_mocks["license_attachment_client"] + module_patches = mock_module_clients() + with ( + mock_ip_asset_registry_client(), + mock_sign_util(), + mock_royalty_module_client(), + mock_module_registry_client, + workflow_patches[0], + workflow_patches[1], + workflow_patches[2], + module_patches[0], + module_patches[1], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + # Verify encode_abi was called with correct method and arguments + license_attachment_client.contract.encode_abi.assert_called_once() + call_args = license_attachment_client.contract.encode_abi.call_args + assert result.is_use_multicall3 is False + assert result.workflow_address == "license_attachment_client_address" + assert result.extra_data is None + assert call_args[1]["abi_element_identifier"] == ( + "registerIpAndAttachPILTerms" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # nft_contract + assert args[1] == 1 # token_id + assert ( + args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data + assert args[4]["signer"] == ACCOUNT_ADDRESS + assert args[4]["deadline"] == 1000 + assert args[4]["signature"] == b"signature" + + def test_register_ip_and_make_derivative( + self, + mock_web3, + mock_ip_asset_registry_client, + mock_sign_util, + mock_module_clients, + mock_pi_license_template_client, + mock_derivative_ip_asset_registry_client, + mock_license_registry_client, + mock_workflow_clients, + ): + request = RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + ip_metadata=IP_METADATA, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], license_terms_ids=[1] + ), + ) + workflow_mocks = mock_workflow_clients() + workflow_patches = workflow_mocks["patches"] + derivative_workflows_client = workflow_mocks["derivative_workflows_client"] + module_patches = mock_module_clients() + with ( + mock_ip_asset_registry_client(), + mock_sign_util(), + mock_pi_license_template_client(), + mock_derivative_ip_asset_registry_client(), + mock_license_registry_client(), + workflow_patches[0], + workflow_patches[1], + workflow_patches[2], + module_patches[0], + module_patches[1], + ): + result = transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) + + # Verify encode_abi was called with correct method and arguments + derivative_workflows_client.contract.encode_abi.assert_called_once() + call_args = derivative_workflows_client.contract.encode_abi.call_args + assert result.is_use_multicall3 is False + assert result.workflow_address == "derivative_workflows_client_address" + assert result.extra_data is None + assert call_args[1]["abi_element_identifier"] == ( + "registerIpAndMakeDerivative" + ) + # Verify args + args = call_args[1]["args"] + assert args[0] == ADDRESS # nft_contract + assert ( + args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + ) # metadata + assert args[3] == ACCOUNT_ADDRESS # wallet_address + assert args[4]["signer"] == ACCOUNT_ADDRESS + assert args[4]["deadline"] == 1000 + assert args[4]["signature"] == b"signature" + + def test_raises_error_when_ip_not_registered( + self, + mock_web3, + mock_ip_asset_registry_client, + ): + request = RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + ip_metadata=IP_METADATA, + license_terms_data=LICENSE_TERMS_DATA, + ) + with ( + mock_ip_asset_registry_client(is_registered=False), + pytest.raises( + ValueError, match="The NFT with id 1 is not registered as IP." + ), + ): + transform_registration_request( + request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID + ) From 6412b050734007c67355160d71c87548cdb84415 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 14 Jan 2026 14:46:45 +0800 Subject: [PATCH 13/52] refactor: update MintAndRegisterRequest to allow None for allow_duplicates and enhance registration request handling with new helper functions --- .../types/resource/IPAsset.py | 2 +- .../transform_registration_request.py | 813 ++++++++++++------ .../test_transform_registration_request.py | 4 +- 3 files changed, 532 insertions(+), 287 deletions(-) diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 12c90449..7d947c1d 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -269,7 +269,7 @@ class MintAndRegisterRequest: spg_nft_contract: Address recipient: Address | None = None - allow_duplicates: bool = True + allow_duplicates: bool | None = None ip_metadata: IPMetadataInput | None = None license_terms_data: list[LicenseTermsDataInput] | None = None deriv_data: DerivativeDataInput | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 7f42254a..da60580e 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -1,7 +1,5 @@ """Transform registration request utilities.""" -from __future__ import annotations - from ens.ens import Address, HexStr from typing_extensions import cast from web3 import Web3 @@ -31,6 +29,7 @@ RegisterRegistrationRequest, TransformedRegistrationRequest, ) +from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.constants import ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeData from story_protocol_python_sdk.utils.ip_metadata import IPMetadata @@ -84,7 +83,9 @@ def transform_registration_request( Args: request: The registration request (MintAndRegisterRequest or RegisterRegistrationRequest) - ip_asset: The IPAsset instance for validation and encoding + web3: Web3 instance for contract interaction + wallet_address: The wallet address for signing and recipient fallback + chain_id: The chain ID for IP ID calculation Returns: TransformedRegistrationRequest with encoded data and multicall strategy @@ -103,6 +104,11 @@ def transform_registration_request( raise ValueError("Invalid registration request type") +# ============================================================================= +# Mint and Register Request Handlers +# ============================================================================= + + def _handle_mint_and_register_request( request: MintAndRegisterRequest, web3: Web3, @@ -144,113 +150,209 @@ def _handle_mint_and_register_request( else None ) - # Validate request - royalty_token_distribution_workflows_client = ( - RoyaltyTokenDistributionWorkflowsClient(web3) - ) - royalty_token_distribution_workflows_address = ( - royalty_token_distribution_workflows_client.contract.address - ) metadata = IPMetadata.from_input(request.ip_metadata).get_validated_data() # Build encoded data based on request type if license_terms_data and royalty_shares: - abi_element_identifier = ( - "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" - ) - encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, - args=[ - spg_nft_contract, - recipient, - metadata, - license_terms_data, - royalty_shares, - get_allow_duplicates( - request.allow_duplicates, - abi_element_identifier, - ), - ], - ) - - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=is_public_minting, - workflow_address=royalty_token_distribution_workflows_address, - extra_data=None, + return _handle_mint_and_register_with_license_terms_and_royalty_tokens( + web3=web3, + spg_nft_contract=spg_nft_contract, + recipient=recipient, + metadata=metadata, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + allow_duplicates=request.allow_duplicates, + is_public_minting=is_public_minting, ) elif deriv_data and royalty_shares: - abi_element_identifier = ( - "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" - ) - encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, - args=[ - spg_nft_contract, - recipient, - metadata, - deriv_data, - royalty_shares, - get_allow_duplicates(request.allow_duplicates, abi_element_identifier), - ], - ) - - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=is_public_minting, - workflow_address=royalty_token_distribution_workflows_address, - extra_data=None, + return _handle_mint_and_register_with_derivative_and_royalty_tokens( + web3=web3, + spg_nft_contract=spg_nft_contract, + recipient=recipient, + metadata=metadata, + deriv_data=deriv_data, + royalty_shares=royalty_shares, + allow_duplicates=request.allow_duplicates, + is_public_minting=is_public_minting, ) elif license_terms_data: - license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) - license_attachment_workflows_address = ( - license_attachment_workflows_client.contract.address - ) - abi_element_identifier = "mintAndRegisterIpAndAttachPILTerms" - encoded_data = license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, - args=[ - spg_nft_contract, - recipient, - metadata, - license_terms_data, - get_allow_duplicates(request.allow_duplicates, abi_element_identifier), - ], - ) - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=is_public_minting, - workflow_address=license_attachment_workflows_address, - extra_data=None, + return _handle_mint_and_register_with_license_terms( + web3=web3, + spg_nft_contract=spg_nft_contract, + recipient=recipient, + metadata=metadata, + license_terms_data=license_terms_data, + allow_duplicates=request.allow_duplicates, + is_public_minting=is_public_minting, ) elif deriv_data: - derivative_workflows_client = DerivativeWorkflowsClient(web3) - derivative_workflows_address = derivative_workflows_client.contract.address - abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" - encoded_data = derivative_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, - args=[ - spg_nft_contract, - deriv_data, - metadata, - recipient, - get_allow_duplicates(request.allow_duplicates, abi_element_identifier), - ], - ) - - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=is_public_minting, - workflow_address=derivative_workflows_address, - extra_data=None, + return _handle_mint_and_register_with_derivative( + web3=web3, + spg_nft_contract=spg_nft_contract, + recipient=recipient, + metadata=metadata, + deriv_data=deriv_data, + allow_duplicates=request.allow_duplicates, + is_public_minting=is_public_minting, ) else: raise ValueError("Invalid mint and register request type") +def _handle_mint_and_register_with_license_terms_and_royalty_tokens( + web3: Web3, + spg_nft_contract: Address, + recipient: Address, + metadata: dict, + license_terms_data: list[dict], + royalty_shares: list[dict], + allow_duplicates: bool | None, + is_public_minting: bool, +) -> TransformedRegistrationRequest: + """Handle mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens.""" + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + royalty_token_distribution_workflows_address = ( + royalty_token_distribution_workflows_client.contract.address + ) + abi_element_identifier = ( + "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" + ) + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier=abi_element_identifier, + args=[ + spg_nft_contract, + recipient, + metadata, + license_terms_data, + royalty_shares, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=is_public_minting, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=None, + ) + + +def _handle_mint_and_register_with_derivative_and_royalty_tokens( + web3: Web3, + spg_nft_contract: Address, + recipient: Address, + metadata: dict, + deriv_data: dict, + royalty_shares: list[dict], + allow_duplicates: bool | None, + is_public_minting: bool, +) -> TransformedRegistrationRequest: + """Handle mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens.""" + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + royalty_token_distribution_workflows_address = ( + royalty_token_distribution_workflows_client.contract.address + ) + abi_element_identifier = ( + "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" + ) + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier=abi_element_identifier, + args=[ + spg_nft_contract, + recipient, + metadata, + deriv_data, + royalty_shares, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=is_public_minting, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=None, + ) + + +def _handle_mint_and_register_with_license_terms( + web3: Web3, + spg_nft_contract: Address, + recipient: Address, + metadata: dict, + license_terms_data: list[dict], + allow_duplicates: bool | None, + is_public_minting: bool, +) -> TransformedRegistrationRequest: + """Handle mintAndRegisterIpAndAttachPILTerms.""" + license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) + license_attachment_workflows_address = ( + license_attachment_workflows_client.contract.address + ) + abi_element_identifier = "mintAndRegisterIpAndAttachPILTerms" + encoded_data = license_attachment_workflows_client.contract.encode_abi( + abi_element_identifier=abi_element_identifier, + args=[ + spg_nft_contract, + recipient, + metadata, + license_terms_data, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=is_public_minting, + workflow_address=license_attachment_workflows_address, + extra_data=None, + ) + + +def _handle_mint_and_register_with_derivative( + web3: Web3, + spg_nft_contract: Address, + recipient: Address, + metadata: dict, + deriv_data: dict, + allow_duplicates: bool | None, + is_public_minting: bool, +) -> TransformedRegistrationRequest: + """Handle mintAndRegisterIpAndMakeDerivative.""" + derivative_workflows_client = DerivativeWorkflowsClient(web3) + derivative_workflows_address = derivative_workflows_client.contract.address + abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" + encoded_data = derivative_workflows_client.contract.encode_abi( + abi_element_identifier=abi_element_identifier, + args=[ + spg_nft_contract, + deriv_data, + metadata, + recipient, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=is_public_minting, + workflow_address=derivative_workflows_address, + extra_data=None, + ) + + +# ============================================================================= +# Register Request Handlers +# ============================================================================= + + def _handle_register_request( request: RegisterRegistrationRequest, web3: Web3, @@ -280,12 +382,6 @@ def _handle_register_request( sign_util = Sign(web3=web3, chain_id=chain_id, account=wallet_address) core_metadata_module_client = CoreMetadataModuleClient(web3) licensing_module_client = LicensingModuleClient(web3) - royalty_token_distribution_workflows_client = ( - RoyaltyTokenDistributionWorkflowsClient(web3) - ) - royalty_token_distribution_workflows_address = ( - royalty_token_distribution_workflows_client.contract.address - ) license_terms_data = ( validate_license_terms_data(request.license_terms_data, web3) if request.license_terms_data @@ -305,208 +401,359 @@ def _handle_register_request( ) state = web3.to_bytes(hexstr=HexStr(ZERO_HASH)) metadata = IPMetadata.from_input(request.ip_metadata).get_validated_data() - deadline = sign_util.get_deadline(deadline=request.deadline) + calculated_deadline = sign_util.get_deadline(deadline=request.deadline) if license_terms_data and royalty_shares: - signature_data = sign_util.get_permission_signature( + return _handle_register_with_license_terms_and_royalty_vault( + web3=web3, + nft_contract=nft_contract, + token_id=request.token_id, + metadata=metadata, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, ip_id=ip_id, - deadline=deadline, + wallet_address=wallet_address, + calculated_deadline=calculated_deadline, + request_deadline=request.deadline, + sign_util=sign_util, + core_metadata_module_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, state=state, - permissions=[ - { - "ipId": ip_id, - "signer": royalty_token_distribution_workflows_address, - "to": core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": royalty_token_distribution_workflows_address, - "to": licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "attachLicenseTerms(address,address,uint256)", - }, - { - "ipId": ip_id, - "signer": royalty_token_distribution_workflows_address, - "to": licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", - }, - ], - ) - encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", - args=[ - nft_contract, - request.token_id, - metadata, - license_terms_data, - { - "signer": wallet_address, - "deadline": deadline, - "signature": signature_data["signature"], - }, - ], - ) - - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=False, - workflow_address=royalty_token_distribution_workflows_address, - extra_data=ExtraData( - royalty_shares=royalty_shares, - deadline=request.deadline, - ), ) elif deriv_data and royalty_shares: - signature_response = sign_util.get_permission_signature( + return _handle_register_with_derivative_and_royalty_vault( + web3=web3, + nft_contract=nft_contract, + token_id=request.token_id, + metadata=metadata, + deriv_data=deriv_data, + royalty_shares=royalty_shares, ip_id=ip_id, - deadline=deadline, - state=web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": royalty_token_distribution_workflows_address, - "to": core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": royalty_token_distribution_workflows_address, - "to": licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", - }, - ], - ) - - encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", - args=[ - nft_contract, - request.token_id, - metadata, - deriv_data, - { - "signer": wallet_address, - "deadline": deadline, - "signature": signature_response["signature"], - }, - ], - ) - - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=False, - workflow_address=royalty_token_distribution_workflows_address, - extra_data=ExtraData( - royalty_shares=royalty_shares, - deadline=deadline, - ), + wallet_address=wallet_address, + calculated_deadline=calculated_deadline, + request_deadline=request.deadline, + sign_util=sign_util, + core_metadata_module_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, + state=state, ) elif license_terms_data: - license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) - license_attachment_workflows_address = ( - license_attachment_workflows_client.contract.address - ) - signature_data = sign_util.get_permission_signature( + return _handle_register_with_license_terms( + web3=web3, + nft_contract=nft_contract, + token_id=request.token_id, + metadata=metadata, + license_terms_data=license_terms_data, ip_id=ip_id, - deadline=deadline, + wallet_address=wallet_address, + calculated_deadline=calculated_deadline, + sign_util=sign_util, + core_metadata_module_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, state=state, - permissions=[ - { - "ipId": ip_id, - "signer": license_attachment_workflows_address, - "to": core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": license_attachment_workflows_address, - "to": licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "attachLicenseTerms(address,address,uint256)", - }, - { - "ipId": ip_id, - "signer": license_attachment_workflows_address, - "to": licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", - }, - ], - ) - encoded_data = license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndAttachPILTerms", - args=[ - nft_contract, - request.token_id, - metadata, - license_terms_data, - { - "signer": wallet_address, - "deadline": deadline, - "signature": signature_data["signature"], - }, - ], - ) - - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=False, - workflow_address=license_attachment_workflows_address, - extra_data=None, ) elif deriv_data: - derivative_workflows_client = DerivativeWorkflowsClient(web3) - derivative_workflows_address = derivative_workflows_client.contract.address - signature_data = sign_util.get_permission_signature( + return _handle_register_with_derivative( + web3=web3, + nft_contract=nft_contract, + deriv_data=deriv_data, + metadata=metadata, + wallet_address=wallet_address, ip_id=ip_id, - deadline=deadline, + calculated_deadline=calculated_deadline, + sign_util=sign_util, + core_metadata_module_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, state=state, - permissions=[ - { - "ipId": ip_id, - "signer": derivative_workflows_address, - "to": core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": derivative_workflows_address, - "to": licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", - }, - ], - ) - encoded_data = derivative_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndMakeDerivative", - args=[ - nft_contract, - deriv_data, - metadata, - wallet_address, - { - "signer": wallet_address, - "deadline": deadline, - "signature": signature_data["signature"], - }, - ], - ) - return TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=False, - workflow_address=derivative_workflows_address, - extra_data=None, ) else: - raise ValueError("Invalid mint and register request type") + raise ValueError("Invalid register request type") + + +def _handle_register_with_license_terms_and_royalty_vault( + web3: Web3, + nft_contract: Address, + token_id: int, + metadata: dict, + license_terms_data: list[dict], + royalty_shares: list[dict], + ip_id: Address, + wallet_address: Address, + calculated_deadline: int, + request_deadline: int | None, + sign_util: Sign, + core_metadata_module_client: CoreMetadataModuleClient, + licensing_module_client: LicensingModuleClient, + state: bytes, +) -> TransformedRegistrationRequest: + """Handle registerIpAndAttachPILTermsAndDeployRoyaltyVault.""" + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + royalty_token_distribution_workflows_address = ( + royalty_token_distribution_workflows_client.contract.address + ) + signature_data = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=state, + permissions=_get_license_terms_permissions( + ip_id=ip_id, + signer_address=royalty_token_distribution_workflows_address, + core_metadata_address=core_metadata_module_client.contract.address, + licensing_module_address=licensing_module_client.contract.address, + ), + ) + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", + args=[ + nft_contract, + token_id, + metadata, + license_terms_data, + { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_data["signature"], + }, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=ExtraData( + royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), + deadline=request_deadline, + ), + ) + + +def _handle_register_with_derivative_and_royalty_vault( + web3: Web3, + nft_contract: Address, + token_id: int, + metadata: dict, + deriv_data: dict, + royalty_shares: list[dict], + ip_id: Address, + wallet_address: Address, + calculated_deadline: int, + request_deadline: int | None, + sign_util: Sign, + core_metadata_module_client: CoreMetadataModuleClient, + licensing_module_client: LicensingModuleClient, + state: bytes, +) -> TransformedRegistrationRequest: + """Handle registerIpAndMakeDerivativeAndDeployRoyaltyVault.""" + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + royalty_token_distribution_workflows_address = ( + royalty_token_distribution_workflows_client.contract.address + ) + signature_response = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=state, + permissions=_get_derivative_permissions( + ip_id=ip_id, + signer_address=royalty_token_distribution_workflows_address, + core_metadata_address=core_metadata_module_client.contract.address, + licensing_module_address=licensing_module_client.contract.address, + ), + ) + + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", + args=[ + nft_contract, + token_id, + metadata, + deriv_data, + { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=royalty_token_distribution_workflows_address, + extra_data=ExtraData( + royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), + deadline=request_deadline, + ), + ) + + +def _handle_register_with_license_terms( + web3: Web3, + nft_contract: Address, + token_id: int, + metadata: dict, + license_terms_data: list[dict], + ip_id: Address, + wallet_address: Address, + calculated_deadline: int, + sign_util: Sign, + core_metadata_module_client: CoreMetadataModuleClient, + licensing_module_client: LicensingModuleClient, + state: bytes, +) -> TransformedRegistrationRequest: + """Handle registerIpAndAttachPILTerms.""" + license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) + license_attachment_workflows_address = ( + license_attachment_workflows_client.contract.address + ) + signature_data = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=state, + permissions=_get_license_terms_permissions( + ip_id=ip_id, + signer_address=license_attachment_workflows_address, + core_metadata_address=core_metadata_module_client.contract.address, + licensing_module_address=licensing_module_client.contract.address, + ), + ) + encoded_data = license_attachment_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndAttachPILTerms", + args=[ + nft_contract, + token_id, + metadata, + license_terms_data, + { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_data["signature"], + }, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=license_attachment_workflows_address, + extra_data=None, + ) + + +def _handle_register_with_derivative( + web3: Web3, + nft_contract: Address, + deriv_data: dict, + metadata: dict, + wallet_address: Address, + ip_id: Address, + calculated_deadline: int, + sign_util: Sign, + core_metadata_module_client: CoreMetadataModuleClient, + licensing_module_client: LicensingModuleClient, + state: bytes, +) -> TransformedRegistrationRequest: + """Handle registerIpAndMakeDerivative.""" + derivative_workflows_client = DerivativeWorkflowsClient(web3) + derivative_workflows_address = derivative_workflows_client.contract.address + signature_data = sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=state, + permissions=_get_derivative_permissions( + ip_id=ip_id, + signer_address=derivative_workflows_address, + core_metadata_address=core_metadata_module_client.contract.address, + licensing_module_address=licensing_module_client.contract.address, + ), + ) + encoded_data = derivative_workflows_client.contract.encode_abi( + abi_element_identifier="registerIpAndMakeDerivative", + args=[ + nft_contract, + deriv_data, + metadata, + wallet_address, + { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_data["signature"], + }, + ], + ) + + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=derivative_workflows_address, + extra_data=None, + ) + + +# ============================================================================= +# Internal Helper Methods +# ============================================================================= + + +def _get_license_terms_permissions( + ip_id: Address, + signer_address: Address, + core_metadata_address: Address, + licensing_module_address: Address, +) -> list[dict]: + """Get permissions for license terms operations.""" + return [ + { + "ipId": ip_id, + "signer": signer_address, + "to": core_metadata_address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": signer_address, + "to": licensing_module_address, + "permission": AccessPermission.ALLOW, + "func": "attachLicenseTerms(address,address,uint256)", + }, + { + "ipId": ip_id, + "signer": signer_address, + "to": licensing_module_address, + "permission": AccessPermission.ALLOW, + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ] + + +def _get_derivative_permissions( + ip_id: Address, + signer_address: Address, + core_metadata_address: Address, + licensing_module_address: Address, +) -> list[dict]: + """Get permissions for derivative operations.""" + return [ + { + "ipId": ip_id, + "signer": signer_address, + "to": core_metadata_address, + "permission": AccessPermission.ALLOW, + "func": "setAll(address,string,bytes32,bytes32)", + }, + { + "ipId": ip_id, + "signer": signer_address, + "to": licensing_module_address, + "permission": AccessPermission.ALLOW, + "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", + }, + ] diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index e802a1dc..93d34ad3 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -352,9 +352,7 @@ def test_raises_error_for_invalid_request_type( self, mock_web3, mock_ip_asset_registry_client ): with mock_ip_asset_registry_client(): - with pytest.raises( - ValueError, match="Invalid mint and register request type" - ): + with pytest.raises(ValueError, match="Invalid register request type"): transform_registration_request( RegisterRegistrationRequest( nft_contract=ADDRESS, From 93a5a41fbe593275e3cede31a310c7d69378b8f7 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 14 Jan 2026 15:19:48 +0800 Subject: [PATCH 14/52] feat: add validated_request structure to registration request handling for improved data management --- .../types/resource/IPAsset.py | 1 + .../transform_registration_request.py | 208 ++++++++++++------ .../test_transform_registration_request.py | 4 +- 3 files changed, 147 insertions(+), 66 deletions(-) diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 7d947c1d..39208882 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -375,4 +375,5 @@ class TransformedRegistrationRequest: encoded_tx_data: bytes is_use_multicall3: bool workflow_address: Address + validated_request: dict extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index da60580e..e1888386 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -222,15 +222,25 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( abi_element_identifier = ( "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" ) + validated_request = { + "spg_nft_contract": spg_nft_contract, + "recipient": recipient, + "metadata": metadata, + "license_terms_data": license_terms_data, + "royalty_shares": royalty_shares, + "allow_duplicates": get_allow_duplicates( + allow_duplicates, abi_element_identifier + ), + } encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, args=[ - spg_nft_contract, - recipient, - metadata, - license_terms_data, - royalty_shares, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + validated_request["spg_nft_contract"], + validated_request["recipient"], + validated_request["metadata"], + validated_request["license_terms_data"], + validated_request["royalty_shares"], + validated_request["allow_duplicates"], ], ) @@ -238,6 +248,7 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, + validated_request=validated_request, extra_data=None, ) @@ -262,15 +273,25 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( abi_element_identifier = ( "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" ) + validated_request = { + "spg_nft_contract": spg_nft_contract, + "recipient": recipient, + "metadata": metadata, + "deriv_data": deriv_data, + "royalty_shares": royalty_shares, + "allow_duplicates": get_allow_duplicates( + allow_duplicates, abi_element_identifier + ), + } encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, args=[ - spg_nft_contract, - recipient, - metadata, - deriv_data, - royalty_shares, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + validated_request["spg_nft_contract"], + validated_request["recipient"], + validated_request["metadata"], + validated_request["deriv_data"], + validated_request["royalty_shares"], + validated_request["allow_duplicates"], ], ) @@ -278,6 +299,7 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, + validated_request=validated_request, extra_data=None, ) @@ -297,14 +319,23 @@ def _handle_mint_and_register_with_license_terms( license_attachment_workflows_client.contract.address ) abi_element_identifier = "mintAndRegisterIpAndAttachPILTerms" + validated_request = { + "spg_nft_contract": spg_nft_contract, + "recipient": recipient, + "metadata": metadata, + "license_terms_data": license_terms_data, + "allow_duplicates": get_allow_duplicates( + allow_duplicates, abi_element_identifier + ), + } encoded_data = license_attachment_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, args=[ - spg_nft_contract, - recipient, - metadata, - license_terms_data, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + validated_request["spg_nft_contract"], + validated_request["recipient"], + validated_request["metadata"], + validated_request["license_terms_data"], + validated_request["allow_duplicates"], ], ) @@ -312,6 +343,7 @@ def _handle_mint_and_register_with_license_terms( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=license_attachment_workflows_address, + validated_request=validated_request, extra_data=None, ) @@ -329,14 +361,23 @@ def _handle_mint_and_register_with_derivative( derivative_workflows_client = DerivativeWorkflowsClient(web3) derivative_workflows_address = derivative_workflows_client.contract.address abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" + validated_request = { + "spg_nft_contract": spg_nft_contract, + "recipient": recipient, + "metadata": metadata, + "deriv_data": deriv_data, + "allow_duplicates": get_allow_duplicates( + allow_duplicates, abi_element_identifier + ), + } encoded_data = derivative_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, args=[ - spg_nft_contract, - deriv_data, - metadata, - recipient, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + validated_request["spg_nft_contract"], + validated_request["deriv_data"], + validated_request["metadata"], + validated_request["recipient"], + validated_request["allow_duplicates"], ], ) @@ -344,6 +385,7 @@ def _handle_mint_and_register_with_derivative( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=derivative_workflows_address, + validated_request=validated_request, extra_data=None, ) @@ -460,6 +502,7 @@ def _handle_register_request( nft_contract=nft_contract, deriv_data=deriv_data, metadata=metadata, + token_id=request.token_id, wallet_address=wallet_address, ip_id=ip_id, calculated_deadline=calculated_deadline, @@ -507,18 +550,26 @@ def _handle_register_with_license_terms_and_royalty_vault( licensing_module_address=licensing_module_client.contract.address, ), ) + abi_element_identifier = "registerIpAndAttachPILTermsAndDeployRoyaltyVault" + validated_request = { + "nft_contract": nft_contract, + "token_id": token_id, + "metadata": metadata, + "license_terms_data": license_terms_data, + "signature_data": { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_data["signature"], + }, + } encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", + abi_element_identifier=abi_element_identifier, args=[ - nft_contract, - token_id, - metadata, - license_terms_data, - { - "signer": wallet_address, - "deadline": calculated_deadline, - "signature": signature_data["signature"], - }, + validated_request["nft_contract"], + validated_request["token_id"], + validated_request["metadata"], + validated_request["license_terms_data"], + validated_request["signature_data"], ], ) @@ -526,6 +577,7 @@ def _handle_register_with_license_terms_and_royalty_vault( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_address, + validated_request=validated_request, extra_data=ExtraData( royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), deadline=request_deadline, @@ -567,19 +619,27 @@ def _handle_register_with_derivative_and_royalty_vault( licensing_module_address=licensing_module_client.contract.address, ), ) - + abi_element_identifier = "registerIpAndMakeDerivativeAndDeployRoyaltyVault" + validated_request = { + "nft_contract": nft_contract, + "token_id": token_id, + "metadata": metadata, + "deriv_data": deriv_data, + "royalty_shares": royalty_shares, + "signature_data": { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_response["signature"], + }, + } encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", + abi_element_identifier=abi_element_identifier, args=[ - nft_contract, - token_id, - metadata, - deriv_data, - { - "signer": wallet_address, - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, + validated_request["nft_contract"], + validated_request["token_id"], + validated_request["metadata"], + validated_request["deriv_data"], + validated_request["signature_data"], ], ) @@ -587,6 +647,7 @@ def _handle_register_with_derivative_and_royalty_vault( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_address, + validated_request=validated_request, extra_data=ExtraData( royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), deadline=request_deadline, @@ -624,18 +685,26 @@ def _handle_register_with_license_terms( licensing_module_address=licensing_module_client.contract.address, ), ) + abi_element_identifier = "registerIpAndAttachPILTerms" + validated_request = { + "nft_contract": nft_contract, + "token_id": token_id, + "metadata": metadata, + "license_terms_data": license_terms_data, + "signature_data": { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_data["signature"], + }, + } encoded_data = license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndAttachPILTerms", + abi_element_identifier=abi_element_identifier, args=[ - nft_contract, - token_id, - metadata, - license_terms_data, - { - "signer": wallet_address, - "deadline": calculated_deadline, - "signature": signature_data["signature"], - }, + validated_request["nft_contract"], + validated_request["token_id"], + validated_request["metadata"], + validated_request["license_terms_data"], + validated_request["signature_data"], ], ) @@ -643,6 +712,7 @@ def _handle_register_with_license_terms( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=license_attachment_workflows_address, + validated_request=validated_request, extra_data=None, ) @@ -652,8 +722,9 @@ def _handle_register_with_derivative( nft_contract: Address, deriv_data: dict, metadata: dict, - wallet_address: Address, + token_id: int, ip_id: Address, + wallet_address: Address, calculated_deadline: int, sign_util: Sign, core_metadata_module_client: CoreMetadataModuleClient, @@ -674,18 +745,26 @@ def _handle_register_with_derivative( licensing_module_address=licensing_module_client.contract.address, ), ) + abi_element_identifier = "registerIpAndMakeDerivative" + validated_request = { + "nft_contract": nft_contract, + "token_id": token_id, + "metadata": metadata, + "deriv_data": deriv_data, + "signature_data": { + "signer": wallet_address, + "deadline": calculated_deadline, + "signature": signature_data["signature"], + }, + } encoded_data = derivative_workflows_client.contract.encode_abi( - abi_element_identifier="registerIpAndMakeDerivative", + abi_element_identifier=abi_element_identifier, args=[ - nft_contract, - deriv_data, - metadata, - wallet_address, - { - "signer": wallet_address, - "deadline": calculated_deadline, - "signature": signature_data["signature"], - }, + validated_request["nft_contract"], + validated_request["token_id"], + validated_request["deriv_data"], + validated_request["metadata"], + validated_request["signature_data"], ], ) @@ -693,6 +772,7 @@ def _handle_register_with_derivative( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=derivative_workflows_address, + validated_request=validated_request, extra_data=None, ) diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index 93d34ad3..36aa8842 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -807,10 +807,10 @@ def test_register_ip_and_make_derivative( # Verify args args = call_args[1]["args"] assert args[0] == ADDRESS # nft_contract + assert args[1] == 1 # token_id assert ( - args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() + args[3] == IPMetadata.from_input(IP_METADATA).get_validated_data() ) # metadata - assert args[3] == ACCOUNT_ADDRESS # wallet_address assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" From 8370f38822074c76548b795307d9a97d5688d508 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 14 Jan 2026 16:40:31 +0800 Subject: [PATCH 15/52] refactor: streamline registration request handling by integrating transform_request utility and enhancing ExtraData structure --- .../resources/IPAsset.py | 71 ++++++------------ .../types/resource/IPAsset.py | 7 +- .../transform_registration_request.py | 60 ++++++++-------- .../test_transform_registration_request.py | 72 ++++++++----------- 4 files changed, 82 insertions(+), 128 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 28ca5b85..d3dbef3b 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -57,6 +57,7 @@ from story_protocol_python_sdk.types.resource.IPAsset import ( BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, + ExtraData, LicenseTermsDataInput, LinkDerivativeResponse, MintedNFT, @@ -67,6 +68,7 @@ RegisteredIP, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, + RegisterRegistrationRequest, RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, @@ -92,6 +94,9 @@ from story_protocol_python_sdk.utils.registration.registration_utils import ( validate_license_terms_data, ) +from story_protocol_python_sdk.utils.registration.transform_registration_request import ( + transform_request, +) from story_protocol_python_sdk.utils.royalty import get_royalty_shares from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -1392,58 +1397,24 @@ def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( :return `RegisterAndAttachAndDistributeRoyaltyTokensResponse`: Response with tx hash, license terms IDs, royalty vault address, and distribute royalty tokens transaction hash. """ try: - nft_contract = validate_address(nft_contract) - 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 = validate_license_terms_data(license_terms_data, self.web3) - calculated_deadline = self.sign_util.get_deadline(deadline=deadline) - royalty_shares_obj = get_royalty_shares(royalty_shares) - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "attachLicenseTerms(address,address,uint256)", - }, - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", - }, - ], + transformed_request = transform_request( + request=RegisterRegistrationRequest( + nft_contract=nft_contract, + token_id=token_id, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, ) response = build_and_send_transaction( self.web3, self.account, self.royalty_token_distribution_workflows_client.build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction, - nft_contract, - token_id, - IPMetadata.from_input(ip_metadata).get_validated_data(), - license_terms, - { - "signer": self.web3.to_checksum_address(self.account.address), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, + *transformed_request.validated_request, tx_options=tx_options, ) ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ @@ -1456,15 +1427,15 @@ def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( response["tx_receipt"], ip_registered["ip_id"], ) - + extra_data = cast(ExtraData, transformed_request.extra_data) # Distribute royalty tokens distribute_tx_hash = self._distribute_royalty_tokens( ip_id=ip_registered["ip_id"], - royalty_shares=royalty_shares_obj["royalty_shares"], + royalty_shares=extra_data["royalty_shares"], royalty_vault=royalty_vault, - total_amount=royalty_shares_obj["total_amount"], + total_amount=extra_data["royalty_total_amount"], tx_options=tx_options, - deadline=calculated_deadline, + deadline=extra_data["deadline"], ) return RegisterAndAttachAndDistributeRoyaltyTokensResponse( diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 39208882..676f9598 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -355,9 +355,8 @@ class ExtraData(TypedDict, total=False): """ royalty_shares: list[RoyaltyShareInput] - deadline: int | None - max_license_tokens: list[int | None] - license_terms_data: list[LicenseTermsDataInput] + deadline: int + royalty_total_amount: int @dataclass @@ -375,5 +374,5 @@ class TransformedRegistrationRequest: encoded_tx_data: bytes is_use_multicall3: bool workflow_address: Address - validated_request: dict + validated_request: list extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index e1888386..47b03ee2 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -1,6 +1,7 @@ """Transform registration request utilities.""" from ens.ens import Address, HexStr +from eth_account.signers.local import LocalAccount from typing_extensions import cast from web3 import Web3 @@ -66,10 +67,10 @@ def get_allow_duplicates(allow_duplicates: bool | None, request_type: str) -> bo ) -def transform_registration_request( +def transform_request( request: MintAndRegisterRequest | RegisterRegistrationRequest, web3: Web3, - wallet_address: Address, + account: LocalAccount, chain_id: int, ) -> TransformedRegistrationRequest: """ @@ -84,7 +85,7 @@ def transform_registration_request( Args: request: The registration request (MintAndRegisterRequest or RegisterRegistrationRequest) web3: Web3 instance for contract interaction - wallet_address: The wallet address for signing and recipient fallback + account: The account for signing and recipient fallback chain_id: The chain ID for IP ID calculation Returns: @@ -96,10 +97,10 @@ def transform_registration_request( # Check request type by attribute presence (following TypeScript SDK pattern) if hasattr(request, "spg_nft_contract"): return _handle_mint_and_register_request( - cast(MintAndRegisterRequest, request), web3, wallet_address + cast(MintAndRegisterRequest, request), web3, account.address ) elif hasattr(request, "nft_contract") and hasattr(request, "token_id"): - return _handle_register_request(request, web3, wallet_address, chain_id) + return _handle_register_request(request, web3, account, chain_id) else: raise ValueError("Invalid registration request type") @@ -149,7 +150,6 @@ def _handle_mint_and_register_request( if request.royalty_shares else None ) - metadata = IPMetadata.from_input(request.ip_metadata).get_validated_data() # Build encoded data based on request type if license_terms_data and royalty_shares: @@ -398,7 +398,7 @@ def _handle_mint_and_register_with_derivative( def _handle_register_request( request: RegisterRegistrationRequest, web3: Web3, - wallet_address: Address, + account: LocalAccount, chain_id: int, ) -> TransformedRegistrationRequest: """ @@ -417,11 +417,13 @@ def _handle_register_request( ip_id = ip_asset_registry_client.ipId( chain_id, request.nft_contract, request.token_id ) - if not ip_asset_registry_client.isRegistered(ip_id): - raise ValueError(f"The NFT with id {request.token_id} is not registered as IP.") + if ip_asset_registry_client.isRegistered(ip_id): + raise ValueError( + f"The NFT with id {request.token_id} is already registered as IP." + ) nft_contract = validate_address(request.nft_contract) - sign_util = Sign(web3=web3, chain_id=chain_id, account=wallet_address) + sign_util = Sign(web3=web3, chain_id=chain_id, account=account) core_metadata_module_client = CoreMetadataModuleClient(web3) licensing_module_client = LicensingModuleClient(web3) license_terms_data = ( @@ -437,13 +439,12 @@ def _handle_register_request( else None ) royalty_shares = ( - get_royalty_shares(request.royalty_shares)["royalty_shares"] - if request.royalty_shares - else None + get_royalty_shares(request.royalty_shares) if request.royalty_shares else None ) state = web3.to_bytes(hexstr=HexStr(ZERO_HASH)) metadata = IPMetadata.from_input(request.ip_metadata).get_validated_data() calculated_deadline = sign_util.get_deadline(deadline=request.deadline) + wallet_address = account.address if license_terms_data and royalty_shares: return _handle_register_with_license_terms_and_royalty_vault( web3=web3, @@ -451,11 +452,11 @@ def _handle_register_request( token_id=request.token_id, metadata=metadata, license_terms_data=license_terms_data, - royalty_shares=royalty_shares, + royalty_shares=royalty_shares["royalty_shares"], + royalty_total_amount=royalty_shares["total_amount"], ip_id=ip_id, wallet_address=wallet_address, calculated_deadline=calculated_deadline, - request_deadline=request.deadline, sign_util=sign_util, core_metadata_module_client=core_metadata_module_client, licensing_module_client=licensing_module_client, @@ -469,7 +470,7 @@ def _handle_register_request( token_id=request.token_id, metadata=metadata, deriv_data=deriv_data, - royalty_shares=royalty_shares, + royalty_shares=royalty_shares["royalty_shares"], ip_id=ip_id, wallet_address=wallet_address, calculated_deadline=calculated_deadline, @@ -526,11 +527,11 @@ def _handle_register_with_license_terms_and_royalty_vault( ip_id: Address, wallet_address: Address, calculated_deadline: int, - request_deadline: int | None, sign_util: Sign, core_metadata_module_client: CoreMetadataModuleClient, licensing_module_client: LicensingModuleClient, state: bytes, + royalty_total_amount: int, ) -> TransformedRegistrationRequest: """Handle registerIpAndAttachPILTermsAndDeployRoyaltyVault.""" royalty_token_distribution_workflows_client = ( @@ -551,26 +552,20 @@ def _handle_register_with_license_terms_and_royalty_vault( ), ) abi_element_identifier = "registerIpAndAttachPILTermsAndDeployRoyaltyVault" - validated_request = { - "nft_contract": nft_contract, - "token_id": token_id, - "metadata": metadata, - "license_terms_data": license_terms_data, - "signature_data": { + validated_request = [ + nft_contract, + token_id, + metadata, + license_terms_data, + { "signer": wallet_address, "deadline": calculated_deadline, "signature": signature_data["signature"], }, - } + ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["nft_contract"], - validated_request["token_id"], - validated_request["metadata"], - validated_request["license_terms_data"], - validated_request["signature_data"], - ], + args=validated_request, ) return TransformedRegistrationRequest( @@ -580,7 +575,8 @@ def _handle_register_with_license_terms_and_royalty_vault( validated_request=validated_request, extra_data=ExtraData( royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), - deadline=request_deadline, + deadline=calculated_deadline, + royalty_total_amount=royalty_total_amount, ), ) diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index 36aa8842..9f1800d2 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -2,6 +2,7 @@ import pytest from typing_extensions import cast +from web3 import Account from story_protocol_python_sdk import ( DerivativeDataInput, @@ -21,10 +22,9 @@ from story_protocol_python_sdk.utils.ip_metadata import IPMetadata from story_protocol_python_sdk.utils.registration.transform_registration_request import ( get_allow_duplicates, - transform_registration_request, + transform_request, ) from tests.unit.fixtures.data import ( - ACCOUNT_ADDRESS, ADDRESS, CHAIN_ID, IP_ID, @@ -155,7 +155,7 @@ def _mock(): def mock_ip_asset_registry_client(): """Mock IPAssetRegistryClient.""" - def _mock(is_registered: bool = True, ip_id: str = IP_ID): + def _mock(is_registered: bool = False, ip_id: str = IP_ID): mock_client = MagicMock() mock_client.ipId = MagicMock(return_value=ip_id) mock_client.isRegistered = MagicMock(return_value=is_registered) @@ -215,6 +215,12 @@ def _mock(): return _mock +account = Account.from_key( + "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +) +ACCOUNT_ADDRESS = account.address + + class TestGetAllowDuplicates: def test_returns_default_for_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( self, @@ -273,9 +279,7 @@ def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_pres patches[1], patches[2], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) # Assert real encoding result (not mock value) license_attachment_client.contract.encode_abi.assert_called_once() call_args = license_attachment_client.contract.encode_abi.call_args @@ -325,9 +329,7 @@ def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_ module_patches[0], module_patches[1], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) # Assert real encoding result (not mock value) license_attachment_client.contract.encode_abi.assert_called_once() assert result.workflow_address == "license_attachment_client_address" @@ -353,23 +355,23 @@ def test_raises_error_for_invalid_request_type( ): with mock_ip_asset_registry_client(): with pytest.raises(ValueError, match="Invalid register request type"): - transform_registration_request( + transform_request( RegisterRegistrationRequest( nft_contract=ADDRESS, token_id=1, ), mock_web3, - ACCOUNT_ADDRESS, + account, CHAIN_ID, ) def test_raises_error_for_invalid_registration_request_type(self, mock_web3): """Test that ValueError is raised when request doesn't match any known type.""" with pytest.raises(ValueError, match="Invalid registration request type"): - transform_registration_request( + transform_request( None, # type: ignore[arg-type] mock_web3, - ACCOUNT_ADDRESS, + account, CHAIN_ID, ) @@ -403,9 +405,7 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens patches[1], patches[2], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) royalty_token_distribution_client.contract.encode_abi.assert_called_once() call_args = royalty_token_distribution_client.contract.encode_abi.call_args @@ -463,9 +463,7 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( patches[1], patches[2], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) royalty_token_distribution_client.contract.encode_abi.assert_called_once() call_args = royalty_token_distribution_client.contract.encode_abi.call_args @@ -519,9 +517,7 @@ def test_mint_and_register_ip_and_make_derivative( patches[1], patches[2], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) # Assert real encoding result (not mock value) derivative_workflows_client.contract.encode_abi.assert_called_once() call_args = derivative_workflows_client.contract.encode_abi.call_args @@ -562,9 +558,7 @@ def test_raises_error_for_invalid_mint_and_register_request_type( with pytest.raises( ValueError, match="Invalid mint and register request type" ): - transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + transform_request(request, mock_web3, account, CHAIN_ID) class TestHandleRegisterRequest: @@ -603,9 +597,7 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( module_patches[0], module_patches[1], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) royalty_token_distribution_client.contract.encode_abi.assert_called_once() call_args = royalty_token_distribution_client.contract.encode_abi.call_args @@ -628,6 +620,10 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( ) assert result.extra_data is not None royalty_shares = result.extra_data["royalty_shares"] + royalty_total_amount = cast(dict[str, int], result.extra_data)[ + "royalty_total_amount" + ] + assert royalty_total_amount == 50 * 10**6 assert len(royalty_shares) == 1 royalty_share_dict = cast(list[dict[str, str | int]], royalty_shares)[0] assert royalty_share_dict["recipient"] == ADDRESS @@ -671,9 +667,7 @@ def test_register_ip_and_make_derivative_and_deploy_royalty_vault( module_patches[0], module_patches[1], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) # Verify encode_abi was called with correct method and arguments royalty_token_distribution_client.contract.encode_abi.assert_called_once() @@ -731,9 +725,7 @@ def test_register_ip_and_attach_pil_terms( module_patches[0], module_patches[1], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) # Verify encode_abi was called with correct method and arguments license_attachment_client.contract.encode_abi.assert_called_once() @@ -791,9 +783,7 @@ def test_register_ip_and_make_derivative( module_patches[0], module_patches[1], ): - result = transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + result = transform_request(request, mock_web3, account, CHAIN_ID) # Verify encode_abi was called with correct method and arguments derivative_workflows_client.contract.encode_abi.assert_called_once() @@ -827,11 +817,9 @@ def test_raises_error_when_ip_not_registered( license_terms_data=LICENSE_TERMS_DATA, ) with ( - mock_ip_asset_registry_client(is_registered=False), + mock_ip_asset_registry_client(is_registered=True), pytest.raises( - ValueError, match="The NFT with id 1 is not registered as IP." + ValueError, match="The NFT with id 1 is already registered as IP." ), ): - transform_registration_request( - request, mock_web3, ACCOUNT_ADDRESS, CHAIN_ID - ) + transform_request(request, mock_web3, account, CHAIN_ID) From 647b41c2cbbd22ee0cc4823f12f7d057e13802d8 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 15 Jan 2026 16:51:28 +0800 Subject: [PATCH 16/52] refactor: register_ip_and_attach_pil_terms_and_distribute_royalty_tokens with transfer request --- .../resources/IPAsset.py | 5 + tests/unit/resources/test_ip_asset.py | 264 +++++++++++++++--- 2 files changed, 231 insertions(+), 38 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index d3dbef3b..305ef349 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1397,6 +1397,11 @@ def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( :return `RegisterAndAttachAndDistributeRoyaltyTokensResponse`: Response with tx hash, license terms IDs, royalty vault address, and distribute royalty tokens transaction hash. """ try: + if not royalty_shares: + raise ValueError("Royalty shares must be provided.") + if not license_terms_data: + raise ValueError("License terms data must be provided.") + transformed_request = transform_request( request=RegisterRegistrationRequest( nft_contract=nft_contract, diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 2f682c01..367f083e 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from ens.ens import HexStr @@ -138,6 +138,135 @@ def _mock(owner=ACCOUNT_ADDRESS): return _mock +@pytest.fixture(scope="class") +def mock_transform_request_dependencies(): + """ + In order to coverage edge cases, we need to mock all dependencies of transform_request. + Mock all dependencies of transform_request for detailed testing. + + This fixture mocks all the internal dependencies of transform_request: + - IPAssetRegistryClient (for IP ID and registration check) + - Sign utility (for signatures) + - CoreMetadataModuleClient (for metadata operations) + - LicensingModuleClient (for licensing operations) + - RoyaltyTokenDistributionWorkflowsClient (for transaction building) + - RoyaltyModuleClient (for validate_license_terms_data) + - ModuleRegistryClient (for validate_license_terms_data) + + This allows testing the actual transform_request logic while mocking only + the external dependencies. The validate_license_terms_data function will + use the real implementation since its dependencies are mocked. + + Usage: + with mock_transform_request_dependencies( + is_registered=False, + ip_id=IP_ID, + deadline=1000, + signature=b"signature", + license_terms_data=LICENSE_TERMS_DATA, + ): + """ + + def _mock( + is_registered: bool = False, + ip_id: str = IP_ID, + deadline: int = 1000, + signature: bytes = b"signature", + license_terms_data: list | None = None, + ): + # Mock IPAssetRegistryClient + mock_ip_registry_client = MagicMock() + mock_ip_registry_client.ipId = MagicMock(return_value=ip_id) + mock_ip_registry_client.isRegistered = MagicMock(return_value=is_registered) + + # Mock Sign utility + mock_sign_util = MagicMock() + mock_sign_util.get_deadline = MagicMock(return_value=deadline) + mock_sign_util.get_permission_signature = MagicMock( + return_value={"signature": signature} + ) + + # Mock CoreMetadataModuleClient + mock_core_metadata_contract = MagicMock() + mock_core_metadata_contract.address = ADDRESS + mock_core_metadata_client = MagicMock() + mock_core_metadata_client.contract = mock_core_metadata_contract + + # Mock LicensingModuleClient + mock_licensing_contract = MagicMock() + mock_licensing_contract.address = ADDRESS + mock_licensing_client = MagicMock() + mock_licensing_client.contract = mock_licensing_contract + + # Mock RoyaltyTokenDistributionWorkflowsClient + mock_royalty_workflows_contract = MagicMock() + mock_royalty_workflows_contract.address = ADDRESS + mock_royalty_workflows_contract.encode_abi = MagicMock( + return_value=b"encoded_data" + ) + mock_royalty_workflows_client = MagicMock() + mock_royalty_workflows_client.contract = mock_royalty_workflows_contract + + # Mock RoyaltyModuleClient (for validate_license_terms_data) + mock_royalty_module_client = MagicMock() + mock_royalty_module_client.isWhitelistedRoyaltyPolicy = MagicMock( + return_value=True + ) + mock_royalty_module_client.isWhitelistedRoyaltyToken = MagicMock( + return_value=True + ) + + # Mock ModuleRegistryClient (for validate_license_terms_data) + mock_module_registry_client = MagicMock() + + # Create patches + patches = [ + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.IPAssetRegistryClient", + return_value=mock_ip_registry_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.Sign", + return_value=mock_sign_util, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.CoreMetadataModuleClient", + return_value=mock_core_metadata_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.LicensingModuleClient", + return_value=mock_licensing_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyTokenDistributionWorkflowsClient", + return_value=mock_royalty_workflows_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.registration_utils.RoyaltyModuleClient", + return_value=mock_royalty_module_client, + ), + patch( + "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", + return_value=mock_module_registry_client, + ), + ] + + # Return context manager that applies all patches + class MockContext: + def __enter__(self): + for p in patches: + p.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for p in reversed(patches): + p.stop() + + return MockContext() + + return _mock + + class TestIPAssetRegister: def test_register_invalid_deadline_type( self, ip_asset, mock_get_ip_id, mock_is_registered @@ -1458,13 +1587,27 @@ def test_token_id_is_already_registered( ], ) + def test_throw_error_when_license_terms_data_is_empty( + self, ip_asset: IPAsset, mock_transform_request_dependencies + ): + with (mock_transform_request_dependencies(),): + with pytest.raises( + ValueError, + match="Failed to register IP, attach PIL terms and distribute royalty tokens: License terms data must be provided.", + ): + ip_asset.register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[], + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0) + ], + ) + def test_throw_error_when_royalty_shares_empty( - self, ip_asset: IPAsset, mock_get_ip_id, mock_is_registered + self, ip_asset: IPAsset, mock_transform_request_dependencies ): - with ( - mock_get_ip_id(), - mock_is_registered(), - ): + with (mock_transform_request_dependencies(),): with pytest.raises( ValueError, match="Failed to register IP, attach PIL terms and distribute royalty tokens: Royalty shares must be provided.", @@ -1479,13 +1622,10 @@ def test_throw_error_when_royalty_shares_empty( def test_success_with_default_values( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, mock_parse_tx_license_terms_attached_event, - mock_signature_related_methods, mock_get_royalty_vault_address_by_ip_id, - mock_ip_account_impl_client, ): royalty_shares = [ RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), @@ -1493,13 +1633,79 @@ def test_success_with_default_values( ] with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), + mock_parse_ip_registered_event(), + mock_parse_tx_license_terms_attached_event(), + mock_get_royalty_vault_address_by_ip_id(), + ): + with ( + patch.object( + ip_asset.royalty_token_distribution_workflows_client, + "build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction", + return_value={"tx_hash": TX_HASH.hex()}, + ), + patch.object( + ip_asset, + "_distribute_royalty_tokens", + return_value=TX_HASH.hex(), + ) as mock_distribute, + ): + result = ip_asset.register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=royalty_shares, + ) + + # Verify distribute was called with correct arguments + mock_distribute.assert_called_once() + call_kwargs = mock_distribute.call_args[1] + assert call_kwargs["ip_id"] == IP_ID + assert ( + call_kwargs["royalty_shares"] + == get_royalty_shares(royalty_shares)["royalty_shares"] + ) + assert call_kwargs["royalty_vault"] == ADDRESS + assert ( + call_kwargs["total_amount"] + == get_royalty_shares(royalty_shares)["total_amount"] + ) + + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + assert result["token_id"] == 3 + assert result["license_terms_ids"] == [1, 2] + assert result["royalty_vault"] == ADDRESS + assert result["distribute_royalty_tokens_tx_hash"] == TX_HASH.hex() + + def test_success_with_default_values_using_detailed_mocks( + self, + ip_asset: IPAsset, + mock_transform_request_dependencies, + mock_parse_ip_registered_event, + mock_parse_tx_license_terms_attached_event, + mock_get_royalty_vault_address_by_ip_id, + ): + """ + Test using detailed mocks for transform_request dependencies. + This approach mocks all internal dependencies instead of the entire transform_request function. + """ + royalty_shares = [ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + RoyaltyShareInput(recipient=ADDRESS, percentage=30.0), + ] + + with ( + mock_transform_request_dependencies( + is_registered=False, + ip_id=IP_ID, + deadline=1000, + signature=b"signature", + license_terms_data=LICENSE_TERMS_DATA, + ), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), - mock_signature_related_methods(), mock_get_royalty_vault_address_by_ip_id(), - mock_ip_account_impl_client(), ): with ( patch.object( @@ -1544,26 +1750,20 @@ def test_success_with_default_values( def test_success_with_custom_values( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, mock_parse_tx_license_terms_attached_event, - mock_signature_related_methods, mock_get_royalty_vault_address_by_ip_id, - mock_ip_account_impl_client, ): royalty_shares = [ RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=60.0), ] with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), - mock_signature_related_methods(), mock_get_royalty_vault_address_by_ip_id(), - mock_ip_account_impl_client(), ): with ( patch.object( @@ -1605,15 +1805,9 @@ def test_success_with_custom_values( def test_throw_error_when_transaction_failed( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, - mock_signature_related_methods, + mock_transform_request_dependencies, ): - with ( - mock_get_ip_id(), - mock_is_registered(), - mock_signature_related_methods(), - ): + with (mock_transform_request_dependencies(),): with patch.object( ip_asset.royalty_token_distribution_workflows_client, "build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction", @@ -2136,11 +2330,8 @@ def test_throw_not_provided_license_terms_data_when_royalty_shares_provided_for_ def test_success_when_license_terms_data_and_royalty_shares_provided_for_minted_nft( self, ip_asset: IPAsset, - mock_is_registered, - mock_get_ip_id, - mock_signature_related_methods, + mock_transform_request_dependencies, mock_parse_ip_registered_event, - mock_ip_account_impl_client, mock_parse_tx_license_terms_attached_event, mock_get_royalty_vault_address_by_ip_id, ): @@ -2150,11 +2341,8 @@ def test_success_when_license_terms_data_and_royalty_shares_provided_for_minted_ royalty_shares_obj = get_royalty_shares(royalty_shares) royalty_vault = HexStr("0x" + "a" * 64) with ( - mock_is_registered(is_registered=False), - mock_get_ip_id(), - mock_signature_related_methods(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), - mock_ip_account_impl_client(), mock_parse_tx_license_terms_attached_event(), mock_get_royalty_vault_address_by_ip_id(royalty_vault), patch.object( From 97c9f2ef79e8869e4a920ebc03e4348f5839f183 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 10:32:21 +0800 Subject: [PATCH 17/52] refactor: register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens with transform_request --- .../resources/IPAsset.py | 64 +++----- .../transform_registration_request.py | 30 ++-- tests/unit/resources/test_ip_asset.py | 139 +++--------------- 3 files changed, 53 insertions(+), 180 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 305ef349..6166358e 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1290,54 +1290,28 @@ def register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( :return `RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse`: Response with tx hash, IP ID, token ID, royalty vault address, and distribute royalty tokens transaction hash. """ try: - nft_contract = validate_address(nft_contract) - 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." - ) - - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=deriv_data - ).get_validated_data() - calculated_deadline = self.sign_util.get_deadline(deadline=deadline) - royalty_shares_obj = get_royalty_shares(royalty_shares) + if not royalty_shares: + raise ValueError("Royalty shares must be provided.") - signature_response = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", - }, - { - "ipId": ip_id, - "signer": self.royalty_token_distribution_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,uint32)", - }, - ], + transformed_request = transform_request( + request=RegisterRegistrationRequest( + nft_contract=nft_contract, + token_id=token_id, + deriv_data=deriv_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + deadline=deadline, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, ) response = build_and_send_transaction( self.web3, self.account, self.royalty_token_distribution_workflows_client.build_registerIpAndMakeDerivativeAndDeployRoyaltyVault_transaction, - nft_contract, - token_id, - IPMetadata.from_input(ip_metadata).get_validated_data(), - validated_deriv_data, - { - "signer": self.web3.to_checksum_address(self.account.address), - "deadline": calculated_deadline, - "signature": signature_response["signature"], - }, + *transformed_request.validated_request, tx_options=tx_options, ) @@ -1348,15 +1322,15 @@ def register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( response["tx_receipt"], ip_registered["ip_id"], ) - + extra_data = cast(ExtraData, transformed_request.extra_data) # Distribute royalty tokens distribute_tx_hash = self._distribute_royalty_tokens( ip_id=ip_registered["ip_id"], - royalty_shares=royalty_shares_obj["royalty_shares"], + royalty_shares=extra_data["royalty_shares"], royalty_vault=royalty_vault, - total_amount=royalty_shares_obj["total_amount"], + total_amount=extra_data["royalty_total_amount"], tx_options=tx_options, - deadline=calculated_deadline, + deadline=extra_data["deadline"], ) return RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse( diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 47b03ee2..8f5a805a 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -474,11 +474,11 @@ def _handle_register_request( ip_id=ip_id, wallet_address=wallet_address, calculated_deadline=calculated_deadline, - request_deadline=request.deadline, sign_util=sign_util, core_metadata_module_client=core_metadata_module_client, licensing_module_client=licensing_module_client, state=state, + royalty_total_amount=royalty_shares["total_amount"], ) elif license_terms_data: @@ -591,11 +591,11 @@ def _handle_register_with_derivative_and_royalty_vault( ip_id: Address, wallet_address: Address, calculated_deadline: int, - request_deadline: int | None, sign_util: Sign, core_metadata_module_client: CoreMetadataModuleClient, licensing_module_client: LicensingModuleClient, state: bytes, + royalty_total_amount: int, ) -> TransformedRegistrationRequest: """Handle registerIpAndMakeDerivativeAndDeployRoyaltyVault.""" royalty_token_distribution_workflows_client = ( @@ -616,27 +616,20 @@ def _handle_register_with_derivative_and_royalty_vault( ), ) abi_element_identifier = "registerIpAndMakeDerivativeAndDeployRoyaltyVault" - validated_request = { - "nft_contract": nft_contract, - "token_id": token_id, - "metadata": metadata, - "deriv_data": deriv_data, - "royalty_shares": royalty_shares, - "signature_data": { + validated_request = [ + nft_contract, + token_id, + metadata, + deriv_data, + { "signer": wallet_address, "deadline": calculated_deadline, "signature": signature_response["signature"], }, - } + ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["nft_contract"], - validated_request["token_id"], - validated_request["metadata"], - validated_request["deriv_data"], - validated_request["signature_data"], - ], + args=validated_request, ) return TransformedRegistrationRequest( @@ -646,7 +639,8 @@ def _handle_register_with_derivative_and_royalty_vault( validated_request=validated_request, extra_data=ExtraData( royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), - deadline=request_deadline, + deadline=calculated_deadline, + royalty_total_amount=royalty_total_amount, ), ) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 367f083e..f2ae449c 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -172,7 +172,6 @@ def _mock( ip_id: str = IP_ID, deadline: int = 1000, signature: bytes = b"signature", - license_terms_data: list | None = None, ): # Mock IPAssetRegistryClient mock_ip_registry_client = MagicMock() @@ -1678,75 +1677,6 @@ def test_success_with_default_values( assert result["royalty_vault"] == ADDRESS assert result["distribute_royalty_tokens_tx_hash"] == TX_HASH.hex() - def test_success_with_default_values_using_detailed_mocks( - self, - ip_asset: IPAsset, - mock_transform_request_dependencies, - mock_parse_ip_registered_event, - mock_parse_tx_license_terms_attached_event, - mock_get_royalty_vault_address_by_ip_id, - ): - """ - Test using detailed mocks for transform_request dependencies. - This approach mocks all internal dependencies instead of the entire transform_request function. - """ - royalty_shares = [ - RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), - RoyaltyShareInput(recipient=ADDRESS, percentage=30.0), - ] - - with ( - mock_transform_request_dependencies( - is_registered=False, - ip_id=IP_ID, - deadline=1000, - signature=b"signature", - license_terms_data=LICENSE_TERMS_DATA, - ), - mock_parse_ip_registered_event(), - mock_parse_tx_license_terms_attached_event(), - mock_get_royalty_vault_address_by_ip_id(), - ): - with ( - patch.object( - ip_asset.royalty_token_distribution_workflows_client, - "build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction", - return_value={"tx_hash": TX_HASH.hex()}, - ), - patch.object( - ip_asset, - "_distribute_royalty_tokens", - return_value=TX_HASH.hex(), - ) as mock_distribute, - ): - result = ip_asset.register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( - nft_contract=ADDRESS, - token_id=3, - license_terms_data=LICENSE_TERMS_DATA, - royalty_shares=royalty_shares, - ) - - # Verify distribute was called with correct arguments - mock_distribute.assert_called_once() - call_kwargs = mock_distribute.call_args[1] - assert call_kwargs["ip_id"] == IP_ID - assert ( - call_kwargs["royalty_shares"] - == get_royalty_shares(royalty_shares)["royalty_shares"] - ) - assert call_kwargs["royalty_vault"] == ADDRESS - assert ( - call_kwargs["total_amount"] - == get_royalty_shares(royalty_shares)["total_amount"] - ) - - assert result["tx_hash"] == TX_HASH.hex() - assert result["ip_id"] == IP_ID - assert result["token_id"] == 3 - assert result["license_terms_ids"] == [1, 2] - assert result["royalty_vault"] == ADDRESS - assert result["distribute_royalty_tokens_tx_hash"] == TX_HASH.hex() - def test_success_with_custom_values( self, ip_asset: IPAsset, @@ -1831,12 +1761,9 @@ def test_throw_error_when_transaction_failed( class TestRegisterDerivativeIpAndAttachPilTermsAndDistributeRoyaltyTokens: def test_token_id_is_already_registered( - self, ip_asset: IPAsset, mock_get_ip_id, mock_is_registered + self, ip_asset: IPAsset, mock_transform_request_dependencies ): - with ( - mock_get_ip_id(), - mock_is_registered(True), - ): + with (mock_transform_request_dependencies(is_registered=True),): with pytest.raises( ValueError, match="Failed to register derivative IP and distribute royalty tokens: The NFT with id 3 is already registered as IP.", @@ -1856,13 +1783,11 @@ def test_token_id_is_already_registered( def test_throw_error_when_royalty_shares_empty( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_license_registry_client, ): with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_license_registry_client(), ): with pytest.raises( @@ -1882,12 +1807,9 @@ def test_throw_error_when_royalty_shares_empty( def test_success_with_default_values( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, - mock_signature_related_methods, mock_get_royalty_vault_address_by_ip_id, - mock_ip_account_impl_client, mock_license_registry_client, ): royalty_shares = [ @@ -1896,12 +1818,9 @@ def test_success_with_default_values( ] with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), - mock_signature_related_methods(), mock_get_royalty_vault_address_by_ip_id(), - mock_ip_account_impl_client(), mock_license_registry_client(), ): with ( @@ -1950,12 +1869,9 @@ def test_success_with_default_values( def test_success_with_custom_values( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, - mock_signature_related_methods, mock_get_royalty_vault_address_by_ip_id, - mock_ip_account_impl_client, mock_license_registry_client, ): royalty_shares = [ @@ -1963,12 +1879,9 @@ def test_success_with_custom_values( ] with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), - mock_signature_related_methods(), mock_get_royalty_vault_address_by_ip_id(), - mock_ip_account_impl_client(), mock_license_registry_client(), ): with ( @@ -2023,15 +1936,11 @@ def test_success_with_custom_values( def test_throw_error_when_transaction_failed( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, - mock_signature_related_methods, + mock_transform_request_dependencies, mock_license_registry_client, ): with ( - mock_get_ip_id(), - mock_is_registered(), - mock_signature_related_methods(), + mock_transform_request_dependencies(), mock_license_registry_client(), ): with patch.object( @@ -2060,13 +1969,12 @@ def test_throw_error_when_transaction_failed( def test_success_with_tx_options( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, - mock_signature_related_methods, mock_get_royalty_vault_address_by_ip_id, - mock_ip_account_impl_client, mock_license_registry_client, + mock_ip_account_impl_client, + mock_signature_related_methods, ): royalty_shares = [ RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=60.0), @@ -2078,13 +1986,12 @@ def test_success_with_tx_options( "chainId": 1, } with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), - mock_signature_related_methods(), mock_get_royalty_vault_address_by_ip_id(), - mock_ip_account_impl_client(), mock_license_registry_client(), + mock_ip_account_impl_client(), + mock_signature_related_methods(), ): with patch( "story_protocol_python_sdk.resources.IPAsset.build_and_send_transaction" @@ -3076,21 +2983,19 @@ def test_throw_error_when_deriv_data_is_not_provided_and_royalty_shares_are_prov def test_success_when_deriv_data_and_royalty_shares_are_provided_for_minted_nft( self, ip_asset: IPAsset, + mock_transform_request_dependencies, + mock_license_registry_client, 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_signature_related_methods, mock_ip_account_impl_client, ): with ( - mock_get_ip_id(), - mock_is_registered(is_registered=False), + mock_transform_request_dependencies(), + mock_license_registry_client(), mock_parse_ip_registered_event(), - mock_signature_related_methods(), mock_get_royalty_vault_address_by_ip_id(), - mock_license_registry_client(), + mock_signature_related_methods(), mock_ip_account_impl_client(), patch.object( ip_asset.royalty_token_distribution_workflows_client, From ff16fac4a674669c98d68a05a9b34d34b59b5961 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 10:58:12 +0800 Subject: [PATCH 18/52] refactor: simplify register_ip_and_attach_pil_terms by integrating transform_request and improving error handling for license terms data --- .../resources/IPAsset.py | 89 ++++---------- .../transform_registration_request.py | 22 ++-- tests/unit/resources/test_ip_asset.py | 115 ++++++++++-------- 3 files changed, 95 insertions(+), 131 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 6166358e..c4d8c6e4 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -758,78 +758,35 @@ def register_ip_and_attach_pil_terms( :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 = validate_license_terms_data(license_terms_data, self.web3) - calculated_deadline = self.sign_util.get_deadline(deadline=deadline) + if not license_terms_data: + raise ValueError("License terms data must be provided.") - # 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=HexStr(ZERO_HASH)), - permissions=[ - { - "ipId": ip_id, - "signer": self.license_attachment_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.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": AccessPermission.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": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", - }, - ], + transformed_request = transform_request( + request=RegisterRegistrationRequest( + nft_contract=nft_contract, + token_id=token_id, + license_terms_data=license_terms_data, + ip_metadata=( + IPMetadataInput( + ip_metadata_uri=ip_metadata["ip_metadata_uri"], + ip_metadata_hash=ip_metadata["ip_metadata_hash"], + nft_metadata_uri=ip_metadata["nft_metadata_uri"], + nft_metadata_hash=ip_metadata["nft_metadata_hash"], + ) + if ip_metadata + else None + ), + deadline=deadline, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, ) - - metadata = { - "ipMetadataURI": "", - "ipMetadataHash": ZERO_HASH, - "nftMetadataURI": "", - "nftMetadataHash": ZERO_HASH, - } - - 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 - ), - } - ) - 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_response["signature"], - }, + *transformed_request.validated_request, tx_options=tx_options, ) diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 8f5a805a..514bb976 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -676,26 +676,20 @@ def _handle_register_with_license_terms( ), ) abi_element_identifier = "registerIpAndAttachPILTerms" - validated_request = { - "nft_contract": nft_contract, - "token_id": token_id, - "metadata": metadata, - "license_terms_data": license_terms_data, - "signature_data": { + validated_request = [ + nft_contract, + token_id, + metadata, + license_terms_data, + { "signer": wallet_address, "deadline": calculated_deadline, "signature": signature_data["signature"], }, - } + ] encoded_data = license_attachment_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["nft_contract"], - validated_request["token_id"], - validated_request["metadata"], - validated_request["license_terms_data"], - validated_request["signature_data"], - ], + args=validated_request, ) return TransformedRegistrationRequest( diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index f2ae449c..fdac1924 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -436,23 +436,39 @@ def test_mint_failed_transaction(self, ip_asset): class TestRegisterIpAndAttachPilTerms: + def test_throw_error_when_license_terms_data_is_not_provided( + self, ip_asset: IPAsset, mock_transform_request_dependencies + ): + with mock_transform_request_dependencies(): + with pytest.raises( + ValueError, match="License terms data must be provided." + ): + ip_asset.register_ip_and_attach_pil_terms( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[], + ) + def test_token_id_is_already_registered( - self, ip_asset, mock_get_ip_id, mock_is_registered + self, ip_asset, mock_transform_request_dependencies, mock_is_registered ): - with mock_get_ip_id(), mock_is_registered(True): + with ( + mock_transform_request_dependencies(is_registered=True), + mock_is_registered(True), + ): with pytest.raises( ValueError, match="The NFT with id 3 is already registered as IP." ): ip_asset.register_ip_and_attach_pil_terms( nft_contract=ADDRESS, token_id=3, - license_terms_data=[], + license_terms_data=LICENSE_TERMS_DATA, ) def test_royalty_policy_commercial_rev_share_is_less_than_0( - self, ip_asset: IPAsset, mock_get_ip_id, mock_is_registered + self, ip_asset: IPAsset, mock_transform_request_dependencies ): - with mock_get_ip_id(), mock_is_registered(): + with mock_transform_request_dependencies(): with pytest.raises( PILFlavorError, match="commercial_rev_share must be between 0 and 100." ): @@ -473,18 +489,14 @@ def test_royalty_policy_commercial_rev_share_is_less_than_0( def test_transaction_to_be_called_with_correct_parameters( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, mock_parse_tx_license_terms_attached_event, - mock_signature_related_methods, ): with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), - mock_signature_related_methods(), ): with patch.object( ip_asset.license_attachment_workflows_client, @@ -541,41 +553,50 @@ def test_transaction_to_be_called_with_correct_parameters( def test_success( self, ip_asset: IPAsset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, - mock_signature_related_methods, mock_parse_tx_license_terms_attached_event, ): with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), - mock_signature_related_methods(), ): - result = ip_asset.register_ip_and_attach_pil_terms( - nft_contract=ADDRESS, - token_id=3, - license_terms_data=[ - { - "terms": LICENSE_TERMS, - "licensing_config": LICENSING_CONFIG, - } - ], - ip_metadata={ - "ip_metadata_uri": "https://example.com/metadata/custom-value.json", - "ip_metadata_hash": "ip_metadata_hash", - "nft_metadata_uri": "https://example.com/metadata/custom-value.json", - "nft_metadata_hash": "nft_metadata_hash", - }, - ) - assert result == { - "tx_hash": TX_HASH.hex(), - "ip_id": IP_ID, - "license_terms_ids": [1, 2], - "token_id": 3, - } + with patch.object( + ip_asset.license_attachment_workflows_client, + "build_registerIpAndAttachPILTerms_transaction", + ) as mock_build_registerIpAndAttachPILTerms_transaction: + result = ip_asset.register_ip_and_attach_pil_terms( + nft_contract=ADDRESS, + token_id=3, + license_terms_data=[ + { + "terms": LICENSE_TERMS, + "licensing_config": LICENSING_CONFIG, + } + ], + ip_metadata={ + "ip_metadata_uri": "https://example.com/metadata/custom-value.json", + "ip_metadata_hash": "ip_metadata_hash", + "nft_metadata_uri": "https://example.com/metadata/custom-value.json", + "nft_metadata_hash": "nft_metadata_hash", + }, + ) + call_args = ( + mock_build_registerIpAndAttachPILTerms_transaction.call_args[0] + ) + assert call_args[2] == { + "ipMetadataURI": "https://example.com/metadata/custom-value.json", + "ipMetadataHash": "ip_metadata_hash", + "nftMetadataURI": "https://example.com/metadata/custom-value.json", + "nftMetadataHash": "nft_metadata_hash", + } + assert result == { + "tx_hash": TX_HASH.hex(), + "ip_id": IP_ID, + "license_terms_ids": [1, 2], + "token_id": 3, + } class TestRegisterDerivative: @@ -2692,16 +2713,12 @@ def test_success_when_license_terms_data_provided_for_minted_nft( ip_asset: IPAsset, mock_parse_ip_registered_event, mock_parse_tx_license_terms_attached_event, - mock_get_ip_id, - mock_signature_related_methods, - mock_is_registered, + mock_transform_request_dependencies, ): with ( mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), - mock_get_ip_id(), - mock_signature_related_methods(), - mock_is_registered(is_registered=False), + mock_transform_request_dependencies(), patch.object( ip_asset.license_attachment_workflows_client, "build_registerIpAndAttachPILTerms_transaction", @@ -2731,16 +2748,12 @@ def test_success_when_license_terms_data_is_commercial_use_for_minted_nft( ip_asset: IPAsset, mock_parse_ip_registered_event, mock_parse_tx_license_terms_attached_event, - mock_get_ip_id, - mock_signature_related_methods, - mock_is_registered, + mock_transform_request_dependencies, ): with ( mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), - mock_get_ip_id(), - mock_signature_related_methods(), - mock_is_registered(is_registered=False), + mock_transform_request_dependencies(), patch.object( ip_asset.license_attachment_workflows_client, "build_registerIpAndAttachPILTerms_transaction", From e067c4a2618b01915118eab135c92ce7fd0d9c90 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 11:07:32 +0800 Subject: [PATCH 19/52] refactor: register_derivative_ip with transform_request --- .../resources/IPAsset.py | 56 ++++--------------- .../transform_registration_request.py | 22 +++----- 2 files changed, 20 insertions(+), 58 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index c4d8c6e4..3c90a71d 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -830,55 +830,23 @@ def register_derivative_ip( :return dict: Dictionary with the tx hash 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." - ) - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=deriv_data - ).get_validated_data() - calculated_deadline = self.sign_util.get_deadline(deadline=deadline) - sig_register_signature = self.sign_util.get_permission_signature( - ip_id=ip_id, - deadline=calculated_deadline, - state=Web3.to_bytes(0), - permissions=[ - { - "ipId": ip_id, - "signer": self.derivative_workflows_client.contract.address, - "to": self.core_metadata_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": get_function_signature( - self.core_metadata_module_client.contract.abi, - "setAll", - ), - }, - { - "ipId": ip_id, - "signer": self.derivative_workflows_client.contract.address, - "to": self.licensing_module_client.contract.address, - "permission": AccessPermission.ALLOW, - "func": get_function_signature( - self.licensing_module_client.contract.abi, - "registerDerivative", - ), - }, - ], + transformed_request = transform_request( + request=RegisterRegistrationRequest( + nft_contract=nft_contract, + token_id=token_id, + deriv_data=deriv_data, + ip_metadata=metadata, + deadline=deadline, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, ) response = build_and_send_transaction( self.web3, self.account, self.derivative_workflows_client.build_registerIpAndMakeDerivative_transaction, - nft_contract, - token_id, - validated_deriv_data, - IPMetadata.from_input(metadata).get_validated_data(), - { - "signer": self.account.address, - "deadline": calculated_deadline, - "signature": sig_register_signature["signature"], - }, + *transformed_request.validated_request, tx_options=tx_options, ) diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 514bb976..7f90a341 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -730,26 +730,20 @@ def _handle_register_with_derivative( ), ) abi_element_identifier = "registerIpAndMakeDerivative" - validated_request = { - "nft_contract": nft_contract, - "token_id": token_id, - "metadata": metadata, - "deriv_data": deriv_data, - "signature_data": { + validated_request = [ + nft_contract, + token_id, + deriv_data, + metadata, + { "signer": wallet_address, "deadline": calculated_deadline, "signature": signature_data["signature"], }, - } + ] encoded_data = derivative_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["nft_contract"], - validated_request["token_id"], - validated_request["deriv_data"], - validated_request["metadata"], - validated_request["signature_data"], - ], + args=validated_request, ) return TransformedRegistrationRequest( From b8abc34072c355f6aefc2f127bb763526f6c9054 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 13:53:23 +0800 Subject: [PATCH 20/52] refactor:mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens with transfer_request --- .../resources/IPAsset.py | 29 +++++++++++------ .../transform_registration_request.py | 27 ++++++---------- tests/unit/resources/test_ip_asset.py | 32 +++++++++++++------ 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 3c90a71d..3a163089 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -60,6 +60,7 @@ ExtraData, LicenseTermsDataInput, LinkDerivativeResponse, + MintAndRegisterRequest, MintedNFT, MintNFT, RegisterAndAttachAndDistributeRoyaltyTokensResponse, @@ -1089,21 +1090,29 @@ def mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( :return `RegistrationWithRoyaltyVaultAndLicenseTermsResponse`: Response with tx hash, IP ID, token ID, license terms IDs, and royalty vault address. """ try: - validated_royalty_shares = get_royalty_shares(royalty_shares)[ - "royalty_shares" - ] - license_terms = validate_license_terms_data(license_terms_data, self.web3) + if not license_terms_data: + raise ValueError("License terms data must be provided.") + if not royalty_shares: + raise ValueError("Royalty shares must be provided.") + transformed_request = transform_request( + request=MintAndRegisterRequest( + spg_nft_contract=spg_nft_contract, + license_terms_data=license_terms_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + recipient=recipient, + allow_duplicates=allow_duplicates, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, + ) response = build_and_send_transaction( self.web3, self.account, self.royalty_token_distribution_workflows_client.build_mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_transaction, - validate_address(spg_nft_contract), - self._validate_recipient(recipient), - IPMetadata.from_input(ip_metadata).get_validated_data(), - license_terms, - validated_royalty_shares, - allow_duplicates, + *transformed_request.validated_request, tx_options=tx_options, ) diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 7f90a341..0ebcba2f 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -222,26 +222,17 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( abi_element_identifier = ( "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" ) - validated_request = { - "spg_nft_contract": spg_nft_contract, - "recipient": recipient, - "metadata": metadata, - "license_terms_data": license_terms_data, - "royalty_shares": royalty_shares, - "allow_duplicates": get_allow_duplicates( - allow_duplicates, abi_element_identifier - ), - } + validated_request = [ + spg_nft_contract, + recipient, + metadata, + license_terms_data, + royalty_shares, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["spg_nft_contract"], - validated_request["recipient"], - validated_request["metadata"], - validated_request["license_terms_data"], - validated_request["royalty_shares"], - validated_request["allow_duplicates"], - ], + args=validated_request, ) return TransformedRegistrationRequest( diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index fdac1924..31860892 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -367,8 +367,8 @@ def test_ip_is_already_registered( }, ) - def test_parent_ip_id_is_empty(self, ip_asset, mock_get_ip_id, mock_is_registered): - with mock_get_ip_id(), mock_is_registered(): + def test_parent_ip_id_is_empty(self, ip_asset, mock_transform_request_dependencies): + with mock_transform_request_dependencies(): with pytest.raises(ValueError, match="The parent IP IDs must be provided."): ip_asset.register_derivative_ip( nft_contract=ADDRESS, @@ -382,16 +382,14 @@ def test_parent_ip_id_is_empty(self, ip_asset, mock_get_ip_id, mock_is_registere def test_success( self, ip_asset, - mock_get_ip_id, - mock_is_registered, + mock_transform_request_dependencies, mock_parse_ip_registered_event, mock_signature_related_methods, mock_get_function_signature, mock_license_registry_client, ): with ( - mock_get_ip_id(), - mock_is_registered(), + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_get_function_signature(), mock_license_registry_client(), @@ -1449,9 +1447,24 @@ def test_throw_error_when_royalty_shares_empty(self, ip_asset: IPAsset): royalty_shares=[], ) + def test_throw_error_when_license_terms_data_is_empty(self, ip_asset: IPAsset): + + with pytest.raises( + ValueError, + match="Failed to mint, register IP, attach PIL terms and distribute royalty tokens: License terms data must be provided.", + ): + ip_asset.mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( + spg_nft_contract=ADDRESS, + license_terms_data=[], + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0) + ], + ) + def test_success_with_default_values( self, ip_asset: IPAsset, + mock_transform_request_dependencies, mock_license_registry_client, mock_parse_ip_registered_event, mock_parse_tx_license_terms_attached_event, @@ -1463,6 +1476,7 @@ def test_success_with_default_values( ] with ( + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_parse_tx_license_terms_attached_event(), mock_license_registry_client(), @@ -3170,16 +3184,14 @@ def test_throw_error_when_license_token_ids_and_deriv_data_are_not_provided_for_ def test_success_when_deriv_data_only_are_provided_for_minted_nft( self, ip_asset: IPAsset, + mock_transform_request_dependencies, 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_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_license_registry_client(), mock_signature_related_methods(), From 6325581393899c4a8089236dec70bbe9f7c0e024 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 14:16:34 +0800 Subject: [PATCH 21/52] refactor: mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens with transform_request --- .../resources/IPAsset.py | 27 ++++++++----- .../transform_registration_request.py | 27 +++++-------- tests/unit/resources/test_ip_asset.py | 40 +++++++++++++------ 3 files changed, 52 insertions(+), 42 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 3a163089..8d31f527 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -98,7 +98,6 @@ from story_protocol_python_sdk.utils.registration.transform_registration_request import ( transform_request, ) -from story_protocol_python_sdk.utils.royalty import get_royalty_shares from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction from story_protocol_python_sdk.utils.validation import ( @@ -1163,21 +1162,27 @@ def mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( :return `RegistrationWithRoyaltyVaultResponse`: Dictionary with the tx hash, IP ID and token ID, royalty vault. """ try: - validated_royalty_shares_obj = get_royalty_shares(royalty_shares) - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=deriv_data - ).get_validated_data() + if not royalty_shares: + raise ValueError("Royalty shares must be provided.") + transformed_request = transform_request( + request=MintAndRegisterRequest( + spg_nft_contract=spg_nft_contract, + deriv_data=deriv_data, + royalty_shares=royalty_shares, + ip_metadata=ip_metadata, + recipient=recipient, + allow_duplicates=allow_duplicates, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, + ) response = build_and_send_transaction( self.web3, self.account, self.royalty_token_distribution_workflows_client.build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transaction, - validate_address(spg_nft_contract), - self._validate_recipient(recipient), - IPMetadata.from_input(ip_metadata).get_validated_data(), - validated_deriv_data, - validated_royalty_shares_obj["royalty_shares"], - allow_duplicates, + *transformed_request.validated_request, tx_options=tx_options, ) diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 0ebcba2f..8d27477d 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -264,26 +264,17 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( abi_element_identifier = ( "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" ) - validated_request = { - "spg_nft_contract": spg_nft_contract, - "recipient": recipient, - "metadata": metadata, - "deriv_data": deriv_data, - "royalty_shares": royalty_shares, - "allow_duplicates": get_allow_duplicates( - allow_duplicates, abi_element_identifier - ), - } + validated_request = [ + spg_nft_contract, + recipient, + metadata, + deriv_data, + royalty_shares, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["spg_nft_contract"], - validated_request["recipient"], - validated_request["metadata"], - validated_request["deriv_data"], - validated_request["royalty_shares"], - validated_request["allow_duplicates"], - ], + args=validated_request, ) return TransformedRegistrationRequest( diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 31860892..d0250c15 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -1247,22 +1247,26 @@ def test_throw_error_when_royalty_shares_empty(self, ip_asset: IPAsset): royalty_shares=[], ) - def test_throw_error_when_deriv_data_is_invalid(self, ip_asset: IPAsset): - with pytest.raises(ValueError, match="The parent IP IDs must be provided."): - ip_asset.mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( - spg_nft_contract=ADDRESS, - deriv_data=DerivativeDataInput( - parent_ip_ids=[], - license_terms_ids=[1], - ), - royalty_shares=[ - RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0) - ], - ) + def test_throw_error_when_deriv_data_is_invalid( + self, ip_asset: IPAsset, mock_transform_request_dependencies + ): + with mock_transform_request_dependencies(): + with pytest.raises(ValueError, match="The parent IP IDs must be provided."): + ip_asset.mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( + spg_nft_contract=ADDRESS, + deriv_data=DerivativeDataInput( + parent_ip_ids=[], + license_terms_ids=[1], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0) + ], + ) def test_success_with_default_values( self, ip_asset: IPAsset, + mock_transform_request_dependencies, mock_license_registry_client, mock_parse_ip_registered_event, mock_get_royalty_vault_address_by_ip_id, @@ -1273,6 +1277,7 @@ def test_success_with_default_values( ] with ( + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_license_registry_client(), mock_get_royalty_vault_address_by_ip_id(), @@ -1305,6 +1310,7 @@ def test_success_with_default_values( def test_royalty_vault_address( self, ip_asset: IPAsset, + mock_transform_request_dependencies, mock_license_registry_client, mock_parse_ip_registered_event, ): @@ -1314,6 +1320,7 @@ def test_royalty_vault_address( ] with ( + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_license_registry_client(), ): @@ -1354,6 +1361,7 @@ def test_royalty_vault_address( def test_success_with_custom_values( self, ip_asset: IPAsset, + mock_transform_request_dependencies, mock_license_registry_client, mock_parse_ip_registered_event, mock_get_royalty_vault_address_by_ip_id, @@ -1368,6 +1376,7 @@ def test_success_with_custom_values( nft_metadata_hash="0xabcdef1234567890", ) with ( + mock_transform_request_dependencies(), mock_parse_ip_registered_event(), mock_license_registry_client(), mock_get_royalty_vault_address_by_ip_id(), @@ -1411,10 +1420,15 @@ def test_success_with_custom_values( def test_throw_error_when_transaction_failed( self, ip_asset: IPAsset, + mock_transform_request_dependencies, mock_license_registry_client, mock_parse_ip_registered_event, ): - with mock_parse_ip_registered_event(), mock_license_registry_client(): + with ( + mock_transform_request_dependencies(), + mock_parse_ip_registered_event(), + mock_license_registry_client(), + ): with patch.object( ip_asset.royalty_token_distribution_workflows_client, "build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transaction", From 95a0d8c4c23f3fddb467c98fe3b1ac992c42f614 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 14:20:17 +0800 Subject: [PATCH 22/52] refactor: enhance mint_and_register_ip_and_attach_pil_terms by integrating transform_request for improved request handling --- .../resources/IPAsset.py | 57 +++++++++---------- .../transform_registration_request.py | 24 +++----- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 8d31f527..9d99b6b4 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -533,44 +533,39 @@ def mint_and_register_ip_asset_with_pil_terms( :return dict: Dictionary with tx hash, IP ID, token ID, and license term IDs. """ try: - if not self.web3.is_address(spg_nft_contract): - raise ValueError( - f"The NFT contract address {spg_nft_contract} is not valid." - ) - license_terms = validate_license_terms_data(terms, self.web3) - metadata = { - "ipMetadataURI": "", - "ipMetadataHash": ZERO_HASH, - "nftMetadataURI": "", - "nftMetadataHash": ZERO_HASH, - } - - 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 - ), - } - ) + transformed_request = transform_request( + request=MintAndRegisterRequest( + spg_nft_contract=spg_nft_contract, + recipient=recipient, + ip_metadata=( + IPMetadataInput( + ip_metadata_uri=ip_metadata.get("ip_metadata_uri", ""), + ip_metadata_hash=ip_metadata.get( + "ip_metadata_hash", ZERO_HASH + ), + nft_metadata_uri=ip_metadata.get("nft_metadata_uri", ""), + nft_metadata_hash=ip_metadata.get( + "nft_metadata_hash", ZERO_HASH + ), + ) + if ip_metadata + else None + ), + license_terms_data=terms, + allow_duplicates=allow_duplicates, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, + ) response = build_and_send_transaction( self.web3, self.account, self.license_attachment_workflows_client.build_mintAndRegisterIpAndAttachPILTerms_transaction, - spg_nft_contract, - self._validate_recipient(recipient), - metadata, - license_terms, - allow_duplicates, + *transformed_request.validated_request, tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ 0 ] diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 8d27477d..ed0b2d02 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -301,24 +301,16 @@ def _handle_mint_and_register_with_license_terms( license_attachment_workflows_client.contract.address ) abi_element_identifier = "mintAndRegisterIpAndAttachPILTerms" - validated_request = { - "spg_nft_contract": spg_nft_contract, - "recipient": recipient, - "metadata": metadata, - "license_terms_data": license_terms_data, - "allow_duplicates": get_allow_duplicates( - allow_duplicates, abi_element_identifier - ), - } + validated_request = [ + spg_nft_contract, + recipient, + metadata, + license_terms_data, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ] encoded_data = license_attachment_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["spg_nft_contract"], - validated_request["recipient"], - validated_request["metadata"], - validated_request["license_terms_data"], - validated_request["allow_duplicates"], - ], + args=validated_request, ) return TransformedRegistrationRequest( From 32f729c1a4a6996878a1f3231ff366db27528354 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 14:28:06 +0800 Subject: [PATCH 23/52] refactor: update IPAsset and transform_registration_request to utilize transformed_request for improved request handling --- .../resources/IPAsset.py | 30 ++++++++++++++----- .../transform_registration_request.py | 24 +++++---------- tests/unit/resources/test_ip_asset.py | 22 +++++++++----- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 9d99b6b4..4801c0be 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -881,18 +881,32 @@ def mint_and_register_ip_and_make_derivative( """ try: - validated_deriv_data = DerivativeData.from_input( - web3=self.web3, input_data=deriv_data - ).get_validated_data() + transformed_request = transform_request( + request=MintAndRegisterRequest( + spg_nft_contract=spg_nft_contract, + recipient=recipient, + ip_metadata=( + IPMetadataInput( + ip_metadata_uri=ip_metadata.ip_metadata_uri, + ip_metadata_hash=ip_metadata.ip_metadata_hash, + nft_metadata_uri=ip_metadata.nft_metadata_uri, + nft_metadata_hash=ip_metadata.nft_metadata_hash, + ) + if ip_metadata + else None + ), + deriv_data=deriv_data, + allow_duplicates=allow_duplicates, + ), + web3=self.web3, + account=self.account, + chain_id=self.chain_id, + ) response = build_and_send_transaction( self.web3, self.account, self.derivative_workflows_client.build_mintAndRegisterIpAndMakeDerivative_transaction, - validate_address(spg_nft_contract), - validated_deriv_data, - IPMetadata.from_input(ip_metadata).get_validated_data(), - self._validate_recipient(recipient), - allow_duplicates, + *transformed_request.validated_request, tx_options=tx_options, ) ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index ed0b2d02..cd269061 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -335,24 +335,16 @@ def _handle_mint_and_register_with_derivative( derivative_workflows_client = DerivativeWorkflowsClient(web3) derivative_workflows_address = derivative_workflows_client.contract.address abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" - validated_request = { - "spg_nft_contract": spg_nft_contract, - "recipient": recipient, - "metadata": metadata, - "deriv_data": deriv_data, - "allow_duplicates": get_allow_duplicates( - allow_duplicates, abi_element_identifier - ), - } + validated_request = [ + spg_nft_contract, + deriv_data, + metadata, + recipient, + get_allow_duplicates(allow_duplicates, abi_element_identifier), + ] encoded_data = derivative_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, - args=[ - validated_request["spg_nft_contract"], - validated_request["deriv_data"], - validated_request["metadata"], - validated_request["recipient"], - validated_request["allow_duplicates"], - ], + args=validated_request, ) return TransformedRegistrationRequest( diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index d0250c15..25c8c84e 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -700,8 +700,13 @@ def test_success_and_expect_value_when_default_values_not_provided( ip_asset: IPAsset, mock_license_registry_client, mock_parse_ip_registered_event, + mock_transform_request_dependencies, ): - with mock_parse_ip_registered_event(), mock_license_registry_client(): + with ( + mock_transform_request_dependencies(), + mock_parse_ip_registered_event(), + mock_license_registry_client(), + ): with patch.object( ip_asset.derivative_workflows_client, "build_mintAndRegisterIpAndMakeDerivative_transaction", @@ -741,10 +746,15 @@ def test_success_and_expect_value_when_default_values_not_provided( def test_with_custom_value( self, ip_asset: IPAsset, + mock_transform_request_dependencies, mock_license_registry_client, mock_parse_ip_registered_event, ): - with mock_parse_ip_registered_event(), mock_license_registry_client(): + with ( + mock_transform_request_dependencies(), + mock_parse_ip_registered_event(), + mock_license_registry_client(), + ): with patch.object( ip_asset.derivative_workflows_client, "build_mintAndRegisterIpAndMakeDerivative_transaction", @@ -761,10 +771,8 @@ def test_with_custom_value( license_template=ADDRESS, ), ip_metadata=IPMetadataInput( - ip_metadata_uri="https://example.com/metadata/custom-value.json", ip_metadata_hash=HexStr("ip_metadata_hash"), nft_metadata_uri="https://example.com/metadata/custom-value.json", - nft_metadata_hash=HexStr("nft_metadata_hash"), ), recipient=ADDRESS, allow_duplicates=False, @@ -783,10 +791,10 @@ def test_with_custom_value( "licenseTemplate": ADDRESS, } assert mock_build_transaction.call_args[0][2] == { - "ipMetadataURI": "https://example.com/metadata/custom-value.json", - "ipMetadataHash": "ip_metadata_hash", + "ipMetadataURI": "", + "ipMetadataHash": HexStr("ip_metadata_hash"), "nftMetadataURI": "https://example.com/metadata/custom-value.json", - "nftMetadataHash": "nft_metadata_hash", + "nftMetadataHash": ZERO_HASH, } assert mock_build_transaction.call_args[0][3] == ADDRESS # recipient assert not mock_build_transaction.call_args[0][4] # allowDuplicates From 825739a922d0ec5ecdfaacedc45cfc624e1218e0 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 16 Jan 2026 14:31:00 +0800 Subject: [PATCH 24/52] refactor: simplify IPAsset's minting request by directly assigning ip_metadata for cleaner code --- src/story_protocol_python_sdk/resources/IPAsset.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 4801c0be..2457f036 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -885,16 +885,7 @@ def mint_and_register_ip_and_make_derivative( request=MintAndRegisterRequest( spg_nft_contract=spg_nft_contract, recipient=recipient, - ip_metadata=( - IPMetadataInput( - ip_metadata_uri=ip_metadata.ip_metadata_uri, - ip_metadata_hash=ip_metadata.ip_metadata_hash, - nft_metadata_uri=ip_metadata.nft_metadata_uri, - nft_metadata_hash=ip_metadata.nft_metadata_hash, - ) - if ip_metadata - else None - ), + ip_metadata=ip_metadata, deriv_data=deriv_data, allow_duplicates=allow_duplicates, ), From db6657417379470b63c047387d83f0150a828afd Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 19 Jan 2026 14:01:32 +0800 Subject: [PATCH 25/52] refactor: remove unused _validate_derivative_data method from IPAsset for cleaner code --- .../resources/IPAsset.py | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 2457f036..cecea1a4 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1853,75 +1853,6 @@ def _handle_mint_nft_derivative_registration( token_id=token_result["token_id"], ) - 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.") - - 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. From 5b997a4e8e9a45274c9ffc5b1f67d0a5a6fac86e Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 19 Jan 2026 14:03:14 +0800 Subject: [PATCH 26/52] refactor: use get_permission_signature instead of hardcode --- .../transform_registration_request.py | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index cd269061..d6f9c96f 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -33,6 +33,7 @@ from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.constants import ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeData +from story_protocol_python_sdk.utils.function_signature import get_function_signature from story_protocol_python_sdk.utils.ip_metadata import IPMetadata from story_protocol_python_sdk.utils.registration.registration_utils import ( get_public_minting, @@ -513,8 +514,8 @@ def _handle_register_with_license_terms_and_royalty_vault( permissions=_get_license_terms_permissions( ip_id=ip_id, signer_address=royalty_token_distribution_workflows_address, - core_metadata_address=core_metadata_module_client.contract.address, - licensing_module_address=licensing_module_client.contract.address, + core_metadata_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, ), ) abi_element_identifier = "registerIpAndAttachPILTermsAndDeployRoyaltyVault" @@ -577,8 +578,8 @@ def _handle_register_with_derivative_and_royalty_vault( permissions=_get_derivative_permissions( ip_id=ip_id, signer_address=royalty_token_distribution_workflows_address, - core_metadata_address=core_metadata_module_client.contract.address, - licensing_module_address=licensing_module_client.contract.address, + core_metadata_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, ), ) abi_element_identifier = "registerIpAndMakeDerivativeAndDeployRoyaltyVault" @@ -637,8 +638,8 @@ def _handle_register_with_license_terms( permissions=_get_license_terms_permissions( ip_id=ip_id, signer_address=license_attachment_workflows_address, - core_metadata_address=core_metadata_module_client.contract.address, - licensing_module_address=licensing_module_client.contract.address, + core_metadata_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, ), ) abi_element_identifier = "registerIpAndAttachPILTerms" @@ -691,8 +692,8 @@ def _handle_register_with_derivative( permissions=_get_derivative_permissions( ip_id=ip_id, signer_address=derivative_workflows_address, - core_metadata_address=core_metadata_module_client.contract.address, - licensing_module_address=licensing_module_client.contract.address, + core_metadata_client=core_metadata_module_client, + licensing_module_client=licensing_module_client, ), ) abi_element_identifier = "registerIpAndMakeDerivative" @@ -729,31 +730,35 @@ def _handle_register_with_derivative( def _get_license_terms_permissions( ip_id: Address, signer_address: Address, - core_metadata_address: Address, - licensing_module_address: Address, + core_metadata_client: CoreMetadataModuleClient, + licensing_module_client: LicensingModuleClient, ) -> list[dict]: """Get permissions for license terms operations.""" return [ { "ipId": ip_id, "signer": signer_address, - "to": core_metadata_address, + "to": core_metadata_client.contract.address, "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", + "func": get_function_signature(core_metadata_client.contract.abi, "setAll"), }, { "ipId": ip_id, "signer": signer_address, - "to": licensing_module_address, + "to": licensing_module_client.contract.address, "permission": AccessPermission.ALLOW, - "func": "attachLicenseTerms(address,address,uint256)", + "func": get_function_signature( + licensing_module_client.contract.abi, "attachLicenseTerms" + ), }, { "ipId": ip_id, "signer": signer_address, - "to": licensing_module_address, + "to": licensing_module_client.contract.address, "permission": AccessPermission.ALLOW, - "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + "func": get_function_signature( + licensing_module_client.contract.abi, "setLicensingConfig" + ), }, ] @@ -761,23 +766,25 @@ def _get_license_terms_permissions( def _get_derivative_permissions( ip_id: Address, signer_address: Address, - core_metadata_address: Address, - licensing_module_address: Address, + core_metadata_client: CoreMetadataModuleClient, + licensing_module_client: LicensingModuleClient, ) -> list[dict]: """Get permissions for derivative operations.""" return [ { "ipId": ip_id, "signer": signer_address, - "to": core_metadata_address, + "to": core_metadata_client.contract.address, "permission": AccessPermission.ALLOW, - "func": "setAll(address,string,bytes32,bytes32)", + "func": get_function_signature(core_metadata_client.contract.abi, "setAll"), }, { "ipId": ip_id, "signer": signer_address, - "to": licensing_module_address, + "to": licensing_module_client.contract.address, "permission": AccessPermission.ALLOW, - "func": "registerDerivative(address,address[],uint256[],address,bytes,uint256,uint32,address)", + "func": get_function_signature( + licensing_module_client.contract.abi, "registerDerivative" + ), }, ] From b83756ac8cabc7d349aac50cad7af3cc8bb45497 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 19 Jan 2026 15:28:30 +0800 Subject: [PATCH 27/52] feat: return contract_call in the transfer request --- .../types/resource/IPAsset.py | 8 +- .../transform_registration_request.py | 70 ++++++++++ tests/unit/resources/test_ip_asset.py | 4 + .../test_transform_registration_request.py | 132 ++++++++++-------- 4 files changed, 151 insertions(+), 63 deletions(-) diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 676f9598..bdbe8fe2 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Literal, TypedDict +from typing import Callable, Literal, TypedDict from ens.ens import Address, HexStr @@ -352,11 +352,15 @@ class ExtraData(TypedDict, total=False): deadline: [Optional] The deadline for the signature. max_license_tokens: [Optional] Maximum license tokens for each license term. license_terms_data: [Optional] The license terms data. + nft_contract: [Optional] The NFT contract address. + token_id: [Optional] The token ID. """ royalty_shares: list[RoyaltyShareInput] deadline: int royalty_total_amount: int + nft_contract: Address + token_id: int @dataclass @@ -369,10 +373,12 @@ class TransformedRegistrationRequest: is_use_multicall3: Whether to use multicall3 or SPG's native multicall. workflow_address: The workflow contract address. extra_data: [Optional] Extra data for post-processing. + contract_call: [Optional] The contract call function. """ encoded_tx_data: bytes is_use_multicall3: bool workflow_address: Address validated_request: list + contract_call: Callable[[], HexStr] extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index d6f9c96f..3db0d470 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -236,12 +236,20 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( args=validated_request, ) + def contract_call() -> HexStr: + response = royalty_token_distribution_workflows_client.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, validated_request=validated_request, extra_data=None, + contract_call=contract_call, ) @@ -278,12 +286,20 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( args=validated_request, ) + def contract_call() -> HexStr: + response = royalty_token_distribution_workflows_client.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, validated_request=validated_request, extra_data=None, + contract_call=contract_call, ) @@ -314,12 +330,22 @@ def _handle_mint_and_register_with_license_terms( args=validated_request, ) + def contract_call() -> HexStr: + response = ( + license_attachment_workflows_client.mintAndRegisterIpAndAttachPILTerms( + *validated_request + ) + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=license_attachment_workflows_address, validated_request=validated_request, extra_data=None, + contract_call=contract_call, ) @@ -348,12 +374,20 @@ def _handle_mint_and_register_with_derivative( args=validated_request, ) + def contract_call() -> HexStr: + response = derivative_workflows_client.mintAndRegisterIpAndMakeDerivative( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=derivative_workflows_address, validated_request=validated_request, extra_data=None, + contract_call=contract_call, ) @@ -535,6 +569,13 @@ def _handle_register_with_license_terms_and_royalty_vault( args=validated_request, ) + def contract_call() -> HexStr: + response = royalty_token_distribution_workflows_client.registerIpAndAttachPILTermsAndDeployRoyaltyVault( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, @@ -544,7 +585,10 @@ def _handle_register_with_license_terms_and_royalty_vault( royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), deadline=calculated_deadline, royalty_total_amount=royalty_total_amount, + nft_contract=nft_contract, + token_id=token_id, ), + contract_call=contract_call, ) @@ -599,6 +643,13 @@ def _handle_register_with_derivative_and_royalty_vault( args=validated_request, ) + def contract_call() -> HexStr: + response = royalty_token_distribution_workflows_client.registerIpAndMakeDerivativeAndDeployRoyaltyVault( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, @@ -608,7 +659,10 @@ def _handle_register_with_derivative_and_royalty_vault( royalty_shares=cast(list[RoyaltyShareInput], royalty_shares), deadline=calculated_deadline, royalty_total_amount=royalty_total_amount, + nft_contract=nft_contract, + token_id=token_id, ), + contract_call=contract_call, ) @@ -659,12 +713,20 @@ def _handle_register_with_license_terms( args=validated_request, ) + def contract_call() -> HexStr: + response = license_attachment_workflows_client.registerIpAndAttachPILTerms( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=license_attachment_workflows_address, validated_request=validated_request, extra_data=None, + contract_call=contract_call, ) @@ -713,12 +775,20 @@ def _handle_register_with_derivative( args=validated_request, ) + def contract_call() -> HexStr: + response = derivative_workflows_client.registerIpAndMakeDerivative( + *validated_request + ) + web3.eth.wait_for_transaction_receipt(response["tx_hash"]) + return response["tx_hash"] + return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=derivative_workflows_address, validated_request=validated_request, extra_data=None, + contract_call=contract_call, ) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 25c8c84e..3a8ccecd 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -248,6 +248,10 @@ def _mock( "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", return_value=mock_module_registry_client, ), + patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.get_function_signature", + return_value="", + ), ] # Return context manager that applies all patches diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index 9f1800d2..aed1d8b0 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -127,25 +127,30 @@ def _mock(): derivative_workflows_client.contract.address = ( "derivative_workflows_client_address" ) - patches = [ - patch( + return { + "royalty_token_distribution_patch": patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyTokenDistributionWorkflowsClient", return_value=royalty_token_distribution_client, ), - patch( + "license_attachment_patch": patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.LicenseAttachmentWorkflowsClient", return_value=license_attachment_client, ), - patch( + "derivative_workflows_patch": patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.DerivativeWorkflowsClient", return_value=derivative_workflows_client, ), - ] - return { - "patches": patches, "royalty_token_distribution_client": royalty_token_distribution_client, "license_attachment_client": license_attachment_client, "derivative_workflows_client": derivative_workflows_client, + "get_function_signature": patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.get_function_signature", + return_value="", + ), + "royalty_token_distribution_workflows_client": patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyTokenDistributionWorkflowsClient", + return_value=royalty_token_distribution_client, + ), } return _mock @@ -200,17 +205,16 @@ def _mock(): mock_licensing_client = MagicMock() mock_licensing_client.contract = mock_licensing_contract - patches = [ - patch( + return { + "core_metadata_module_patch": patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.CoreMetadataModuleClient", return_value=mock_core_metadata_client, ), - patch( + "licensing_module_patch": patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.LicensingModuleClient", return_value=mock_licensing_client, ), - ] - return patches + } return _mock @@ -269,15 +273,14 @@ def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_pres license_terms_data=LICENSE_TERMS_DATA, ) workflow_mocks = mock_workflow_clients() - patches = workflow_mocks["patches"] license_attachment_client = workflow_mocks["license_attachment_client"] with ( mock_get_public_minting(), mock_royalty_module_client(), mock_module_registry_client, - patches[0], - patches[1], - patches[2], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) # Assert real encoding result (not mock value) @@ -298,6 +301,7 @@ def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_pres assert result.workflow_address == "license_attachment_client_address" assert result.is_use_multicall3 is True assert result.extra_data is None + assert result.contract_call is not None def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_id_present( self, @@ -315,7 +319,6 @@ def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_ license_terms_data=LICENSE_TERMS_DATA, ) workflow_mocks = mock_workflow_clients() - workflow_patches = workflow_mocks["patches"] module_patches = mock_module_clients() license_attachment_client = workflow_mocks["license_attachment_client"] with ( @@ -323,11 +326,12 @@ def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_ mock_sign_util(), mock_royalty_module_client(), mock_module_registry_client, - workflow_patches[0], - workflow_patches[1], - workflow_patches[2], - module_patches[0], - module_patches[1], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], + workflow_mocks["get_function_signature"], + module_patches["core_metadata_module_patch"], + module_patches["licensing_module_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) # Assert real encoding result (not mock value) @@ -349,6 +353,7 @@ def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_ assert args[4]["signature"] == b"signature" assert result.extra_data is None assert result.is_use_multicall3 is False + assert result.contract_call is not None def test_raises_error_for_invalid_request_type( self, mock_web3, mock_ip_asset_registry_client @@ -393,7 +398,6 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], ) workflow_mocks = mock_workflow_clients() - patches = workflow_mocks["patches"] royalty_token_distribution_client = workflow_mocks[ "royalty_token_distribution_client" ] @@ -401,9 +405,9 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens mock_get_public_minting(public_minting=True), mock_royalty_module_client(), mock_module_registry_client, - patches[0], - patches[1], - patches[2], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) @@ -431,6 +435,7 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens assert args[4][0]["recipient"] == ADDRESS assert args[4][0]["percentage"] == 50 * 10**6 assert args[5] is True # allow_duplicates (default for this method) + assert result.contract_call is not None def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( self, @@ -450,7 +455,6 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], ) workflow_mocks = mock_workflow_clients() - patches = workflow_mocks["patches"] royalty_token_distribution_client = workflow_mocks[ "royalty_token_distribution_client" ] @@ -459,9 +463,9 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( mock_pi_license_template_client(), mock_derivative_ip_asset_registry_client(), mock_license_registry_client(), - patches[0], - patches[1], - patches[2], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) @@ -486,6 +490,7 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( assert args[4][0]["recipient"] == ADDRESS # royalty_shares assert args[4][0]["percentage"] == 50 * 10**6 # royalty_shares assert args[5] is True # allow_duplicates (default for this method) + assert result.contract_call is not None def test_mint_and_register_ip_and_make_derivative( self, @@ -506,16 +511,15 @@ def test_mint_and_register_ip_and_make_derivative( allow_duplicates=False, ) workflow_mocks = mock_workflow_clients() - patches = workflow_mocks["patches"] derivative_workflows_client = workflow_mocks["derivative_workflows_client"] with ( mock_get_public_minting(public_minting=True), mock_pi_license_template_client(), mock_derivative_ip_asset_registry_client(), mock_license_registry_client(), - patches[0], - patches[1], - patches[2], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) # Assert real encoding result (not mock value) @@ -536,6 +540,7 @@ def test_mint_and_register_ip_and_make_derivative( assert ( call_args[1]["args"][4] is False ) # allow_duplicates (default for this method) + assert result.contract_call is not None def test_raises_error_for_invalid_mint_and_register_request_type( self, @@ -548,12 +553,11 @@ def test_raises_error_for_invalid_mint_and_register_request_type( ip_metadata=IP_METADATA, ) workflow_mocks = mock_workflow_clients() - patches = workflow_mocks["patches"] with ( mock_get_public_minting(), - patches[0], - patches[1], - patches[2], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], ): with pytest.raises( ValueError, match="Invalid mint and register request type" @@ -581,7 +585,6 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( deadline=2000, ) workflow_mocks = mock_workflow_clients() - workflow_patches = workflow_mocks["patches"] royalty_token_distribution_client = workflow_mocks[ "royalty_token_distribution_client" ] @@ -591,11 +594,12 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( mock_sign_util(deadline=2000), mock_royalty_module_client(), mock_module_registry_client, - workflow_patches[0], - workflow_patches[1], - workflow_patches[2], - module_patches[0], - module_patches[1], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], + workflow_mocks["get_function_signature"], + module_patches["core_metadata_module_patch"], + module_patches["licensing_module_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) @@ -629,6 +633,7 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( assert royalty_share_dict["recipient"] == ADDRESS assert royalty_share_dict["percentage"] == 50 * 10**6 assert result.extra_data["deadline"] == 2000 + assert result.contract_call is not None def test_register_ip_and_make_derivative_and_deploy_royalty_vault( self, @@ -650,7 +655,6 @@ def test_register_ip_and_make_derivative_and_deploy_royalty_vault( royalty_shares=[RoyaltyShareInput(recipient=ADDRESS, percentage=50.0)], ) workflow_mocks = mock_workflow_clients() - workflow_patches = workflow_mocks["patches"] royalty_token_distribution_client = workflow_mocks[ "royalty_token_distribution_client" ] @@ -661,11 +665,12 @@ def test_register_ip_and_make_derivative_and_deploy_royalty_vault( mock_pi_license_template_client(), mock_derivative_ip_asset_registry_client(), mock_license_registry_client(), - workflow_patches[0], - workflow_patches[1], - workflow_patches[2], - module_patches[0], - module_patches[1], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], + workflow_mocks["get_function_signature"], + module_patches["core_metadata_module_patch"], + module_patches["licensing_module_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) @@ -693,6 +698,7 @@ def test_register_ip_and_make_derivative_and_deploy_royalty_vault( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" + assert result.contract_call is not None def test_register_ip_and_attach_pil_terms( self, @@ -711,7 +717,6 @@ def test_register_ip_and_attach_pil_terms( license_terms_data=LICENSE_TERMS_DATA, ) workflow_mocks = mock_workflow_clients() - workflow_patches = workflow_mocks["patches"] license_attachment_client = workflow_mocks["license_attachment_client"] module_patches = mock_module_clients() with ( @@ -719,11 +724,12 @@ def test_register_ip_and_attach_pil_terms( mock_sign_util(), mock_royalty_module_client(), mock_module_registry_client, - workflow_patches[0], - workflow_patches[1], - workflow_patches[2], - module_patches[0], - module_patches[1], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], + workflow_mocks["get_function_signature"], + module_patches["core_metadata_module_patch"], + module_patches["licensing_module_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) @@ -747,6 +753,7 @@ def test_register_ip_and_attach_pil_terms( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" + assert result.contract_call is not None def test_register_ip_and_make_derivative( self, @@ -768,7 +775,6 @@ def test_register_ip_and_make_derivative( ), ) workflow_mocks = mock_workflow_clients() - workflow_patches = workflow_mocks["patches"] derivative_workflows_client = workflow_mocks["derivative_workflows_client"] module_patches = mock_module_clients() with ( @@ -777,11 +783,12 @@ def test_register_ip_and_make_derivative( mock_pi_license_template_client(), mock_derivative_ip_asset_registry_client(), mock_license_registry_client(), - workflow_patches[0], - workflow_patches[1], - workflow_patches[2], - module_patches[0], - module_patches[1], + workflow_mocks["royalty_token_distribution_patch"], + workflow_mocks["license_attachment_patch"], + workflow_mocks["derivative_workflows_patch"], + workflow_mocks["get_function_signature"], + module_patches["core_metadata_module_patch"], + module_patches["licensing_module_patch"], ): result = transform_request(request, mock_web3, account, CHAIN_ID) @@ -804,6 +811,7 @@ def test_register_ip_and_make_derivative( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" + assert result.contract_call is not None def test_raises_error_when_ip_not_registered( self, From e71e3bdca24f771818629a24fa7cf6ddb16fdb81 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 19 Jan 2026 16:28:10 +0800 Subject: [PATCH 28/52] feat: add aggregate_multicall_requests function to group registration requests by target address --- .../utils/registration/registration_utils.py | 67 ++++- tests/unit/utils/test_registration_utils.py | 281 ++++++++++++++++++ 2 files changed, 346 insertions(+), 2 deletions(-) diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 31f547be..f334dec3 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -1,8 +1,10 @@ """Registration utilities for IP asset operations.""" +from collections.abc import Callable from dataclasses import asdict, is_dataclass, replace +from typing import TypedDict -from ens.ens import Address +from ens.ens import Address, HexStr from web3 import Web3 from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ( @@ -12,7 +14,10 @@ RoyaltyModuleClient, ) from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient -from story_protocol_python_sdk.types.resource.IPAsset import LicenseTermsDataInput +from story_protocol_python_sdk.types.resource.IPAsset import ( + LicenseTermsDataInput, + TransformedRegistrationRequest, +) from story_protocol_python_sdk.types.resource.License import LicenseTermsInput from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData @@ -24,6 +29,13 @@ ) +class AggregatedRequestData(TypedDict): + """Aggregated request data structure.""" + + encoded_tx_data: list[bytes] + contract_calls: list[Callable[[], HexStr]] + + def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: """ Check if SPG NFT contract has public minting enabled. @@ -97,3 +109,54 @@ def validate_license_terms_data( } ) return validated_license_terms_data + + +def aggregate_multicall_requests( + requests: list[TransformedRegistrationRequest], + is_use_multicall3: bool, + multicall_address: Address, +) -> dict[Address, AggregatedRequestData]: + """ + Aggregate multicall requests by grouping them by target address. + + Groups requests that should be sent to the same multicall address together, + collecting their encoded transaction data and contract call functions. + + Args: + requests: List of transformed registration requests to aggregate. + is_use_multicall3: Whether to use multicall3 for aggregation. + multicall_address: The multicall3 contract address to use when applicable. + + Returns: + Dictionary mapping target addresses to aggregated request data: + - Key: Address (multicall address or workflow address) + - Value: AggregatedRequestData with: + - "encoded_tx_data": List of encoded transaction data (bytes) + - "contract_calls": List of contract call functions + """ + aggregated_requests: dict[Address, AggregatedRequestData] = {} + + for request in requests: + # Determine the target address for this request + target_address = ( + multicall_address + if request.is_use_multicall3 and is_use_multicall3 + else request.workflow_address + ) + + # Initialize entry if it doesn't exist + if target_address not in aggregated_requests: + aggregated_requests[target_address] = { + "encoded_tx_data": [request.encoded_tx_data], + "contract_calls": [request.contract_call], + } + else: + # Append to existing entry + aggregated_requests[target_address]["encoded_tx_data"].append( + request.encoded_tx_data + ) + aggregated_requests[target_address]["contract_calls"].append( + request.contract_call + ) + + return aggregated_requests diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index 5cc87db0..3d64a517 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -2,8 +2,13 @@ from unittest.mock import MagicMock, patch import pytest +from ens.ens import HexStr +from story_protocol_python_sdk.types.resource.IPAsset import ( + TransformedRegistrationRequest, +) from story_protocol_python_sdk.utils.registration.registration_utils import ( + aggregate_multicall_requests, get_public_minting, validate_license_terms_data, ) @@ -166,3 +171,279 @@ def test_validates_multiple_license_terms( }, "licensingConfig": LICENSE_TERMS_DATA_CAMEL_CASE["licensingConfig"], } + + +class TestAggregateMulticallRequests: + def test_aggregates_single_request(self): + """Test aggregating a single request.""" + multicall3_address = "multicall3" + encoded_data = b"encoded_data_1" + contract_call = MagicMock(return_value=HexStr("0x123")) + + request = TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + contract_call=contract_call, + ) + + result = aggregate_multicall_requests( + requests=[request], + is_use_multicall3=False, + multicall_address=multicall3_address, + ) + + assert len(result) == 1 + assert ADDRESS in result + assert result[ADDRESS]["encoded_tx_data"] == [encoded_data] + assert result[ADDRESS]["contract_calls"] == [contract_call] + + def test_aggregates_multiple_requests_same_address(self): + """Test aggregating multiple requests to the same address.""" + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + contract_call_1 = MagicMock(return_value=HexStr("0x111")) + contract_call_2 = MagicMock(return_value=HexStr("0x222")) + + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + contract_call=contract_call_1, + ) + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + contract_call=contract_call_2, + ) + + multicall3_address = "multicall3" + result = aggregate_multicall_requests( + requests=[request_1, request_2], + is_use_multicall3=False, + multicall_address=multicall3_address, + ) + + assert len(result) == 1 + assert ADDRESS in result + assert result[ADDRESS]["encoded_tx_data"] == [encoded_data_1, encoded_data_2] + assert result[ADDRESS]["contract_calls"] == [contract_call_1, contract_call_2] + + def test_aggregates_multiple_requests_different_addresses(self): + """Test aggregating multiple requests to different addresses.""" + workflow_address_1 = ADDRESS + workflow_address_2 = "0x" + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + contract_call_1 = MagicMock(return_value=HexStr("0x111")) + contract_call_2 = MagicMock(return_value=HexStr("0x222")) + + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=False, + workflow_address=workflow_address_1, + validated_request=[], + contract_call=contract_call_1, + ) + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=False, + workflow_address=workflow_address_2, + validated_request=[], + contract_call=contract_call_2, + ) + + result = aggregate_multicall_requests( + requests=[request_1, request_2], + is_use_multicall3=False, + multicall_address="0xmulticall", + ) + + assert len(result) == 2 + assert workflow_address_1 in result + assert workflow_address_2 in result + assert result[workflow_address_1]["encoded_tx_data"] == [encoded_data_1] + assert result[workflow_address_1]["contract_calls"] == [contract_call_1] + assert result[workflow_address_2]["encoded_tx_data"] == [encoded_data_2] + assert result[workflow_address_2]["contract_calls"] == [contract_call_2] + + def test_uses_multicall3_address_when_enabled(self): + """Test using multicall3 address when is_use_multicall3 is True.""" + multicall3_address = "multicall3" + encoded_data = b"encoded_data" + contract_call = MagicMock(return_value=HexStr("0x123")) + + request = TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + contract_call=contract_call, + ) + + result = aggregate_multicall_requests( + requests=[request], + is_use_multicall3=True, + multicall_address=multicall3_address, + ) + + assert len(result) == 1 + assert multicall3_address in result + assert ADDRESS not in result + assert result[multicall3_address]["encoded_tx_data"] == [encoded_data] + assert result[multicall3_address]["contract_calls"] == [contract_call] + + def test_uses_workflow_address_when_multicall3_disabled(self): + """Test using workflow address when is_use_multicall3 is False.""" + multicall3_address = "multicall3" + encoded_data = b"encoded_data" + contract_call = MagicMock(return_value=HexStr("0x123")) + + request = TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + contract_call=contract_call, + ) + + result = aggregate_multicall_requests( + requests=[request], + is_use_multicall3=False, + multicall_address=multicall3_address, + ) + + assert len(result) == 1 + assert ADDRESS in result + assert multicall3_address not in result + assert result[ADDRESS]["encoded_tx_data"] == [encoded_data] + assert result[ADDRESS]["contract_calls"] == [contract_call] + + def test_aggregates_mixed_requests_with_multicall3(self): + """Test aggregating mixed requests where some use multicall3 and some don't.""" + multicall3_address = "multicall3" + workflow_address_1 = ADDRESS + workflow_address_2 = "0x" + + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + encoded_data_3 = b"encoded_data_3" + contract_call_1 = MagicMock(return_value=HexStr("0x111")) + contract_call_2 = MagicMock(return_value=HexStr("0x222")) + contract_call_3 = MagicMock(return_value=HexStr("0x333")) + + # Request 1: uses multicall3 + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=True, + workflow_address=workflow_address_1, + validated_request=[], + contract_call=contract_call_1, + ) + # Request 2: uses multicall3 + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=True, + workflow_address=workflow_address_2, + validated_request=[], + contract_call=contract_call_2, + ) + # Request 3: doesn't use multicall3 + request_3 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_3, + is_use_multicall3=False, + workflow_address=workflow_address_1, + validated_request=[], + contract_call=contract_call_3, + ) + + result = aggregate_multicall_requests( + requests=[request_1, request_2, request_3], + is_use_multicall3=True, + multicall_address=multicall3_address, + ) + + # Request 1 and 2 should be aggregated to multicall3_address + # Request 3 should use its workflow_address + assert len(result) == 2 + assert multicall3_address in result + assert workflow_address_1 in result + + # Check multicall3 aggregation (request_1 and request_2) + assert len(result[multicall3_address]["encoded_tx_data"]) == 2 + assert encoded_data_1 in result[multicall3_address]["encoded_tx_data"] + assert encoded_data_2 in result[multicall3_address]["encoded_tx_data"] + assert contract_call_1 in result[multicall3_address]["contract_calls"] + assert contract_call_2 in result[multicall3_address]["contract_calls"] + + # Check workflow address (request_3) + assert result[workflow_address_1]["encoded_tx_data"] == [encoded_data_3] + assert result[workflow_address_1]["contract_calls"] == [contract_call_3] + + def test_aggregates_empty_requests_list(self): + """Test aggregating an empty list of requests.""" + multicall3_address = "multicall3" + result = aggregate_multicall_requests( + requests=[], + is_use_multicall3=False, + multicall_address=multicall3_address, + ) + + assert len(result) == 0 + assert isinstance(result, dict) + + def test_aggregates_multiple_requests_to_multicall3(self): + """Test aggregating multiple requests that all use multicall3.""" + multicall3_address = "0xmulticall3" + workflow_address_1 = "0x1" + workflow_address_2 = "0x2" + workflow_address_3 = "0x33333333" + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + encoded_data_3 = b"encoded_data_3" + contract_call_1 = MagicMock(return_value=HexStr("0x111")) + contract_call_2 = MagicMock(return_value=HexStr("0x222")) + contract_call_3 = MagicMock(return_value=HexStr("0x333")) + + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=True, + workflow_address=workflow_address_1, + validated_request=[], + contract_call=contract_call_1, + ) + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=True, + workflow_address=workflow_address_2, + validated_request=[], + contract_call=contract_call_2, + ) + request_3 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_3, + is_use_multicall3=True, + workflow_address=workflow_address_3, + validated_request=[], + contract_call=contract_call_3, + ) + + result = aggregate_multicall_requests( + requests=[request_1, request_2, request_3], + is_use_multicall3=True, + multicall_address=multicall3_address, + ) + + assert len(result) == 1 + assert multicall3_address in result + assert len(result[multicall3_address]["encoded_tx_data"]) == 3 + assert len(result[multicall3_address]["contract_calls"]) == 3 + assert encoded_data_1 in result[multicall3_address]["encoded_tx_data"] + assert encoded_data_2 in result[multicall3_address]["encoded_tx_data"] + assert encoded_data_3 in result[multicall3_address]["encoded_tx_data"] + assert contract_call_1 in result[multicall3_address]["contract_calls"] + assert contract_call_2 in result[multicall3_address]["contract_calls"] + assert contract_call_3 in result[multicall3_address]["contract_calls"] From cc4e76ba2f084341e8e1adb59a4553f1deffac72 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 21 Jan 2026 15:01:51 +0800 Subject: [PATCH 29/52] feat: implement batch_ip_asset_with_optimized_workflows method for efficient IP asset registration and royalty distribution --- .../resources/IPAsset.py | 258 +++++++++++------- .../types/resource/IPAsset.py | 24 +- .../utils/registration/registration_utils.py | 202 +++++++------- .../transform_registration_request.py | 183 ++++++++++++- .../integration/test_integration_ip_asset.py | 86 ++++++ 5 files changed, 543 insertions(+), 210 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index cecea1a4..414f698f 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -21,9 +21,6 @@ from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( IPAssetRegistryClient, ) -from story_protocol_python_sdk.abi.IpRoyaltyVaultImpl.IpRoyaltyVaultImpl_client import ( - IpRoyaltyVaultImplClient, -) from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import ( LicenseAttachmentWorkflowsClient, ) @@ -57,7 +54,10 @@ from story_protocol_python_sdk.types.resource.IPAsset import ( BatchMintAndRegisterIPInput, BatchMintAndRegisterIPResponse, + BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, + BatchRegistrationResult, ExtraData, + IPRoyaltyVault, LicenseTermsDataInput, LinkDerivativeResponse, MintAndRegisterRequest, @@ -73,6 +73,7 @@ RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, + TransformedRegistrationRequest, ) from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.constants import ( @@ -93,10 +94,13 @@ is_initial_ip_metadata, ) from story_protocol_python_sdk.utils.registration.registration_utils import ( - validate_license_terms_data, + prepare_distribute_royalty_tokens_requests, + send_transactions, ) from story_protocol_python_sdk.utils.registration.transform_registration_request import ( + transform_distribute_royalty_tokens_request, transform_request, + validate_license_terms_data, ) from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -277,9 +281,7 @@ def register( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] return {"tx_hash": response["tx_hash"], "ip_id": ip_registered["ip_id"]} @@ -566,9 +568,7 @@ def mint_and_register_ip_asset_with_pil_terms( *transformed_request.validated_request, tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] license_terms_ids = self._parse_tx_license_terms_attached_event( response["tx_receipt"] ) @@ -642,9 +642,7 @@ def mint_and_register_ip( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] return { "tx_hash": response["tx_hash"], @@ -691,7 +689,7 @@ def batch_mint_and_register_ip( encoded_data, tx_options=tx_options, ) - registered_ips = self._parse_tx_ip_registered_event(response["tx_receipt"]) + registered_ips = self._get_registered_ips(response["tx_receipt"]) return BatchMintAndRegisterIPResponse( tx_hash=response["tx_hash"], registered_ips=registered_ips, @@ -785,9 +783,7 @@ def register_ip_and_attach_pil_terms( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] license_terms_ids = self._parse_tx_license_terms_attached_event( response["tx_receipt"] ) @@ -845,9 +841,7 @@ def register_derivative_ip( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] return { "tx_hash": response["tx_hash"], @@ -900,9 +894,7 @@ def mint_and_register_ip_and_make_derivative( *transformed_request.validated_request, tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] return RegistrationResponse( tx_hash=response["tx_hash"], ip_id=ip_registered["ip_id"], @@ -952,9 +944,7 @@ def mint_and_register_ip_and_make_derivative_with_license_tokens( allow_duplicates, tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] return RegistrationResponse( tx_hash=response["tx_hash"], ip_id=ip_registered["ip_id"], @@ -1050,9 +1040,7 @@ def register_ip_and_make_derivative_with_license_tokens( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] return RegistrationResponse( tx_hash=response["tx_hash"], @@ -1115,13 +1103,11 @@ def mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] license_terms_ids = self._parse_tx_license_terms_attached_event( response["tx_receipt"] ) - royalty_vault = self.get_royalty_vault_address_by_ip_id( + royalty_vault = self._get_royalty_vault_address_by_ip_id( response["tx_receipt"], ip_registered["ip_id"], ) @@ -1186,10 +1172,8 @@ def mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] - royalty_vault = self.get_royalty_vault_address_by_ip_id( + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] + royalty_vault = self._get_royalty_vault_address_by_ip_id( response["tx_receipt"], ip_registered["ip_id"], ) @@ -1254,10 +1238,8 @@ def register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] - royalty_vault = self.get_royalty_vault_address_by_ip_id( + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] + royalty_vault = self._get_royalty_vault_address_by_ip_id( response["tx_receipt"], ip_registered["ip_id"], ) @@ -1335,13 +1317,11 @@ def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( *transformed_request.validated_request, tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])[ - 0 - ] + ip_registered = self._get_registered_ips(response["tx_receipt"])[0] license_terms_ids = self._parse_tx_license_terms_attached_event( response["tx_receipt"] ) - royalty_vault = self.get_royalty_vault_address_by_ip_id( + royalty_vault = self._get_royalty_vault_address_by_ip_id( response["tx_receipt"], ip_registered["ip_id"], ) @@ -1511,6 +1491,98 @@ def register_ip_asset( except Exception as e: raise ValueError(f"Failed to register IP Asset: {str(e)}") from e + def batch_ip_asset_with_optimized_workflows( + self, + requests: list[RegisterRegistrationRequest], + is_use_multicall: bool = True, + tx_options: dict | None = None, + ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: + """ + Batch register IP assets with optimized workflows. + + :param requests list[RegisterRegistrationRequest]: The list of registration requests. + :param is_use_multicall bool: [Optional] Whether to use multicall. (default: True) + :param tx_options dict: [Optional] Transaction options. + :return list[BatchRegistrationResult]: The list of batch registration results. + """ + try: + transformed_requests: list[TransformedRegistrationRequest] = [ + transform_request(request, self.web3, self.account, self.chain_id) + for request in requests + ] + royalty_distribution_requests: list[ExtraData] = [ + tr.extra_data + for tr in transformed_requests + if tr.extra_data is not None + and tr.extra_data.get("royalty_shares", None) is not None + ] + tx_responses: list[dict[str, HexStr]] = send_transactions( + transformed_requests=transformed_requests, + is_use_multicall3=is_use_multicall, + web3=self.web3, + account=self.account, + tx_options=tx_options, + ) + distribute_royalty_tokens_requests: list[TransformedRegistrationRequest] = ( + [] + ) + response_list: list[BatchRegistrationResult] = [] + + for tx_response in tx_responses: + ip_registered_events = self._parse_tx_ip_registered_event( + tx_response["tx_receipt"] + ) + ip_royalty_vault_deployed_events = ( + self._parse_all_ip_royalty_vault_deployed_events( + tx_response["tx_receipt"] + ) + ) + result = prepare_distribute_royalty_tokens_requests( + extra_data_list=royalty_distribution_requests, + web3=self.web3, + ip_registered=ip_registered_events, + royalty_vault=ip_royalty_vault_deployed_events, + account=self.account, + chain_id=self.chain_id, + ) + if result: + distribute_royalty_tokens_requests.extend(result) + response_list.append( + { + "tx_hash": tx_response["tx_hash"], + "registered_ips": [ + RegisteredIP(ip_id=log["ipId"], token_id=log["tokenId"]) + for log in ip_registered_events + ], + "ip_royalty_vaults": [ + IPRoyaltyVault( + ip_id=log["ipId"], + royalty_vault=log["ipRoyaltyVault"], + ) + for log in ip_royalty_vault_deployed_events + ], + } + ) + + # Send distribute royalty tokens requests + distribute_royalty_tokens_tx_hashes = send_transactions( + transformed_requests=distribute_royalty_tokens_requests, + is_use_multicall3=is_use_multicall, + web3=self.web3, + account=self.account, + tx_options=tx_options, + ) + + return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( + registration_results=response_list, + distribute_royalty_tokens_tx_hashes=[ + tx_hash["tx_hash"] + for tx_hash in distribute_royalty_tokens_tx_hashes + ], + ) + except ValueError as e: + raise ValueError(f"Failed to batch register IP assets: {str(e)}") from e + def _handle_minted_nft_registration( self, nft: MintedNFT, @@ -1901,36 +1973,21 @@ def _distribute_royalty_tokens( :return HexStr: The transaction hash. """ try: - ip_account_impl_client = IPAccountImplClient(self.web3, ip_id) - state = ip_account_impl_client.state() - - ip_royalty_vault_client = IpRoyaltyVaultImplClient(self.web3, royalty_vault) - - signature_response = self.sign_util.get_signature( - state=state, - to=royalty_vault, - encode_data=ip_royalty_vault_client.contract.encode_abi( - abi_element_identifier="approve", - args=[ - self.royalty_token_distribution_workflows_client.contract.address, - total_amount, - ], - ), - verifying_contract=ip_id, + transformed_request = transform_distribute_royalty_tokens_request( + ip_id=ip_id, + royalty_vault=royalty_vault, deadline=deadline, + web3=self.web3, + account=self.account, + chain_id=self.chain_id, + royalty_shares=royalty_shares, + total_amount=total_amount, ) - response = build_and_send_transaction( self.web3, self.account, self.royalty_token_distribution_workflows_client.build_distributeRoyaltyTokens_transaction, - ip_id, - royalty_shares, - { - "signer": self.web3.to_checksum_address(self.account.address), - "deadline": deadline, - "signature": signature_response["signature"], - }, + *transformed_request.validated_request, tx_options=tx_options, ) @@ -1960,28 +2017,39 @@ def _is_registered(self, ip_id: str) -> bool: """ return self.ip_asset_registry_client.isRegistered(ip_id) - def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> list[RegisteredIP]: + def _parse_tx_ip_registered_event( + self, tx_receipt: dict + ) -> list[dict[str, int | Address]]: """ Parse the IPRegistered event from a transaction receipt. :param tx_receipt dict: The transaction receipt. - :return int: The IP ID and token ID from the event, or None. + :return list[dict[str, int | Address]]: The list of IPRegistered event logs. """ event_signature = self.web3.keccak( text="IPRegistered(address,uint256,address,uint256,string,string,uint256)" ).hex() - registered_ips: list[RegisteredIP] = [] + registered_ip_logs: list[dict[str, int | Address]] = [] for log in tx_receipt["logs"]: if log["topics"][0].hex() == event_signature: event_result = self.ip_asset_registry_client.contract.events.IPRegistered.process_log( log ) - registered_ips.append( - RegisteredIP( - ip_id=event_result["args"]["ipId"], - token_id=event_result["args"]["tokenId"], - ) - ) + registered_ip_logs.append(event_result["args"]) + return registered_ip_logs + + def _get_registered_ips(self, tx_receipt: dict) -> list[RegisteredIP]: + """ + Get the registered IPs from a transaction receipt. + + :param tx_receipt dict: The transaction receipt. + :return list[RegisteredIP]: The list of registered IPs. + """ + registered_ip_logs = self._parse_tx_ip_registered_event(tx_receipt) + registered_ips = [ + RegisteredIP(ip_id=log["ip_id"], token_id=log["token_id"]) + for log in registered_ip_logs + ] return registered_ips def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int | None: @@ -2022,8 +2090,8 @@ def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list[int]: return license_terms_ids - def get_royalty_vault_address_by_ip_id( - self, tx_receipt: dict, ipId: Address + def _get_royalty_vault_address_by_ip_id( + self, tx_receipt: dict, ip_id: Address ) -> Address: """ Parse the IpRoyaltyVaultDeployed event from a transaction receipt and return the royalty vault address for a given IP ID. @@ -2032,16 +2100,15 @@ def get_royalty_vault_address_by_ip_id( :param ipId Address: The IP ID. :return Address: The royalty vault address. """ - event_signature = Web3.keccak( - text="IpRoyaltyVaultDeployed(address,address)" - ).hex() - for log in tx_receipt["logs"]: - if log["topics"][0].hex() == event_signature: - event_result = self.royalty_module_client.contract.events.IpRoyaltyVaultDeployed.process_log( - log - ) - if event_result["args"]["ipId"] == ipId: - return event_result["args"]["ipRoyaltyVault"] + ip_royalty_vault_deployed_events = ( + self._parse_all_ip_royalty_vault_deployed_events(tx_receipt) + ) + filtered_ip_royalty_vault_deployed_events: list[dict] = list( + filter(lambda x: x["ipId"] == ip_id, ip_royalty_vault_deployed_events) + ) + if filtered_ip_royalty_vault_deployed_events: + return filtered_ip_royalty_vault_deployed_events[0]["ipRoyaltyVault"] + return None def _validate_recipient(self, recipient: Address | None) -> Address: """ @@ -2056,7 +2123,7 @@ def _validate_recipient(self, recipient: Address | None) -> Address: def _parse_all_ip_royalty_vault_deployed_events( self, tx_receipt: dict - ) -> list[tuple[Address, Address]]: + ) -> list[dict[str, Address]]: """ Parse all IpRoyaltyVaultDeployed events from a transaction receipt. @@ -2066,16 +2133,11 @@ def _parse_all_ip_royalty_vault_deployed_events( event_signature = Web3.keccak( text="IpRoyaltyVaultDeployed(address,address)" ).hex() - results: list[tuple[Address, Address]] = [] + results: list[dict[str, Address]] = [] for log in tx_receipt["logs"]: if log["topics"][0].hex() == event_signature: event_result = self.royalty_module_client.contract.events.IpRoyaltyVaultDeployed.process_log( log ) - results.append( - ( - event_result["args"]["ipId"], - event_result["args"]["ipRoyaltyVault"], - ) - ) + results.append(event_result["args"]) return results diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index bdbe8fe2..925707e1 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -280,6 +280,7 @@ class MintAndRegisterRequest: class RegisterRegistrationRequest: """ Request for register IP operations (already minted NFT). + license_terms_data, deriv_data and royalty_shares at least one of them is required,otherwise it will raise `invalid register request type`. Used for: - registerIpAndAttachPilTerms @@ -310,6 +311,19 @@ class RegisterRegistrationRequest: IpRegistrationWorkflowRequest = MintAndRegisterRequest | RegisterRegistrationRequest +class IPRoyaltyVault(TypedDict): + """ + IP royalty vault. + + Attributes: + ip_id: The IP ID. + royalty_vault: The royalty vault address. + """ + + ip_id: Address + royalty_vault: Address + + class BatchRegistrationResult(TypedDict, total=False): """ Result of a single batch registration transaction. @@ -317,14 +331,12 @@ class BatchRegistrationResult(TypedDict, total=False): Attributes: tx_hash: The transaction hash. registered_ips: List of registered IP assets (ip_id, token_id). - license_terms_ids: [Optional] The IDs of the license terms attached (applies to all IPs in this batch). - ip_royalty_vaults: [Optional] List of (ip_id, ip_royalty_vault) tuples for deployed royalty vaults. + ip_royalty_vaults: [Optional] List of IP royalty vaults for deployed royalty vaults. """ tx_hash: HexStr registered_ips: list[RegisteredIP] - license_terms_ids: list[int] - ip_royalty_vaults: list[tuple[Address, Address]] + ip_royalty_vaults: list[IPRoyaltyVault] class BatchRegisterIpAssetsWithOptimizedWorkflowsResponse(TypedDict, total=False): @@ -380,5 +392,7 @@ class TransformedRegistrationRequest: is_use_multicall3: bool workflow_address: Address validated_request: list - contract_call: Callable[[], HexStr] + original_method_reference: Callable[..., HexStr] extra_data: ExtraData | None = None + # Maybe not needed + contract_call: Callable[[], HexStr] | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index f334dec3..0c338f61 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -1,120 +1,34 @@ """Registration utilities for IP asset operations.""" from collections.abc import Callable -from dataclasses import asdict, is_dataclass, replace from typing import TypedDict from ens.ens import Address, HexStr +from eth_account.signers.local import LocalAccount from web3 import Web3 -from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ( - ModuleRegistryClient, -) -from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( - RoyaltyModuleClient, -) -from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient +from story_protocol_python_sdk.abi.Multicall3.Multicall3_client import Multicall3Client from story_protocol_python_sdk.types.resource.IPAsset import ( - LicenseTermsDataInput, + ExtraData, TransformedRegistrationRequest, ) -from story_protocol_python_sdk.types.resource.License import LicenseTermsInput -from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS -from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData -from story_protocol_python_sdk.utils.pil_flavor import PILFlavor -from story_protocol_python_sdk.utils.util import convert_dict_keys_to_camel_case -from story_protocol_python_sdk.utils.validation import ( - get_revenue_share, - validate_address, +from story_protocol_python_sdk.utils.registration.transform_registration_request import ( + transform_distribute_royalty_tokens_request, ) +from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction class AggregatedRequestData(TypedDict): """Aggregated request data structure.""" encoded_tx_data: list[bytes] - contract_calls: list[Callable[[], HexStr]] - - -def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: - """ - Check if SPG NFT contract has public minting enabled. - - Args: - spg_nft_contract: The address of the SPG NFT contract. - web3: Web3 instance. - - Returns: - True if public minting is enabled, False otherwise. - """ - spg_client = SPGNFTImplClient( - web3, contract_address=validate_address(spg_nft_contract) - ) - return spg_client.publicMinting() - - -def validate_license_terms_data( - license_terms_data: list[LicenseTermsDataInput] | list[dict], - web3: Web3, -) -> list[dict]: - """ - Validate the license terms data. - - Args: - license_terms_data: The license terms data to validate. - web3: Web3 instance. - - Returns: - The validated license terms data. - """ - royalty_module_client = RoyaltyModuleClient(web3) - module_registry_client = ModuleRegistryClient(web3) - - validated_license_terms_data = [] - for term in license_terms_data: - if is_dataclass(term): - terms_dict = asdict(term.terms) - licensing_config_dict = term.licensing_config - else: - terms_dict = term["terms"] - licensing_config_dict = term["licensing_config"] - - license_terms = PILFlavor.validate_license_terms( - LicenseTermsInput(**terms_dict) - ) - license_terms = replace( - license_terms, - commercial_rev_share=get_revenue_share(license_terms.commercial_rev_share), - ) - if license_terms.royalty_policy != ZERO_ADDRESS: - is_whitelisted = royalty_module_client.isWhitelistedRoyaltyPolicy( - license_terms.royalty_policy - ) - if not is_whitelisted: - raise ValueError("The royalty_policy is not whitelisted.") - - if license_terms.currency != ZERO_ADDRESS: - is_whitelisted = royalty_module_client.isWhitelistedRoyaltyToken( - license_terms.currency - ) - if not is_whitelisted: - raise ValueError("The currency is not whitelisted.") - - validated_license_terms_data.append( - { - "terms": convert_dict_keys_to_camel_case(asdict(license_terms)), - "licensingConfig": LicensingConfigData.validate_license_config( - module_registry_client, licensing_config_dict - ), - } - ) - return validated_license_terms_data + method_reference: Callable[[list[bytes], dict], HexStr] def aggregate_multicall_requests( requests: list[TransformedRegistrationRequest], is_use_multicall3: bool, - multicall_address: Address, + web3: Web3, ) -> dict[Address, AggregatedRequestData]: """ Aggregate multicall requests by grouping them by target address. @@ -125,7 +39,7 @@ def aggregate_multicall_requests( Args: requests: List of transformed registration requests to aggregate. is_use_multicall3: Whether to use multicall3 for aggregation. - multicall_address: The multicall3 contract address to use when applicable. + web3: Web3 instance. Returns: Dictionary mapping target addresses to aggregated request data: @@ -135,28 +49,114 @@ def aggregate_multicall_requests( - "contract_calls": List of contract call functions """ aggregated_requests: dict[Address, AggregatedRequestData] = {} + multicall3_client = Multicall3Client(web3) for request in requests: # Determine the target address for this request target_address = ( - multicall_address + multicall3_client.build_aggregate3_transaction if request.is_use_multicall3 and is_use_multicall3 - else request.workflow_address + else request.original_method_reference ) # Initialize entry if it doesn't exist if target_address not in aggregated_requests: aggregated_requests[target_address] = { "encoded_tx_data": [request.encoded_tx_data], - "contract_calls": [request.contract_call], + "method_reference": ( + target_address + if target_address == multicall3_client.build_aggregate3_transaction + else request.original_method_reference + ), } else: # Append to existing entry aggregated_requests[target_address]["encoded_tx_data"].append( request.encoded_tx_data ) - aggregated_requests[target_address]["contract_calls"].append( - request.contract_call - ) return aggregated_requests + + +def prepare_distribute_royalty_tokens_requests( + extra_data_list: list[ExtraData], + web3: Web3, + ip_registered: list[dict[str, int | Address]], + royalty_vault: list[Address], + account: LocalAccount, + chain_id: int, +) -> list[TransformedRegistrationRequest]: + """ + Prepare distribute royalty tokens requests. + + Args: + extra_data_list: The extra data for distribute royalty tokens. + web3: Web3 instance. + ip_registered: The IP registered. + royalty_vault: The royalty vault address. + account: The account for signing and recipient fallback. + chain_id: The chain ID for IP ID calculation. + """ + if not extra_data_list: + return [] + transformed_requests: list[TransformedRegistrationRequest] = [] + for extra_data in extra_data_list: + filtered_ip_registered = list( + filter( + lambda x: x["tokenContract"] == extra_data["nft_contract"] + and x["tokenId"] == extra_data["token_id"], + ip_registered, + ) + ) + if filtered_ip_registered: + ip_royalty_vault = list( + filter( + lambda x: x["ipId"] == filtered_ip_registered[0]["ipId"], + royalty_vault, + ) + )[0]["ipRoyaltyVault"] + transformed_request = transform_distribute_royalty_tokens_request( + ip_id=filtered_ip_registered[0]["ipId"], + royalty_vault=ip_royalty_vault, + deadline=extra_data["deadline"], + web3=web3, + account=account, + chain_id=chain_id, + royalty_shares=extra_data["royalty_shares"], + total_amount=extra_data["royalty_total_amount"], + ) + transformed_requests.append(transformed_request) + return transformed_requests + + +def send_transactions( + transformed_requests: list[TransformedRegistrationRequest], + is_use_multicall3: bool, + web3: Web3, + account: LocalAccount, + tx_options: dict | None = None, +) -> list[dict[str, HexStr | dict]]: + aggregated_requests: dict[Address, AggregatedRequestData] = ( + aggregate_multicall_requests( + requests=transformed_requests, + is_use_multicall3=is_use_multicall3, + web3=web3, + ) + ) + tx_hashes: list[HexStr] = [] + for request_data in aggregated_requests.values(): + # TODO: need to check the argument are correct + response = build_and_send_transaction( + web3, + account, + request_data["method_reference"], + request_data["encoded_tx_data"], + tx_options=tx_options, + ) + tx_hashes.append( + { + "tx_hash": response["tx_hash"], + "tx_receipt": response["tx_receipt"], + } + ) + return tx_hashes diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 3db0d470..6bad886a 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -1,5 +1,7 @@ """Transform registration request utilities.""" +from dataclasses import asdict, is_dataclass, replace + from ens.ens import Address, HexStr from eth_account.signers.local import LocalAccount from typing_extensions import cast @@ -11,37 +13,129 @@ from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( DerivativeWorkflowsClient, ) +from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( + IPAccountImplClient, +) from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( IPAssetRegistryClient, ) +from story_protocol_python_sdk.abi.IpRoyaltyVaultImpl.IpRoyaltyVaultImpl_client import ( + IpRoyaltyVaultImplClient, +) from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import ( LicenseAttachmentWorkflowsClient, ) from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import ( LicensingModuleClient, ) +from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ( + ModuleRegistryClient, +) +from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( + RoyaltyModuleClient, +) from story_protocol_python_sdk.abi.RoyaltyTokenDistributionWorkflows.RoyaltyTokenDistributionWorkflows_client import ( RoyaltyTokenDistributionWorkflowsClient, ) +from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.types.resource.IPAsset import ( ExtraData, + LicenseTermsDataInput, MintAndRegisterRequest, RegisterRegistrationRequest, TransformedRegistrationRequest, ) +from story_protocol_python_sdk.types.resource.License import LicenseTermsInput from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput -from story_protocol_python_sdk.utils.constants import ZERO_HASH +from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeData from story_protocol_python_sdk.utils.function_signature import get_function_signature from story_protocol_python_sdk.utils.ip_metadata import IPMetadata -from story_protocol_python_sdk.utils.registration.registration_utils import ( - get_public_minting, - validate_license_terms_data, -) +from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfigData +from story_protocol_python_sdk.utils.pil_flavor import PILFlavor from story_protocol_python_sdk.utils.royalty import get_royalty_shares from story_protocol_python_sdk.utils.sign import Sign -from story_protocol_python_sdk.utils.validation import validate_address +from story_protocol_python_sdk.utils.util import convert_dict_keys_to_camel_case +from story_protocol_python_sdk.utils.validation import ( + get_revenue_share, + validate_address, +) + + +def get_public_minting(spg_nft_contract: Address, web3: Web3) -> bool: + """ + Check if SPG NFT contract has public minting enabled. + + Args: + spg_nft_contract: The address of the SPG NFT contract. + web3: Web3 instance. + + Returns: + True if public minting is enabled, False otherwise. + """ + spg_client = SPGNFTImplClient( + web3, contract_address=validate_address(spg_nft_contract) + ) + return spg_client.publicMinting() + + +def validate_license_terms_data( + license_terms_data: list[LicenseTermsDataInput] | list[dict], + web3: Web3, +) -> list[dict]: + """ + Validate the license terms data. + + Args: + license_terms_data: The license terms data to validate. + web3: Web3 instance. + + Returns: + The validated license terms data. + """ + royalty_module_client = RoyaltyModuleClient(web3) + module_registry_client = ModuleRegistryClient(web3) + + validated_license_terms_data = [] + for term in license_terms_data: + if is_dataclass(term): + terms_dict = asdict(term.terms) + licensing_config_dict = term.licensing_config + else: + terms_dict = term["terms"] + licensing_config_dict = term["licensing_config"] + + license_terms = PILFlavor.validate_license_terms( + LicenseTermsInput(**terms_dict) + ) + license_terms = replace( + license_terms, + commercial_rev_share=get_revenue_share(license_terms.commercial_rev_share), + ) + if license_terms.royalty_policy != ZERO_ADDRESS: + is_whitelisted = royalty_module_client.isWhitelistedRoyaltyPolicy( + license_terms.royalty_policy + ) + if not is_whitelisted: + raise ValueError("The royalty_policy is not whitelisted.") + + if license_terms.currency != ZERO_ADDRESS: + is_whitelisted = royalty_module_client.isWhitelistedRoyaltyToken( + license_terms.currency + ) + if not is_whitelisted: + raise ValueError("The currency is not whitelisted.") + + validated_license_terms_data.append( + { + "terms": convert_dict_keys_to_camel_case(asdict(license_terms)), + "licensingConfig": LicensingConfigData.validate_license_config( + module_registry_client, licensing_config_dict + ), + } + ) + return validated_license_terms_data def get_allow_duplicates(allow_duplicates: bool | None, request_type: str) -> bool: @@ -106,6 +200,75 @@ def transform_request( raise ValueError("Invalid registration request type") +def transform_distribute_royalty_tokens_request( + ip_id: Address, + royalty_vault: Address, + deadline: int, + web3: Web3, + account: LocalAccount, + chain_id: int, + royalty_shares: list[RoyaltyShareInput], + total_amount: int, +) -> TransformedRegistrationRequest: + """ + Transform a distribute royalty tokens request into encoded transaction data with multicall info. + distributeRoyaltyTokens method don't support multicall3 due to `msg.sender` check. + Args: + ip_id: The IP ID + royalty_vault: The royalty vault address + deadline: The deadline for the transaction + web3: The web3 instance + account: The account for signing and recipient fallback + chain_id: The chain ID for IP ID calculation + royalty_shares: The validated royalty shares with recipient and percentage. + Returns: + TransformedRegistrationRequest with encoded data and multicall strategy + Raises: + ValueError: If the request is invalid + """ + ip_account_impl_client = IPAccountImplClient(web3, ip_id) + state = ip_account_impl_client.state() + royalty_token_distribution_workflows_client = ( + RoyaltyTokenDistributionWorkflowsClient(web3) + ) + ip_royalty_vault_client = IpRoyaltyVaultImplClient(web3, royalty_vault) + signature_response = Sign(web3, chain_id, account).get_signature( + state=state, + to=royalty_vault, + encode_data=ip_royalty_vault_client.contract.encode_abi( + abi_element_identifier="approve", + args=[ + RoyaltyTokenDistributionWorkflowsClient(web3).contract.address, + total_amount, + ], + ), + verifying_contract=ip_id, + deadline=deadline, + ) + validated_request = [ + ip_id, + royalty_shares, + { + "signer": web3.to_checksum_address(account.address), + "deadline": deadline, + "signature": signature_response["signature"], + }, + ] + encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( + abi_element_identifier="distributeRoyaltyTokens", + args=validated_request, + ) + return TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=royalty_token_distribution_workflows_client.contract.address, + validated_request=validated_request, + original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, + extra_data=None, + contract_call=None, + ) + + # ============================================================================= # Mint and Register Request Handlers # ============================================================================= @@ -247,6 +410,7 @@ def contract_call() -> HexStr: encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, + original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, validated_request=validated_request, extra_data=None, contract_call=contract_call, @@ -300,6 +464,7 @@ def contract_call() -> HexStr: validated_request=validated_request, extra_data=None, contract_call=contract_call, + original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -346,6 +511,7 @@ def contract_call() -> HexStr: validated_request=validated_request, extra_data=None, contract_call=contract_call, + original_method_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -388,6 +554,7 @@ def contract_call() -> HexStr: validated_request=validated_request, extra_data=None, contract_call=contract_call, + original_method_reference=derivative_workflows_client.build_multicall_transaction, ) @@ -589,6 +756,7 @@ def contract_call() -> HexStr: token_id=token_id, ), contract_call=contract_call, + original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -663,6 +831,7 @@ def contract_call() -> HexStr: token_id=token_id, ), contract_call=contract_call, + original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -727,6 +896,7 @@ def contract_call() -> HexStr: validated_request=validated_request, extra_data=None, contract_call=contract_call, + original_method_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -789,6 +959,7 @@ def contract_call() -> HexStr: validated_request=validated_request, extra_data=None, contract_call=contract_call, + original_method_reference=derivative_workflows_client.build_multicall_transaction, ) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index f1ec1bf8..6856e42a 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -16,6 +16,7 @@ MintNFT, NativeRoyaltyPolicy, PILFlavor, + RegisterRegistrationRequest, RoyaltyShareInput, StoryClient, ) @@ -1758,3 +1759,88 @@ def test_link_derivative_with_license_tokens( assert "tx_hash" in response assert isinstance(response["tx_hash"], str) assert len(response["tx_hash"]) > 0 + + +class TestBatchRegisterIpAssetsWithOptimizedWorkflows: + def test_batch_register_ip_assets_with_optimized_workflows_with_register_registration_request( + self, + story_client: StoryClient, + ): + """Test batch register IP assets with optimized workflows.""" + token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account) + requests = [ + # RegisterRegistrationRequest( + # nft_contract=MockERC721, + # token_id=token_id_1, + # ip_metadata=COMMON_IP_METADATA, + # deadline=100000, + # license_terms_data=[ + # LicenseTermsDataInput( + # terms=pil_flavor.PILFlavor.commercial_use( + # default_minting_fee=1000000000000000000, + # currency=MockERC20, + # royalty_policy=NativeRoyaltyPolicy.LAP, + # ), + # licensing_config=LicensingConfig( + # is_set=True, + # minting_fee=1000000000000000000, + # licensing_hook=ZERO_ADDRESS, + # hook_data=ZERO_HASH, + # commercial_rev_share=50, + # disabled=False, + # expect_minimum_group_reward_share=0, + # expect_group_reward_pool=ZERO_ADDRESS, + # ), + # ) + # ], + # ), + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_1, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.non_commercial_social_remixing(), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=0, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=10, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ) + ] + response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( + requests=requests, + is_use_multicall=True, + ) + print( + "-------------------------------- Batch Register IP Assets With Optimized Workflows Response --------------------------------" + ) + print(response) + assert response is not None From 03fc689b6a37a0f41dabb1126a12fd4b40f163a7 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 21 Jan 2026 15:51:58 +0800 Subject: [PATCH 30/52] refactor: update IPAsset and registration utilities to enhance request handling and remove contract_call references --- .../types/resource/IPAsset.py | 10 ++- .../utils/registration/registration_utils.py | 57 +++++++--------- .../transform_registration_request.py | 67 ------------------- 3 files changed, 30 insertions(+), 104 deletions(-) diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 925707e1..9dd322ff 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -362,8 +362,7 @@ class ExtraData(TypedDict, total=False): Attributes: royalty_shares: [Optional] The royalty shares for distribution. deadline: [Optional] The deadline for the signature. - max_license_tokens: [Optional] Maximum license tokens for each license term. - license_terms_data: [Optional] The license terms data. + royalty_total_amount: [Optional] The total amount of royalty tokens to distribute. nft_contract: [Optional] The NFT contract address. token_id: [Optional] The token ID. """ @@ -384,15 +383,14 @@ class TransformedRegistrationRequest: encoded_tx_data: The encoded transaction data. is_use_multicall3: Whether to use multicall3 or SPG's native multicall. workflow_address: The workflow contract address. + validated_request: The validated request arguments for the contract method. + original_method_reference: The original method reference for building transactions. extra_data: [Optional] Extra data for post-processing. - contract_call: [Optional] The contract call function. """ encoded_tx_data: bytes is_use_multicall3: bool workflow_address: Address - validated_request: list + validated_request: list[Address | int | str | bytes | dict | bool] original_method_reference: Callable[..., HexStr] extra_data: ExtraData | None = None - # Maybe not needed - contract_call: Callable[[], HexStr] | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 0c338f61..96a2bbd3 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -34,7 +34,7 @@ def aggregate_multicall_requests( Aggregate multicall requests by grouping them by target address. Groups requests that should be sent to the same multicall address together, - collecting their encoded transaction data and contract call functions. + collecting their encoded transaction data and method references. Args: requests: List of transformed registration requests to aggregate. @@ -46,7 +46,7 @@ def aggregate_multicall_requests( - Key: Address (multicall address or workflow address) - Value: AggregatedRequestData with: - "encoded_tx_data": List of encoded transaction data (bytes) - - "contract_calls": List of contract call functions + - "method_reference": The method to build the transaction """ aggregated_requests: dict[Address, AggregatedRequestData] = {} multicall3_client = Multicall3Client(web3) @@ -54,9 +54,9 @@ def aggregate_multicall_requests( for request in requests: # Determine the target address for this request target_address = ( - multicall3_client.build_aggregate3_transaction + multicall3_client.contract.address if request.is_use_multicall3 and is_use_multicall3 - else request.original_method_reference + else request.workflow_address ) # Initialize entry if it doesn't exist @@ -64,16 +64,14 @@ def aggregate_multicall_requests( aggregated_requests[target_address] = { "encoded_tx_data": [request.encoded_tx_data], "method_reference": ( - target_address - if target_address == multicall3_client.build_aggregate3_transaction + multicall3_client.build_aggregate3_transaction + if target_address == multicall3_client.contract.address else request.original_method_reference ), } - else: - # Append to existing entry - aggregated_requests[target_address]["encoded_tx_data"].append( - request.encoded_tx_data - ) + aggregated_requests[target_address]["encoded_tx_data"].append( + request.encoded_tx_data + ) return aggregated_requests @@ -82,7 +80,7 @@ def prepare_distribute_royalty_tokens_requests( extra_data_list: list[ExtraData], web3: Web3, ip_registered: list[dict[str, int | Address]], - royalty_vault: list[Address], + royalty_vault: list[dict[str, Address]], account: LocalAccount, chain_id: int, ) -> list[TransformedRegistrationRequest]: @@ -93,7 +91,7 @@ def prepare_distribute_royalty_tokens_requests( extra_data_list: The extra data for distribute royalty tokens. web3: Web3 instance. ip_registered: The IP registered. - royalty_vault: The royalty vault address. + royalty_vault: The royalty vault addresses. account: The account for signing and recipient fallback. chain_id: The chain ID for IP ID calculation. """ @@ -101,22 +99,20 @@ def prepare_distribute_royalty_tokens_requests( return [] transformed_requests: list[TransformedRegistrationRequest] = [] for extra_data in extra_data_list: - filtered_ip_registered = list( - filter( - lambda x: x["tokenContract"] == extra_data["nft_contract"] - and x["tokenId"] == extra_data["token_id"], - ip_registered, - ) - ) + filtered_ip_registered = [ + x + for x in ip_registered + if x["tokenContract"] == extra_data["nft_contract"] + and x["tokenId"] == extra_data["token_id"] + ] if filtered_ip_registered: - ip_royalty_vault = list( - filter( - lambda x: x["ipId"] == filtered_ip_registered[0]["ipId"], - royalty_vault, - ) - )[0]["ipRoyaltyVault"] + ip_id = filtered_ip_registered[0]["ipId"] + matching_vaults = [x for x in royalty_vault if x["ipId"] == ip_id] + if not matching_vaults: + continue + ip_royalty_vault = matching_vaults[0]["ipRoyaltyVault"] transformed_request = transform_distribute_royalty_tokens_request( - ip_id=filtered_ip_registered[0]["ipId"], + ip_id=ip_id, royalty_vault=ip_royalty_vault, deadline=extra_data["deadline"], web3=web3, @@ -143,9 +139,8 @@ def send_transactions( web3=web3, ) ) - tx_hashes: list[HexStr] = [] + tx_results: list[dict[str, HexStr | dict]] = [] for request_data in aggregated_requests.values(): - # TODO: need to check the argument are correct response = build_and_send_transaction( web3, account, @@ -153,10 +148,10 @@ def send_transactions( request_data["encoded_tx_data"], tx_options=tx_options, ) - tx_hashes.append( + tx_results.append( { "tx_hash": response["tx_hash"], "tx_receipt": response["tx_receipt"], } ) - return tx_hashes + return tx_results diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 6bad886a..28c76396 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -265,7 +265,6 @@ def transform_distribute_royalty_tokens_request( validated_request=validated_request, original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, extra_data=None, - contract_call=None, ) @@ -399,13 +398,6 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( args=validated_request, ) - def contract_call() -> HexStr: - response = royalty_token_distribution_workflows_client.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, @@ -413,7 +405,6 @@ def contract_call() -> HexStr: original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, validated_request=validated_request, extra_data=None, - contract_call=contract_call, ) @@ -450,20 +441,12 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( args=validated_request, ) - def contract_call() -> HexStr: - response = royalty_token_distribution_workflows_client.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=royalty_token_distribution_workflows_address, validated_request=validated_request, extra_data=None, - contract_call=contract_call, original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -495,22 +478,12 @@ def _handle_mint_and_register_with_license_terms( args=validated_request, ) - def contract_call() -> HexStr: - response = ( - license_attachment_workflows_client.mintAndRegisterIpAndAttachPILTerms( - *validated_request - ) - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=license_attachment_workflows_address, validated_request=validated_request, extra_data=None, - contract_call=contract_call, original_method_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -540,20 +513,12 @@ def _handle_mint_and_register_with_derivative( args=validated_request, ) - def contract_call() -> HexStr: - response = derivative_workflows_client.mintAndRegisterIpAndMakeDerivative( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=is_public_minting, workflow_address=derivative_workflows_address, validated_request=validated_request, extra_data=None, - contract_call=contract_call, original_method_reference=derivative_workflows_client.build_multicall_transaction, ) @@ -736,13 +701,6 @@ def _handle_register_with_license_terms_and_royalty_vault( args=validated_request, ) - def contract_call() -> HexStr: - response = royalty_token_distribution_workflows_client.registerIpAndAttachPILTermsAndDeployRoyaltyVault( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, @@ -755,7 +713,6 @@ def contract_call() -> HexStr: nft_contract=nft_contract, token_id=token_id, ), - contract_call=contract_call, original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -811,13 +768,6 @@ def _handle_register_with_derivative_and_royalty_vault( args=validated_request, ) - def contract_call() -> HexStr: - response = royalty_token_distribution_workflows_client.registerIpAndMakeDerivativeAndDeployRoyaltyVault( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, @@ -830,7 +780,6 @@ def contract_call() -> HexStr: nft_contract=nft_contract, token_id=token_id, ), - contract_call=contract_call, original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -882,20 +831,12 @@ def _handle_register_with_license_terms( args=validated_request, ) - def contract_call() -> HexStr: - response = license_attachment_workflows_client.registerIpAndAttachPILTerms( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=license_attachment_workflows_address, validated_request=validated_request, extra_data=None, - contract_call=contract_call, original_method_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -945,20 +886,12 @@ def _handle_register_with_derivative( args=validated_request, ) - def contract_call() -> HexStr: - response = derivative_workflows_client.registerIpAndMakeDerivative( - *validated_request - ) - web3.eth.wait_for_transaction_receipt(response["tx_hash"]) - return response["tx_hash"] - return TransformedRegistrationRequest( encoded_tx_data=encoded_data, is_use_multicall3=False, workflow_address=derivative_workflows_address, validated_request=validated_request, extra_data=None, - contract_call=contract_call, original_method_reference=derivative_workflows_client.build_multicall_transaction, ) From 6256c04823e7baf8d525bffe0256f9bcbbbaa975 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 21 Jan 2026 17:56:22 +0800 Subject: [PATCH 31/52] test: test_batch_register_ip_assets_with_optimized_workflows_with_register_registration_request --- .../resources/IPAsset.py | 61 +++---- .../utils/registration/registration_utils.py | 19 ++- .../integration/test_integration_ip_asset.py | 151 ++++++++++++++---- 3 files changed, 163 insertions(+), 68 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 414f698f..6a516cd4 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -57,7 +57,6 @@ BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, BatchRegistrationResult, ExtraData, - IPRoyaltyVault, LicenseTermsDataInput, LinkDerivativeResponse, MintAndRegisterRequest, @@ -1506,16 +1505,11 @@ def batch_ip_asset_with_optimized_workflows( :return list[BatchRegistrationResult]: The list of batch registration results. """ try: + # Transform registration requests to transformed registration requests and send them into transaction transformed_requests: list[TransformedRegistrationRequest] = [ transform_request(request, self.web3, self.account, self.chain_id) for request in requests ] - royalty_distribution_requests: list[ExtraData] = [ - tr.extra_data - for tr in transformed_requests - if tr.extra_data is not None - and tr.extra_data.get("royalty_shares", None) is not None - ] tx_responses: list[dict[str, HexStr]] = send_transactions( transformed_requests=transformed_requests, is_use_multicall3=is_use_multicall, @@ -1523,11 +1517,22 @@ def batch_ip_asset_with_optimized_workflows( account=self.account, tx_options=tx_options, ) + + # Extract royalty distribution requests from workflow responses that contain royalty shares + # We need to handle `distributeRoyaltyTokens` separately because this method requires + # a signature with the royalty vault address, which is only available after the initial registration distribute_royalty_tokens_requests: list[TransformedRegistrationRequest] = ( [] ) - response_list: list[BatchRegistrationResult] = [] + royalty_distribution_requests: list[ExtraData] = [ + tr.extra_data + for tr in transformed_requests + if tr.extra_data is not None + and tr.extra_data.get("royalty_shares", None) is not None + ] + # Parse the response of the registration requests and collect distribute royalty tokens requests + response_list: list[BatchRegistrationResult] = [] for tx_response in tx_responses: ip_registered_events = self._parse_tx_ip_registered_event( tx_response["tx_receipt"] @@ -1537,31 +1542,29 @@ def batch_ip_asset_with_optimized_workflows( tx_response["tx_receipt"] ) ) - result = prepare_distribute_royalty_tokens_requests( - extra_data_list=royalty_distribution_requests, - web3=self.web3, - ip_registered=ip_registered_events, - royalty_vault=ip_royalty_vault_deployed_events, - account=self.account, - chain_id=self.chain_id, + transferred_distribute_royalty_tokens_requests, matching_vaults = ( + prepare_distribute_royalty_tokens_requests( + extra_data_list=royalty_distribution_requests, + web3=self.web3, + ip_registered=ip_registered_events, + royalty_vault=ip_royalty_vault_deployed_events, + account=self.account, + chain_id=self.chain_id, + ) + ) + + distribute_royalty_tokens_requests.extend( + transferred_distribute_royalty_tokens_requests ) - if result: - distribute_royalty_tokens_requests.extend(result) response_list.append( - { - "tx_hash": tx_response["tx_hash"], - "registered_ips": [ + BatchRegistrationResult( + tx_hash=tx_response["tx_hash"], + registered_ips=[ RegisteredIP(ip_id=log["ipId"], token_id=log["tokenId"]) for log in ip_registered_events ], - "ip_royalty_vaults": [ - IPRoyaltyVault( - ip_id=log["ipId"], - royalty_vault=log["ipRoyaltyVault"], - ) - for log in ip_royalty_vault_deployed_events - ], - } + ip_royalty_vaults=matching_vaults, + ) ) # Send distribute royalty tokens requests @@ -2047,7 +2050,7 @@ def _get_registered_ips(self, tx_receipt: dict) -> list[RegisteredIP]: """ registered_ip_logs = self._parse_tx_ip_registered_event(tx_receipt) registered_ips = [ - RegisteredIP(ip_id=log["ip_id"], token_id=log["token_id"]) + RegisteredIP(ip_id=log["ipId"], token_id=log["tokenId"]) for log in registered_ip_logs ] return registered_ips diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 96a2bbd3..3f3e8165 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -10,6 +10,7 @@ from story_protocol_python_sdk.abi.Multicall3.Multicall3_client import Multicall3Client from story_protocol_python_sdk.types.resource.IPAsset import ( ExtraData, + IPRoyaltyVault, TransformedRegistrationRequest, ) from story_protocol_python_sdk.utils.registration.transform_registration_request import ( @@ -62,7 +63,7 @@ def aggregate_multicall_requests( # Initialize entry if it doesn't exist if target_address not in aggregated_requests: aggregated_requests[target_address] = { - "encoded_tx_data": [request.encoded_tx_data], + "encoded_tx_data": [], "method_reference": ( multicall3_client.build_aggregate3_transaction if target_address == multicall3_client.contract.address @@ -83,7 +84,7 @@ def prepare_distribute_royalty_tokens_requests( royalty_vault: list[dict[str, Address]], account: LocalAccount, chain_id: int, -) -> list[TransformedRegistrationRequest]: +) -> tuple[list[TransformedRegistrationRequest], list[IPRoyaltyVault]]: """ Prepare distribute royalty tokens requests. @@ -96,8 +97,9 @@ def prepare_distribute_royalty_tokens_requests( chain_id: The chain ID for IP ID calculation. """ if not extra_data_list: - return [] + return [], [] transformed_requests: list[TransformedRegistrationRequest] = [] + matching_vaults: list[IPRoyaltyVault] = [] for extra_data in extra_data_list: filtered_ip_registered = [ x @@ -107,10 +109,13 @@ def prepare_distribute_royalty_tokens_requests( ] if filtered_ip_registered: ip_id = filtered_ip_registered[0]["ipId"] - matching_vaults = [x for x in royalty_vault if x["ipId"] == ip_id] - if not matching_vaults: + matching_vault = [x for x in royalty_vault if x["ipId"] == ip_id] + if not matching_vault: continue - ip_royalty_vault = matching_vaults[0]["ipRoyaltyVault"] + ip_royalty_vault = matching_vault[0]["ipRoyaltyVault"] + matching_vaults.append( + IPRoyaltyVault(ip_id=ip_id, royalty_vault=ip_royalty_vault) + ) transformed_request = transform_distribute_royalty_tokens_request( ip_id=ip_id, royalty_vault=ip_royalty_vault, @@ -122,7 +127,7 @@ def prepare_distribute_royalty_tokens_requests( total_amount=extra_data["royalty_total_amount"], ) transformed_requests.append(transformed_request) - return transformed_requests + return transformed_requests, matching_vaults def send_transactions( diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 6856e42a..62fbc4c7 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -1765,38 +1765,51 @@ class TestBatchRegisterIpAssetsWithOptimizedWorkflows: def test_batch_register_ip_assets_with_optimized_workflows_with_register_registration_request( self, story_client: StoryClient, + nft_collection, ): """Test batch register IP assets with optimized workflows.""" token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_2 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_3 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_4 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_5 = get_token_id(MockERC721, story_client.web3, story_client.account) + parent_ip_and_license_terms_1 = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) + parent_ip_and_license_terms_2 = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) requests = [ - # RegisterRegistrationRequest( - # nft_contract=MockERC721, - # token_id=token_id_1, - # ip_metadata=COMMON_IP_METADATA, - # deadline=100000, - # license_terms_data=[ - # LicenseTermsDataInput( - # terms=pil_flavor.PILFlavor.commercial_use( - # default_minting_fee=1000000000000000000, - # currency=MockERC20, - # royalty_policy=NativeRoyaltyPolicy.LAP, - # ), - # licensing_config=LicensingConfig( - # is_set=True, - # minting_fee=1000000000000000000, - # licensing_hook=ZERO_ADDRESS, - # hook_data=ZERO_HASH, - # commercial_rev_share=50, - # disabled=False, - # expect_minimum_group_reward_share=0, - # expect_group_reward_pool=ZERO_ADDRESS, - # ), - # ) - # ], - # ), + # LicenseAttachmentWorkflowsClient RegisterRegistrationRequest( nft_contract=MockERC721, token_id=token_id_1, + ip_metadata=COMMON_IP_METADATA, + deadline=100000, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=1000000000000000000, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=1000000000000000000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + ], + ), + # RoyaltyTokenDistributionWorkflowsClient + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_2, license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.non_commercial_social_remixing(), @@ -1833,14 +1846,88 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_register_registr RoyaltyShareInput(recipient=account.address, percentage=50.0), RoyaltyShareInput(recipient=account_2.address, percentage=50.0), ], - ) + ), + # DerivativeWorkflowsClient + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_3, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + ), + # RoyaltyTokenDistributionWorkflowsClient + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_4, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=60.0), + RoyaltyShareInput(recipient=account_2.address, percentage=40.0), + ], + ), + # RoyaltyTokenDistributionWorkflowsClient + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_5, + ip_metadata=COMMON_IP_METADATA, + deadline=100000, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=1000000000000000000, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=1000000000000000000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ) + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), ] + # Enhanced: Thoroughly verify transaction aggregation, registration output, + # and cross-check the actual on-chain registered asset state with expectations. + # + # Expectations: + # - 3 total blockchain transactions (aggregated by workflow): + # 1. LicenseAttachmentWorkflowsClient: attaches license terms (1 tx) + # 2. RoyaltyTokenDistributionWorkflowsClient: batch of 3, merged to 1 tx (with vault creation) + # 3. DerivativeWorkflowsClient: creates derivatives (1 tx) + # - Only 1 distribute_royalty_tokens_tx_hash, even for multiple assets with royalty shares. response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( requests=requests, - is_use_multicall=True, ) - print( - "-------------------------------- Batch Register IP Assets With Optimized Workflows Response --------------------------------" - ) - print(response) - assert response is not None + # Assert batch-level structure and invariants + assert isinstance(response, dict) + assert "registration_results" in response + assert "distribute_royalty_tokens_tx_hashes" in response + + registration_results = response["registration_results"] + assert registration_results[0]["tx_hash"] is not None + assert len(registration_results[0]["registered_ips"]) == 1 + assert len(registration_results[0]["ip_royalty_vaults"]) == 0 + assert registration_results[1]["tx_hash"] is not None + assert len(registration_results[1]["registered_ips"]) == 3 + assert len(registration_results[1]["ip_royalty_vaults"]) == 3 + assert registration_results[2]["tx_hash"] is not None + assert len(registration_results[2]["registered_ips"]) == 1 + assert len(registration_results[2]["ip_royalty_vaults"]) == 0 From 684991ada1c8232feffb83a98f25f36bd1bb3b33 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 22 Jan 2026 14:52:42 +0800 Subject: [PATCH 32/52] test: test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_register_ip_request --- .../resources/IPAsset.py | 70 ++++++++++- .../utils/registration/registration_utils.py | 31 +++-- .../transform_registration_request.py | 8 +- .../integration/test_integration_ip_asset.py | 111 ++++++++++++++++++ 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 6a516cd4..97422064 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1,5 +1,6 @@ """Module for handling IP Account operations and transactions.""" +from collections.abc import Sequence from typing import cast from ens.ens import Address, HexStr @@ -57,6 +58,7 @@ BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, BatchRegistrationResult, ExtraData, + IpRegistrationWorkflowRequest, LicenseTermsDataInput, LinkDerivativeResponse, MintAndRegisterRequest, @@ -1492,17 +1494,75 @@ def register_ip_asset( def batch_ip_asset_with_optimized_workflows( self, - requests: list[RegisterRegistrationRequest], + requests: Sequence[IpRegistrationWorkflowRequest], is_use_multicall: bool = True, tx_options: dict | None = None, ) -> BatchRegisterIpAssetsWithOptimizedWorkflowsResponse: """ - Batch register IP assets with optimized workflows. + Batch register IP assets with optimized workflow selection. - :param requests list[RegisterRegistrationRequest]: The list of registration requests. - :param is_use_multicall bool: [Optional] Whether to use multicall. (default: True) + This method automatically selects the appropriate workflow based on input parameters and provides + intelligent transaction batching for better gas efficiency. + + **Request Types:** + + - `MintAndRegisterRequest`: Mint a new NFT from an SPG NFT contract and register as IP + - `RegisterRegistrationRequest`: Register an already minted NFT as IP + + **Workflow Selection:** + + For `MintAndRegisterRequest` (supports Multicall3 when `spg_nft_contract` has public minting enabled): + 1. `license_terms_data` + `royalty_shares` → `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens` (RoyaltyTokenDistributionWorkflows) + - Note: Always uses workflow's native multicall due to `msg.sender` limitation + 2. `license_terms_data` → `mintAndRegisterIpAndAttachPILTerms` (LicenseAttachmentWorkflows) + 3. `deriv_data` + `royalty_shares` → `mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens` (RoyaltyTokenDistributionWorkflows) + 4. `deriv_data` → `mintAndRegisterIpAndMakeDerivative` (DerivativeWorkflows) + 5. Other combinations throw `Invalid mint and register request type` error + + For `RegisterRegistrationRequest` (always uses workflow's native multicall due to signature requirements): + 1. `license_terms_data` + `royalty_shares` → `registerIpAndAttachPILTermsAndDeployRoyaltyVault` (RoyaltyTokenDistributionWorkflows) + 2. `deriv_data` + `royalty_shares` → `registerIpAndMakeDerivativeAndDeployRoyaltyVault` (RoyaltyTokenDistributionWorkflows) + 3. `license_terms_data` → `registerIpAndAttachPILTerms` (LicenseAttachmentWorkflows) + 4. `deriv_data` → `registerIpAndMakeDerivative` (DerivativeWorkflows) + 5. Other combinations throw `Invalid register request type` error + + **Multicall Strategy:** + + - Multicall3: Used when `is_use_multicall=True`, request is `MintAndRegisterRequest`, `spg_nft_contract` has public minting, + - Workflow's native multicall: Used for all other cases + - Requests using the same workflow are aggregated into a single multicall transaction + + **Special Handling:** + + Royalty token distribution is handled in a separate transaction because it requires a signature with the + royalty vault address, which is only available after initial registration completes. + + :param requests Sequence[IpRegistrationWorkflowRequest]: The list of registration requests. + :param is_use_multicall bool: [Optional] Whether to use multicall3 for eligible workflows. (default: True) :param tx_options dict: [Optional] Transaction options. - :return list[BatchRegistrationResult]: The list of batch registration results. + :return `BatchRegisterIpAssetsWithOptimizedWorkflowsResponse`: Response with registration results and distribute royalty tokens transaction hashes. + + **Example:** + + ```python + # Mint and register with PIL terms (supports multicall3 if public minting enabled) + response = client.ip_asset.batch_ip_asset_with_optimized_workflows( + requests=[ + MintAndRegisterRequest( + spg_nft_contract="0x...", + license_terms_data=[...], + ip_metadata={...} + ), + RegisterRegistrationRequest( + nft_contract="0x...", + token_id=123, + deriv_data={...}, + ip_metadata={...} + ) + ], + is_use_multicall=True + ) + ``` """ try: # Transform registration requests to transformed registration requests and send them into transaction diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 3f3e8165..ada86b43 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -19,10 +19,17 @@ from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction +class Multicall3Call(TypedDict): + target: Address + allowFailure: bool + value: int + callData: bytes + + class AggregatedRequestData(TypedDict): """Aggregated request data structure.""" - encoded_tx_data: list[bytes] + call_data: list[bytes | Multicall3Call] method_reference: Callable[[list[bytes], dict], HexStr] @@ -46,7 +53,7 @@ def aggregate_multicall_requests( Dictionary mapping target addresses to aggregated request data: - Key: Address (multicall address or workflow address) - Value: AggregatedRequestData with: - - "encoded_tx_data": List of encoded transaction data (bytes) + - "call_data": List of encoded transaction data (bytes) - "method_reference": The method to build the transaction """ aggregated_requests: dict[Address, AggregatedRequestData] = {} @@ -63,16 +70,26 @@ def aggregate_multicall_requests( # Initialize entry if it doesn't exist if target_address not in aggregated_requests: aggregated_requests[target_address] = { - "encoded_tx_data": [], + "call_data": [], "method_reference": ( multicall3_client.build_aggregate3_transaction if target_address == multicall3_client.contract.address else request.original_method_reference ), } - aggregated_requests[target_address]["encoded_tx_data"].append( - request.encoded_tx_data - ) + if target_address == multicall3_client.contract.address: + aggregated_requests[target_address]["call_data"].append( + { + "target": request.workflow_address, + "allowFailure": True, + "value": 0, + "callData": request.encoded_tx_data, + } + ) + else: + aggregated_requests[target_address]["call_data"].append( + request.encoded_tx_data + ) return aggregated_requests @@ -150,7 +167,7 @@ def send_transactions( web3, account, request_data["method_reference"], - request_data["encoded_tx_data"], + request_data["call_data"], tx_options=tx_options, ) tx_results.append( diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 28c76396..9f8a74b4 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -41,6 +41,7 @@ from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.types.resource.IPAsset import ( ExtraData, + IpRegistrationWorkflowRequest, LicenseTermsDataInput, MintAndRegisterRequest, RegisterRegistrationRequest, @@ -163,7 +164,7 @@ def get_allow_duplicates(allow_duplicates: bool | None, request_type: str) -> bo def transform_request( - request: MintAndRegisterRequest | RegisterRegistrationRequest, + request: IpRegistrationWorkflowRequest, web3: Web3, account: LocalAccount, chain_id: int, @@ -178,7 +179,7 @@ def transform_request( 4. Determines whether to use multicall3 or SPG's native multicall Args: - request: The registration request (MintAndRegisterRequest or RegisterRegistrationRequest) + request: The registration request (`IpRegistrationWorkflowRequest`) web3: Web3 instance for contract interaction account: The account for signing and recipient fallback chain_id: The chain ID for IP ID calculation @@ -400,7 +401,8 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( return TransformedRegistrationRequest( encoded_tx_data=encoded_data, - is_use_multicall3=is_public_minting, + # Because mint tokens is given `msg.sender` as the recipient, so we need to set `useMulticall3` to false. + is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_address, original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, validated_request=validated_request, diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 62fbc4c7..44f646fd 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -12,6 +12,7 @@ LicenseTermsInput, LicenseTermsOverride, LicensingConfig, + MintAndRegisterRequest, MintedNFT, MintNFT, NativeRoyaltyPolicy, @@ -1931,3 +1932,113 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_register_registr assert registration_results[2]["tx_hash"] is not None assert len(registration_results[2]["registered_ips"]) == 1 assert len(registration_results[2]["ip_royalty_vaults"]) == 0 + + def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_register_ip_request( + self, + story_client: StoryClient, + nft_collection, + ): + """Test batch register IP assets with optimized workflows with mint and register IP request.""" + parent_ip_and_license_terms_1 = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) + parent_ip_and_license_terms_2 = create_parent_ip_and_license_terms( + story_client, nft_collection, account + ) + requests = [ + MintAndRegisterRequest( + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + ip_metadata=COMMON_IP_METADATA, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=1000000000000000000, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=1000000000000000000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + ), + MintAndRegisterRequest( + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + ), + MintAndRegisterRequest( + spg_nft_contract=nft_collection, + recipient=account.address, + allow_duplicates=True, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=60.0), + RoyaltyShareInput(recipient=account_2.address, percentage=40.0), + ], + ), + MintAndRegisterRequest( + spg_nft_contract=nft_collection, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=0, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + ] + response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( + requests=requests, + tx_options={ + "gas": 16777216, + }, + ) + print( + "-------------------------------- Batch Register IP Assets With Optimized Workflows Response --------------------------------" + ) + print(response) + assert isinstance(response, dict) + assert "registration_results" in response + assert "distribute_royalty_tokens_tx_hashes" in response + + registration_results = response["registration_results"] + assert registration_results[0]["tx_hash"] is not None + assert len(registration_results[0]["registered_ips"]) == 1 + assert len(registration_results[0]["ip_royalty_vaults"]) == 0 From 5f3e6083cded5c14f4ffe1ae740868018d4de7ca Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 22 Jan 2026 17:55:30 +0800 Subject: [PATCH 33/52] feat: add the license terms id via ip_id --- .../resources/IPAsset.py | 63 +++++++++++++++++-- .../types/resource/IPAsset.py | 16 ++++- .../utils/registration/registration_utils.py | 16 ++++- .../transform_registration_request.py | 15 +++-- .../integration/test_integration_ip_asset.py | 10 +++ 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 97422064..bb67b63b 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -68,6 +68,7 @@ RegisterDerivativeIPAndAttachAndDistributeRoyaltyTokensResponse, RegisterDerivativeIpAssetResponse, RegisteredIP, + RegisteredIPWithLicenseTermsIds, RegisterIpAssetResponse, RegisterPILTermsAndAttachResponse, RegisterRegistrationRequest, @@ -95,6 +96,7 @@ is_initial_ip_metadata, ) from story_protocol_python_sdk.utils.registration.registration_utils import ( + AggregatedRequestData, prepare_distribute_royalty_tokens_requests, send_transactions, ) @@ -1570,14 +1572,13 @@ def batch_ip_asset_with_optimized_workflows( transform_request(request, self.web3, self.account, self.chain_id) for request in requests ] - tx_responses: list[dict[str, HexStr]] = send_transactions( + tx_responses, aggregated_requests = send_transactions( transformed_requests=transformed_requests, is_use_multicall3=is_use_multicall, web3=self.web3, account=self.account, tx_options=tx_options, ) - # Extract royalty distribution requests from workflow responses that contain royalty shares # We need to handle `distributeRoyaltyTokens` separately because this method requires # a signature with the royalty vault address, which is only available after the initial registration @@ -1620,7 +1621,11 @@ def batch_ip_asset_with_optimized_workflows( BatchRegistrationResult( tx_hash=tx_response["tx_hash"], registered_ips=[ - RegisteredIP(ip_id=log["ipId"], token_id=log["tokenId"]) + RegisteredIPWithLicenseTermsIds( + ip_id=log["ipId"], + token_id=log["tokenId"], + license_terms_ids=[], + ) for log in ip_registered_events ], ip_royalty_vaults=matching_vaults, @@ -1628,7 +1633,7 @@ def batch_ip_asset_with_optimized_workflows( ) # Send distribute royalty tokens requests - distribute_royalty_tokens_tx_hashes = send_transactions( + distribute_royalty_tokens_tx_responses, _ = send_transactions( transformed_requests=distribute_royalty_tokens_requests, is_use_multicall3=is_use_multicall, web3=self.web3, @@ -1636,16 +1641,62 @@ def batch_ip_asset_with_optimized_workflows( tx_options=tx_options, ) + # Populate the license terms ids into the response + response_list_with_license_terms_ids = ( + self._populate_license_terms_ids_into_response( + response_list, aggregated_requests + ) + ) + return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( - registration_results=response_list, + registration_results=response_list_with_license_terms_ids, distribute_royalty_tokens_tx_hashes=[ tx_hash["tx_hash"] - for tx_hash in distribute_royalty_tokens_tx_hashes + for tx_hash in distribute_royalty_tokens_tx_responses ], ) except ValueError as e: raise ValueError(f"Failed to batch register IP assets: {str(e)}") from e + def _populate_license_terms_ids_into_response( + self, + registration_results: list[BatchRegistrationResult], + aggregated_requests: dict[Address, AggregatedRequestData], + ) -> list[BatchRegistrationResult]: + # Flatten all license_terms_data from aggregated requests into a single list + all_license_terms_data = [ + license_terms_data + for value in aggregated_requests.values() + for license_terms_data in value["license_terms_data"] + ] + print("all_license_terms_data", all_license_terms_data) + # Populate license terms ids for each registered IP + license_terms_index = 0 + for registration_result in registration_results: + for registered_ip in registration_result["registered_ips"]: + if license_terms_index < len(all_license_terms_data): + license_terms_data = all_license_terms_data[license_terms_index] + if license_terms_data: + registered_ip["license_terms_ids"] = self._get_license_terms_id( + license_terms_data + ) + license_terms_index += 1 + return registration_results + + def _get_license_terms_id(self, license_terms_data: list[dict]) -> list[int]: + """ + Get the license terms ids from the license terms data. + :param license_terms_data: The license terms data. + :return: The license terms ids. + """ + license_terms_ids = [] + for license_terms in license_terms_data: + license_terms_id = self.pi_license_template_client.getLicenseTermsId( + license_terms["terms"] + ) + license_terms_ids.append(license_terms_id) + return license_terms_ids + def _handle_minted_nft_registration( self, nft: MintedNFT, diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 9dd322ff..124fed46 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -324,6 +324,18 @@ class IPRoyaltyVault(TypedDict): royalty_vault: Address +class RegisteredIPWithLicenseTermsIds(RegisteredIP): + """ + Data structure for IP and token ID with license terms IDs. + + Attributes: + license_terms_ids: The license terms IDs of the registered IP asset. + If the license terms are not attached, the value is None. + """ + + license_terms_ids: list[int] | None + + class BatchRegistrationResult(TypedDict, total=False): """ Result of a single batch registration transaction. @@ -335,7 +347,7 @@ class BatchRegistrationResult(TypedDict, total=False): """ tx_hash: HexStr - registered_ips: list[RegisteredIP] + registered_ips: list[RegisteredIPWithLicenseTermsIds] ip_royalty_vaults: list[IPRoyaltyVault] @@ -365,6 +377,7 @@ class ExtraData(TypedDict, total=False): royalty_total_amount: [Optional] The total amount of royalty tokens to distribute. nft_contract: [Optional] The NFT contract address. token_id: [Optional] The token ID. + license_terms_data: [Optional] The license terms data. """ royalty_shares: list[RoyaltyShareInput] @@ -372,6 +385,7 @@ class ExtraData(TypedDict, total=False): royalty_total_amount: int nft_contract: Address token_id: int + license_terms_data: list[dict] | None = None @dataclass diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index ada86b43..62f21bcb 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -30,6 +30,7 @@ class AggregatedRequestData(TypedDict): """Aggregated request data structure.""" call_data: list[bytes | Multicall3Call] + license_terms_data: list[list[dict]] method_reference: Callable[[list[bytes], dict], HexStr] @@ -71,6 +72,7 @@ def aggregate_multicall_requests( if target_address not in aggregated_requests: aggregated_requests[target_address] = { "call_data": [], + "license_terms_data": [], "method_reference": ( multicall3_client.build_aggregate3_transaction if target_address == multicall3_client.contract.address @@ -81,7 +83,7 @@ def aggregate_multicall_requests( aggregated_requests[target_address]["call_data"].append( { "target": request.workflow_address, - "allowFailure": True, + "allowFailure": False, "value": 0, "callData": request.encoded_tx_data, } @@ -90,6 +92,14 @@ def aggregate_multicall_requests( aggregated_requests[target_address]["call_data"].append( request.encoded_tx_data ) + license_terms_data = ( + request.extra_data.get("license_terms_data") or [] + if request.extra_data is not None + else [] + ) + aggregated_requests[target_address]["license_terms_data"].append( + license_terms_data + ) return aggregated_requests @@ -153,7 +163,7 @@ def send_transactions( web3: Web3, account: LocalAccount, tx_options: dict | None = None, -) -> list[dict[str, HexStr | dict]]: +) -> tuple[list[dict[str, HexStr | dict]], dict[Address, AggregatedRequestData]]: aggregated_requests: dict[Address, AggregatedRequestData] = ( aggregate_multicall_requests( requests=transformed_requests, @@ -176,4 +186,4 @@ def send_transactions( "tx_receipt": response["tx_receipt"], } ) - return tx_results + return tx_results, aggregated_requests diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 9f8a74b4..37659603 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -325,7 +325,6 @@ def _handle_mint_and_register_request( license_terms_data=license_terms_data, royalty_shares=royalty_shares, allow_duplicates=request.allow_duplicates, - is_public_minting=is_public_minting, ) elif deriv_data and royalty_shares: @@ -374,7 +373,6 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( license_terms_data: list[dict], royalty_shares: list[dict], allow_duplicates: bool | None, - is_public_minting: bool, ) -> TransformedRegistrationRequest: """Handle mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens.""" royalty_token_distribution_workflows_client = ( @@ -406,7 +404,9 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( workflow_address=royalty_token_distribution_workflows_address, original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, validated_request=validated_request, - extra_data=None, + extra_data=ExtraData( + license_terms_data=license_terms_data, + ), ) @@ -485,7 +485,9 @@ def _handle_mint_and_register_with_license_terms( is_use_multicall3=is_public_minting, workflow_address=license_attachment_workflows_address, validated_request=validated_request, - extra_data=None, + extra_data=ExtraData( + license_terms_data=license_terms_data, + ), original_method_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -714,6 +716,7 @@ def _handle_register_with_license_terms_and_royalty_vault( royalty_total_amount=royalty_total_amount, nft_contract=nft_contract, token_id=token_id, + license_terms_data=license_terms_data, ), original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -838,7 +841,9 @@ def _handle_register_with_license_terms( is_use_multicall3=False, workflow_address=license_attachment_workflows_address, validated_request=validated_request, - extra_data=None, + extra_data=ExtraData( + license_terms_data=license_terms_data, + ), original_method_reference=license_attachment_workflows_client.build_multicall_transaction, ) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 44f646fd..26242925 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -1997,6 +1997,7 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe RoyaltyShareInput(recipient=account_2.address, percentage=40.0), ], ), + # Does not support the multicall3 MintAndRegisterRequest( spg_nft_contract=nft_collection, license_terms_data=[ @@ -2024,6 +2025,15 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe ], ), ] + # Enhanced: Thoroughly verify transaction aggregation, registration output, + # and cross-check the actual on-chain registered asset state with expectations. + # + # Expectations: + # - 3 total blockchain transactions by multicall3: + # 1. LicenseAttachmentWorkflowsClient: attaches license terms (1 tx) + # 2. RoyaltyTokenDistributionWorkflowsClient: batch of 3, merged to 1 tx (with vault creation) + # 3. DerivativeWorkflowsClient: creates derivatives (1 tx) + # - Only 1 distribute_royalty_tokens_tx_hash, even for multiple assets with royalty shares. response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( requests=requests, tx_options={ From c75775ec53374a3671cf833cc87c415e43ef22ee Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 23 Jan 2026 16:47:29 +0800 Subject: [PATCH 34/52] test: add test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_register_registration_request --- .../resources/IPAsset.py | 2 - .../types/resource/IPAsset.py | 4 +- .../integration/test_integration_ip_asset.py | 498 ++++++++++++++++-- 3 files changed, 464 insertions(+), 40 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index bb67b63b..39598a35 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1591,7 +1591,6 @@ def batch_ip_asset_with_optimized_workflows( if tr.extra_data is not None and tr.extra_data.get("royalty_shares", None) is not None ] - # Parse the response of the registration requests and collect distribute royalty tokens requests response_list: list[BatchRegistrationResult] = [] for tx_response in tx_responses: @@ -1669,7 +1668,6 @@ def _populate_license_terms_ids_into_response( for value in aggregated_requests.values() for license_terms_data in value["license_terms_data"] ] - print("all_license_terms_data", all_license_terms_data) # Populate license terms ids for each registered IP license_terms_index = 0 for registration_result in registration_results: diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 124fed46..4c0533e5 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -333,7 +333,7 @@ class RegisteredIPWithLicenseTermsIds(RegisteredIP): If the license terms are not attached, the value is None. """ - license_terms_ids: list[int] | None + license_terms_ids: list[int] class BatchRegistrationResult(TypedDict, total=False): @@ -385,7 +385,7 @@ class ExtraData(TypedDict, total=False): royalty_total_amount: int nft_contract: Address token_id: int - license_terms_data: list[dict] | None = None + license_terms_data: list[dict] | None @dataclass diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 26242925..9775c135 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -8,6 +8,7 @@ BatchMintAndRegisterIPInput, DerivativeDataInput, IPMetadataInput, + IpRegistrationWorkflowRequest, LicenseTermsDataInput, LicenseTermsInput, LicenseTermsOverride, @@ -55,6 +56,34 @@ ) +@pytest.fixture(scope="module") +def public_nft_collection(story_client: StoryClient): + tx_data = story_client.NFTClient.create_nft_collection( + name="test-public-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 private_nft_collection(story_client: StoryClient): + tx_data = story_client.NFTClient.create_nft_collection( + name="test-private-collection", + symbol="TEST", + max_supply=100, + is_public_minting=False, + mint_open=True, + contract_uri="test-uri", + mint_fee_recipient=account.address, + ) + return tx_data["nft_contract"] + + class TestIPAssetRegistration: @pytest.fixture(scope="module") def child_ip_id(self, story_client: StoryClient): @@ -310,19 +339,6 @@ def test_register_ip_and_make_derivative_with_license_tokens_with_metadata( class TestIPAssetMinting: - @pytest.fixture(scope="module") - def nft_collection(self, story_client: StoryClient): - tx_data = story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - max_supply=100, - is_public_minting=True, - mint_open=True, - contract_uri="test-uri", - mint_fee_recipient=account.address, - ) - return tx_data["nft_contract"] - def test_mint_register_attach_terms( self, story_client: StoryClient, nft_collection ): @@ -1910,8 +1926,11 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_register_registr # # Expectations: # - 3 total blockchain transactions (aggregated by workflow): - # 1. LicenseAttachmentWorkflowsClient: attaches license terms (1 tx) + # 1. LicenseAttachmentWorkflowsClient: attaches license terms (1 tx) +1 license terms id # 2. RoyaltyTokenDistributionWorkflowsClient: batch of 3, merged to 1 tx (with vault creation) + # 2.1: 2 license terms id + # 2.2: 0 license terms id + # 2.3: 1 license terms id # 3. DerivativeWorkflowsClient: creates derivatives (1 tx) # - Only 1 distribute_royalty_tokens_tx_hash, even for multiple assets with royalty shares. response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( @@ -1921,33 +1940,50 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_register_registr assert isinstance(response, dict) assert "registration_results" in response assert "distribute_royalty_tokens_tx_hashes" in response - registration_results = response["registration_results"] assert registration_results[0]["tx_hash"] is not None assert len(registration_results[0]["registered_ips"]) == 1 + assert ( + len(registration_results[0]["registered_ips"][0]["license_terms_ids"]) == 1 + ) assert len(registration_results[0]["ip_royalty_vaults"]) == 0 + assert registration_results[1]["tx_hash"] is not None assert len(registration_results[1]["registered_ips"]) == 3 + assert ( + len(registration_results[1]["registered_ips"][0]["license_terms_ids"]) == 2 + ) + assert ( + len(registration_results[1]["registered_ips"][1]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[1]["registered_ips"][2]["license_terms_ids"]) == 1 + ) assert len(registration_results[1]["ip_royalty_vaults"]) == 3 + assert registration_results[2]["tx_hash"] is not None assert len(registration_results[2]["registered_ips"]) == 1 + assert ( + len(registration_results[2]["registered_ips"][0]["license_terms_ids"]) == 0 + ) assert len(registration_results[2]["ip_royalty_vaults"]) == 0 def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_register_ip_request( self, story_client: StoryClient, - nft_collection, + public_nft_collection, + private_nft_collection, ): """Test batch register IP assets with optimized workflows with mint and register IP request.""" parent_ip_and_license_terms_1 = create_parent_ip_and_license_terms( - story_client, nft_collection, account + story_client, public_nft_collection, account ) parent_ip_and_license_terms_2 = create_parent_ip_and_license_terms( - story_client, nft_collection, account + story_client, private_nft_collection, account ) requests = [ MintAndRegisterRequest( - spg_nft_contract=nft_collection, + spg_nft_contract=public_nft_collection, recipient=account.address, allow_duplicates=True, ip_metadata=COMMON_IP_METADATA, @@ -1972,7 +2008,7 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe ], ), MintAndRegisterRequest( - spg_nft_contract=nft_collection, + spg_nft_contract=public_nft_collection, recipient=account.address, allow_duplicates=True, deriv_data=DerivativeDataInput( @@ -1983,7 +2019,7 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe ), ), MintAndRegisterRequest( - spg_nft_contract=nft_collection, + spg_nft_contract=public_nft_collection, recipient=account.address, allow_duplicates=True, deriv_data=DerivativeDataInput( @@ -1997,9 +2033,9 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe RoyaltyShareInput(recipient=account_2.address, percentage=40.0), ], ), - # Does not support the multicall3 + # public minting + royalty_token_distribution_workflows_client+ workflow_multicall MintAndRegisterRequest( - spg_nft_contract=nft_collection, + spg_nft_contract=public_nft_collection, license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.commercial_use( @@ -2024,31 +2060,421 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe RoyaltyShareInput(recipient=account_2.address, percentage=50.0), ], ), + # private minting + license_attachment_workflows_client + MintAndRegisterRequest( + spg_nft_contract=private_nft_collection, + allow_duplicates=True, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=10, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + LicenseTermsDataInput( + terms=PILFlavor.non_commercial_social_remixing(), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=0, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + ), + # private minting + royalty_token_distribution_workflows_client + MintAndRegisterRequest( + spg_nft_contract=private_nft_collection, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=20, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=20, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # # private minting + royalty_token_distribution_workflows_client + MintAndRegisterRequest( + spg_nft_contract=private_nft_collection, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # private minting + derivative_workflows_client + MintAndRegisterRequest( + spg_nft_contract=private_nft_collection, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + ), ] # Enhanced: Thoroughly verify transaction aggregation, registration output, # and cross-check the actual on-chain registered asset state with expectations. # + # Enhanced verification of batch registration logic and on-chain state: + # # Expectations: - # - 3 total blockchain transactions by multicall3: - # 1. LicenseAttachmentWorkflowsClient: attaches license terms (1 tx) - # 2. RoyaltyTokenDistributionWorkflowsClient: batch of 3, merged to 1 tx (with vault creation) - # 3. DerivativeWorkflowsClient: creates derivatives (1 tx) - # - Only 1 distribute_royalty_tokens_tx_hash, even for multiple assets with royalty shares. + # - 4 total blockchain transactions: + # 1. Multicall3: 3 ip_ids + # 1.1: 1 license_terms_id + # 2. royalty_token_distribution_workflows_client: 3 ip_ids + # 2.1: 1 license_terms_id + # 2.2: 1 license_terms_id + # 2.3: + # 3. license_attachment_workflows_client 1 ip_id + # 2.1: 2 license_terms_id + # 4. derivative_workflows_client 1 ip_id + # + # --- Enhanced assertions and on-chain checks follow below --- response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( requests=requests, - tx_options={ - "gas": 16777216, - }, ) - print( - "-------------------------------- Batch Register IP Assets With Optimized Workflows Response --------------------------------" + assert isinstance(response, dict) + assert "registration_results" in response + assert "distribute_royalty_tokens_tx_hashes" in response + + registration_results = response["registration_results"] + assert registration_results[0]["tx_hash"] is not None + assert len(registration_results[0]["registered_ips"]) == 3 + assert ( + len(registration_results[0]["registered_ips"][0]["license_terms_ids"]) == 1 + ) + assert ( + len(registration_results[0]["registered_ips"][1]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[0]["registered_ips"][2]["license_terms_ids"]) == 0 + ) + + assert len(registration_results[1]["registered_ips"]) == 3 + assert ( + len(registration_results[1]["registered_ips"][0]["license_terms_ids"]) == 1 + ) + assert ( + len(registration_results[1]["registered_ips"][1]["license_terms_ids"]) == 1 ) - print(response) + assert ( + len(registration_results[1]["registered_ips"][2]["license_terms_ids"]) == 0 + ) + + assert len(registration_results[2]["registered_ips"]) == 1 + assert ( + len(registration_results[2]["registered_ips"][0]["license_terms_ids"]) == 2 + ) + + assert len(registration_results[3]["registered_ips"]) == 1 + assert ( + len(registration_results[3]["registered_ips"][0]["license_terms_ids"]) == 0 + ) + + assert len(response["distribute_royalty_tokens_tx_hashes"]) == 0 + + def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_register_registration_request( + self, + story_client: StoryClient, + public_nft_collection, + private_nft_collection, + ): + """Test batch register IP assets with optimized workflows with mint and register registration request.""" + parent_ip_and_license_terms_1 = create_parent_ip_and_license_terms( + story_client, public_nft_collection, account + ) + parent_ip_and_license_terms_2 = create_parent_ip_and_license_terms( + story_client, private_nft_collection, account + ) + token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_2 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_3 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_4 = get_token_id(MockERC721, story_client.web3, story_client.account) + requests: list[IpRegistrationWorkflowRequest] = [ + # derivative_workflows_client+ workflow_multicall + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_1, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + ), + # royalty_token_distribution_workflows_client+ workflow_multicall+distribute royalty tokens + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_2, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # public minting + royalty_token_distribution_workflows_client + workflow_multicall + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + ip_metadata=COMMON_IP_METADATA, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=1000000000000000000, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=1000000000000000000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # multicall3 + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + ip_metadata=COMMON_IP_METADATA, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.non_commercial_social_remixing(), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=0, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=10, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=10, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + ), + # royalty_token_distribution_workflows_client+ workflow_multicall + MintAndRegisterRequest( + spg_nft_contract=private_nft_collection, + recipient=account.address, + allow_duplicates=True, + ip_metadata=COMMON_IP_METADATA, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=30, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=30, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # public minting + multicall3 + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # private minting + royalty_token_distribution_workflows_client + workflow_multicall + MintAndRegisterRequest( + spg_nft_contract=private_nft_collection, + recipient=account.address, + allow_duplicates=True, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + # derivative_workflows_client+ workflow_multicall + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_3, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + ), + # royalty_token_distribution_workflows_client+ workflow_multicall+distribute royalty tokens + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_4, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + ] + + # Enhanced: Thoroughly verify transaction aggregation, registration output, + # and cross-check the actual on-chain registered asset state with expectations. + # + # Enhanced verification of batch registration logic and on-chain state: + # + # Expectations: + # - 3 total blockchain transactions: + # 1. derivative_workflows_client: 2 ip_ids + # 2. royalty_token_distribution_workflows_client: 5 ip_ids+ 2 royalty vaults + # 2.1: distribute royalty tokens + # 2.2: 1 license_terms_id + # 2.3: 1 license_terms_id + # 2.4: + # 2.5: distribute royalty tokens + # 3. multicall3 2 ip_ids + # 2.1: 2 license_terms_id + # + response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( + requests=requests, + ) + assert isinstance(response, dict) assert "registration_results" in response assert "distribute_royalty_tokens_tx_hashes" in response registration_results = response["registration_results"] assert registration_results[0]["tx_hash"] is not None - assert len(registration_results[0]["registered_ips"]) == 1 - assert len(registration_results[0]["ip_royalty_vaults"]) == 0 + assert len(registration_results[0]["registered_ips"]) == 2 + assert ( + len(registration_results[0]["registered_ips"][0]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[0]["registered_ips"][1]["license_terms_ids"]) == 0 + ) + + assert registration_results[1]["tx_hash"] is not None + assert len(registration_results[1]["registered_ips"]) == 5 + assert ( + len(registration_results[1]["registered_ips"][0]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[1]["registered_ips"][1]["license_terms_ids"]) == 1 + ) + assert ( + len(registration_results[1]["registered_ips"][2]["license_terms_ids"]) == 1 + ) + assert ( + len(registration_results[1]["registered_ips"][3]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[1]["registered_ips"][4]["license_terms_ids"]) == 0 + ) + + assert len(registration_results[1]["ip_royalty_vaults"]) == 2 + + assert registration_results[2]["tx_hash"] is not None + assert len(registration_results[2]["registered_ips"]) == 2 + assert ( + len(registration_results[2]["registered_ips"][0]["license_terms_ids"]) == 2 + ) + assert ( + len(registration_results[2]["registered_ips"][1]["license_terms_ids"]) == 0 + ) + + assert len(response["distribute_royalty_tokens_tx_hashes"]) == 1 From 1dad61151dcf88fa6f563b2a1a317c8eb3a73b9b Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 23 Jan 2026 17:37:23 +0800 Subject: [PATCH 35/52] test: add test_batch_register_ip_assets_with_optimized_workflows_without_multicall to validate IP asset registration without multicall3 --- .../integration/test_integration_ip_asset.py | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 9775c135..ee1f6134 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -2478,3 +2478,227 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe ) assert len(response["distribute_royalty_tokens_tx_hashes"]) == 1 + + def test_batch_register_ip_assets_with_optimized_workflows_without_multicall( + self, + story_client: StoryClient, + public_nft_collection, + private_nft_collection, + ): + """Test batch register IP assets with optimized workflows without using multicall3.""" + # Create parent IP assets for derivative tests + parent_ip_and_license_terms_1 = create_parent_ip_and_license_terms( + story_client, public_nft_collection, account + ) + parent_ip_and_license_terms_2 = create_parent_ip_and_license_terms( + story_client, private_nft_collection, account + ) + + # Create token IDs for RegisterRegistrationRequest + token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_2 = get_token_id(MockERC721, story_client.web3, story_client.account) + token_id_3 = get_token_id(MockERC721, story_client.web3, story_client.account) + + requests: list[IpRegistrationWorkflowRequest] = [ + # MintAndRegisterRequest with license terms data - LicenseAttachmentWorkflows + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + ip_metadata=COMMON_IP_METADATA, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=1000000000000000000, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=1000000000000000000, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + ), + # MintAndRegisterRequest with derivative data - DerivativeWorkflows + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + ), + # MintAndRegisterRequest with license terms data + royalty shares - RoyaltyTokenDistributionWorkflows + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=20, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=20, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=70.0), + RoyaltyShareInput(recipient=account_2.address, percentage=30.0), + ], + ), + # MintAndRegisterRequest with derivative data + royalty shares - RoyaltyTokenDistributionWorkflows + MintAndRegisterRequest( + spg_nft_contract=public_nft_collection, + recipient=account.address, + allow_duplicates=True, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_2["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_2["license_terms_id"] + ], + ), + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=60.0), + RoyaltyShareInput(recipient=account_2.address, percentage=40.0), + ], + ), + # RegisterRegistrationRequest with license terms data - LicenseAttachmentWorkflows + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_1, + ip_metadata=COMMON_IP_METADATA, + deadline=100000, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.non_commercial_social_remixing(), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=0, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=0, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + ), + # RegisterRegistrationRequest with derivative data - DerivativeWorkflows + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_2, + deriv_data=DerivativeDataInput( + parent_ip_ids=[parent_ip_and_license_terms_1["parent_ip_id"]], + license_terms_ids=[ + parent_ip_and_license_terms_1["license_terms_id"] + ], + ), + ), + # RegisterRegistrationRequest with license terms data + royalty shares - RoyaltyTokenDistributionWorkflows + RegisterRegistrationRequest( + nft_contract=MockERC721, + token_id=token_id_3, + license_terms_data=[ + LicenseTermsDataInput( + terms=PILFlavor.commercial_use( + default_minting_fee=50, + currency=MockERC20, + royalty_policy=NativeRoyaltyPolicy.LAP, + ), + licensing_config=LicensingConfig( + is_set=True, + minting_fee=50, + licensing_hook=ZERO_ADDRESS, + hook_data=ZERO_HASH, + commercial_rev_share=50, + disabled=False, + expect_minimum_group_reward_share=0, + expect_group_reward_pool=ZERO_ADDRESS, + ), + ), + ], + royalty_shares=[ + RoyaltyShareInput(recipient=account.address, percentage=50.0), + RoyaltyShareInput(recipient=account_2.address, percentage=50.0), + ], + ), + ] + + # Test batch register IP assets with optimized workflows without using multicall3. + # Expectations: + # - 3 total blockchain transactions: + # 1. LicenseAttachmentWorkflows: 2 ip_ids + # 1.1: 1 license_terms_id + # 1.2: 1 license_terms_id + # 2. DerivativeWorkflows: 2 ip_ids + # 3. RoyaltyTokenDistributionWorkflows: 3 ip_ids + 1 royalty vaults + # 3.1: 1 license_terms_id + # 3.2: + # 3.3: 1 license_terms_id+distribute royalty tokens + response = story_client.IPAsset.batch_ip_asset_with_optimized_workflows( + requests=requests, + is_use_multicall=False, + ) + # Verify response structure + assert isinstance(response, dict) + assert "registration_results" in response + assert "distribute_royalty_tokens_tx_hashes" in response + + registration_results = response["registration_results"] + assert len(registration_results) == 3 + assert registration_results[0]["tx_hash"] is not None + assert len(registration_results[0]["registered_ips"]) == 2 + assert ( + len(registration_results[0]["registered_ips"][0]["license_terms_ids"]) == 1 + ) + assert ( + len(registration_results[0]["registered_ips"][1]["license_terms_ids"]) == 1 + ) + assert len(registration_results[0]["ip_royalty_vaults"]) == 0 + + assert registration_results[1]["tx_hash"] is not None + assert len(registration_results[1]["registered_ips"]) == 2 + assert ( + len(registration_results[1]["registered_ips"][0]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[1]["registered_ips"][1]["license_terms_ids"]) == 0 + ) + assert len(registration_results[1]["ip_royalty_vaults"]) == 0 + + assert registration_results[2]["tx_hash"] is not None + assert len(registration_results[2]["registered_ips"]) == 3 + assert ( + len(registration_results[2]["registered_ips"][0]["license_terms_ids"]) == 1 + ) + assert ( + len(registration_results[2]["registered_ips"][1]["license_terms_ids"]) == 0 + ) + assert ( + len(registration_results[2]["registered_ips"][2]["license_terms_ids"]) == 1 + ) + assert len(registration_results[2]["ip_royalty_vaults"]) == 1 + + assert len(response["distribute_royalty_tokens_tx_hashes"]) == 1 From 5156f3a829331d117bf37a4cbbe7a1ef78de508c Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 13:53:50 +0800 Subject: [PATCH 36/52] test: update registration utility tests to improve mocking and add new validation tests for license terms --- tests/unit/utils/test_registration_utils.py | 33 --- .../test_transform_registration_request.py | 194 ++++++++++++++++-- 2 files changed, 177 insertions(+), 50 deletions(-) diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index 3d64a517..c55ab80b 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -19,39 +19,6 @@ ) -@pytest.fixture -def mock_spg_nft_client(): - """Mock SPGNFTImplClient.""" - - def _mock(public_minting: bool = True): - return patch( - "story_protocol_python_sdk.utils.registration.registration_utils.SPGNFTImplClient", - return_value=MagicMock( - publicMinting=MagicMock(return_value=public_minting) - ), - ) - - return _mock - - -@pytest.fixture -def mock_royalty_module_client(): - """Mock RoyaltyModuleClient.""" - - def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True): - return patch( - "story_protocol_python_sdk.utils.registration.registration_utils.RoyaltyModuleClient", - return_value=MagicMock( - isWhitelistedRoyaltyPolicy=MagicMock( - return_value=is_whitelisted_policy - ), - isWhitelistedRoyaltyToken=MagicMock(return_value=is_whitelisted_token), - ), - ) - - return _mock - - @pytest.fixture def mock_module_registry_client(): """Mock ModuleRegistryClient.""" diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index aed1d8b0..225ad091 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -1,3 +1,4 @@ +from dataclasses import asdict, replace from unittest.mock import MagicMock, patch import pytest @@ -22,7 +23,9 @@ from story_protocol_python_sdk.utils.ip_metadata import IPMetadata from story_protocol_python_sdk.utils.registration.transform_registration_request import ( get_allow_duplicates, + get_public_minting, transform_request, + validate_license_terms_data, ) from tests.unit.fixtures.data import ( ADDRESS, @@ -40,7 +43,7 @@ def mock_get_public_minting(): def _mock(public_minting: bool = True): return patch( - "story_protocol_python_sdk.utils.registration.registration_utils.SPGNFTImplClient", + "story_protocol_python_sdk.utils.registration.transform_registration_request.SPGNFTImplClient", return_value=MagicMock( publicMinting=MagicMock(return_value=public_minting) ), @@ -55,7 +58,7 @@ def mock_royalty_module_client(): def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True): return patch( - "story_protocol_python_sdk.utils.registration.registration_utils.RoyaltyModuleClient", + "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyModuleClient", return_value=MagicMock( isWhitelistedRoyaltyPolicy=MagicMock( return_value=is_whitelisted_policy @@ -71,7 +74,7 @@ def _mock(is_whitelisted_policy: bool = True, is_whitelisted_token: bool = True) def mock_module_registry_client(): """Mock ModuleRegistryClient for validate_license_terms_data.""" return patch( - "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", + "story_protocol_python_sdk.utils.registration.transform_registration_request.ModuleRegistryClient", return_value=MagicMock(), ) @@ -105,6 +108,24 @@ def _mock(is_registered: bool = True): return _mock +@pytest.fixture +def mock_license_registry_client(): + """Mock LicenseRegistryClient for DerivativeData.""" + + def _mock(has_attached_license_terms: bool = True, royalty_percent: int = 1000000): + return patch( + "story_protocol_python_sdk.utils.derivative_data.LicenseRegistryClient", + return_value=MagicMock( + hasIpAttachedLicenseTerms=MagicMock( + return_value=has_attached_license_terms + ), + getRoyaltyPercent=MagicMock(return_value=royalty_percent), + ), + ) + + return _mock + + @pytest.fixture def mock_workflow_clients(mock_web3): """Mock workflow clients (RoyaltyTokenDistributionWorkflowsClient, LicenseAttachmentWorkflowsClient, DerivativeWorkflowsClient). @@ -219,6 +240,21 @@ def _mock(): return _mock +@pytest.fixture +def mock_spg_nft_client(): + """Mock SPGNFTImplClient.""" + + def _mock(public_minting: bool = True): + return patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.SPGNFTImplClient", + return_value=MagicMock( + publicMinting=MagicMock(return_value=public_minting) + ), + ) + + return _mock + + account = Account.from_key( "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" ) @@ -258,6 +294,118 @@ def test_returns_provided_value_when_not_none(self): assert result is True +class TestGetPublicMinting: + def test_returns_true_when_public_minting_enabled( + self, mock_web3, mock_spg_nft_client + ): + with mock_spg_nft_client(public_minting=True): + result = get_public_minting(ADDRESS, mock_web3) + assert result is True + + def test_returns_false_when_public_minting_disabled( + self, mock_web3, mock_spg_nft_client + ): + with mock_spg_nft_client(public_minting=False): + result = get_public_minting(ADDRESS, mock_web3) + assert result is False + + def test_throws_error_when_spg_nft_contract_invalid(self, mock_web3): + with pytest.raises(Exception): + get_public_minting("invalid_address", mock_web3) + + +class TestValidateLicenseTermsData: + def test_validates_license_terms_with_dataclass_input( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(), + mock_module_registry_client, + ): + result = validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) + assert isinstance(result, list) + assert len(result) == len(LICENSE_TERMS_DATA) + assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE + + def test_validates_license_terms_with_dict_input( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(), + mock_module_registry_client, + ): + result = validate_license_terms_data( + [ + { + "terms": asdict(LICENSE_TERMS_DATA[0].terms), + "licensing_config": LICENSE_TERMS_DATA[0].licensing_config, + } + ], + mock_web3, + ) + assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE + + def test_throws_error_when_royalty_policy_not_whitelisted( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(is_whitelisted_policy=False), + mock_module_registry_client, + pytest.raises(ValueError, match="The royalty_policy is not whitelisted."), + ): + validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) + + def test_throws_error_when_currency_not_whitelisted( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + with ( + mock_royalty_module_client(is_whitelisted_token=False), + mock_module_registry_client, + pytest.raises(ValueError, match="The currency is not whitelisted."), + ): + validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) + + def test_validates_multiple_license_terms( + self, + mock_web3, + mock_royalty_module_client, + mock_module_registry_client, + ): + # Use LICENSE_TERMS_DATA twice to test multiple terms + license_terms_data = LICENSE_TERMS_DATA + [ + replace( + LICENSE_TERMS_DATA[0], + terms=replace(LICENSE_TERMS_DATA[0].terms, commercial_rev_share=20), + ) + ] + + with ( + mock_royalty_module_client(), + mock_module_registry_client, + ): + result = validate_license_terms_data(license_terms_data, mock_web3) + assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE + assert result[1] == { + "terms": { + **LICENSE_TERMS_DATA_CAMEL_CASE["terms"], + "commercialRevShare": 20 * 10**6, + }, + "licensingConfig": LICENSE_TERMS_DATA_CAMEL_CASE["licensingConfig"], + } + + class TestTransformRegistrationRequest: def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_present( self, @@ -300,8 +448,11 @@ def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_pres assert args[4] is False # allow_duplicates (default for this method) assert result.workflow_address == "license_attachment_client_address" assert result.is_use_multicall3 is True - assert result.extra_data is None - assert result.contract_call is not None + assert result.extra_data is not None + license_terms_data = result.extra_data.get("license_terms_data") + assert license_terms_data is not None + assert license_terms_data[0] == LICENSE_TERMS_DATA_CAMEL_CASE + assert result.original_method_reference is not None def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_id_present( self, @@ -351,9 +502,12 @@ def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_ assert args[4]["signer"] == ACCOUNT_ADDRESS # signature data assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.extra_data is None + assert result.extra_data is not None + license_terms_data = result.extra_data.get("license_terms_data") + assert license_terms_data is not None + assert license_terms_data[0] == LICENSE_TERMS_DATA_CAMEL_CASE assert result.is_use_multicall3 is False - assert result.contract_call is not None + assert result.original_method_reference is not None def test_raises_error_for_invalid_request_type( self, mock_web3, mock_ip_asset_registry_client @@ -413,11 +567,14 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens royalty_token_distribution_client.contract.encode_abi.assert_called_once() call_args = royalty_token_distribution_client.contract.encode_abi.call_args - assert result.is_use_multicall3 is True + assert result.is_use_multicall3 is False assert ( result.workflow_address == "royalty_token_distribution_client_address" ) - assert result.extra_data is None + assert result.extra_data is not None + license_terms_data = result.extra_data.get("license_terms_data") + assert license_terms_data is not None + assert license_terms_data[0] == LICENSE_TERMS_DATA_CAMEL_CASE # Verify encode_abi was called with correct method and arguments assert call_args[1]["abi_element_identifier"] == ( "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" @@ -435,7 +592,7 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens assert args[4][0]["recipient"] == ADDRESS assert args[4][0]["percentage"] == 50 * 10**6 assert args[5] is True # allow_duplicates (default for this method) - assert result.contract_call is not None + assert result.original_method_reference is not None def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( self, @@ -490,7 +647,7 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( assert args[4][0]["recipient"] == ADDRESS # royalty_shares assert args[4][0]["percentage"] == 50 * 10**6 # royalty_shares assert args[5] is True # allow_duplicates (default for this method) - assert result.contract_call is not None + assert result.original_method_reference is not None def test_mint_and_register_ip_and_make_derivative( self, @@ -540,7 +697,7 @@ def test_mint_and_register_ip_and_make_derivative( assert ( call_args[1]["args"][4] is False ) # allow_duplicates (default for this method) - assert result.contract_call is not None + assert result.original_method_reference is not None def test_raises_error_for_invalid_mint_and_register_request_type( self, @@ -633,7 +790,7 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( assert royalty_share_dict["recipient"] == ADDRESS assert royalty_share_dict["percentage"] == 50 * 10**6 assert result.extra_data["deadline"] == 2000 - assert result.contract_call is not None + assert result.original_method_reference is not None def test_register_ip_and_make_derivative_and_deploy_royalty_vault( self, @@ -698,7 +855,7 @@ def test_register_ip_and_make_derivative_and_deploy_royalty_vault( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.contract_call is not None + assert result.original_method_reference is not None def test_register_ip_and_attach_pil_terms( self, @@ -738,7 +895,10 @@ def test_register_ip_and_attach_pil_terms( call_args = license_attachment_client.contract.encode_abi.call_args assert result.is_use_multicall3 is False assert result.workflow_address == "license_attachment_client_address" - assert result.extra_data is None + assert result.extra_data is not None + license_terms_data = result.extra_data.get("license_terms_data") + assert license_terms_data is not None + assert license_terms_data[0] == LICENSE_TERMS_DATA_CAMEL_CASE assert call_args[1]["abi_element_identifier"] == ( "registerIpAndAttachPILTerms" ) @@ -753,7 +913,7 @@ def test_register_ip_and_attach_pil_terms( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.contract_call is not None + assert result.original_method_reference is not None def test_register_ip_and_make_derivative( self, @@ -811,7 +971,7 @@ def test_register_ip_and_make_derivative( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.contract_call is not None + assert result.original_method_reference is not None def test_raises_error_when_ip_not_registered( self, From da4a5e194aa41d055a73bd36d80e77fa3e752d4a Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 14:59:38 +0800 Subject: [PATCH 37/52] test:enhance the aggregate_multicall_requests method tests --- .../types/resource/IPAsset.py | 1 + tests/unit/utils/test_registration_utils.py | 734 +++++++++--------- 2 files changed, 357 insertions(+), 378 deletions(-) diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 4c0533e5..6c8281df 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -406,5 +406,6 @@ class TransformedRegistrationRequest: is_use_multicall3: bool workflow_address: Address validated_request: list[Address | int | str | bytes | dict | bool] + # TODO: need to rename with multicall3 method reference original_method_reference: Callable[..., HexStr] extra_data: ExtraData | None = None diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index c55ab80b..74aa0ea0 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -1,416 +1,394 @@ -from dataclasses import asdict, replace from unittest.mock import MagicMock, patch import pytest from ens.ens import HexStr from story_protocol_python_sdk.types.resource.IPAsset import ( + ExtraData, TransformedRegistrationRequest, ) +from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.registration.registration_utils import ( aggregate_multicall_requests, - get_public_minting, - validate_license_terms_data, -) -from tests.unit.fixtures.data import ( - ADDRESS, - LICENSE_TERMS_DATA, - LICENSE_TERMS_DATA_CAMEL_CASE, ) +from tests.unit.fixtures.data import ADDRESS, LICENSE_TERMS_DATA_CAMEL_CASE @pytest.fixture -def mock_module_registry_client(): - """Mock ModuleRegistryClient.""" - return patch( - "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", - return_value=MagicMock(), - ) - - -class TestGetPublicMinting: - def test_returns_true_when_public_minting_enabled( - self, mock_web3, mock_spg_nft_client - ): - with mock_spg_nft_client(public_minting=True): - result = get_public_minting(ADDRESS, mock_web3) - assert result is True - - def test_returns_false_when_public_minting_disabled( - self, mock_web3, mock_spg_nft_client - ): - with mock_spg_nft_client(public_minting=False): - result = get_public_minting(ADDRESS, mock_web3) - assert result is False - - def test_throws_error_when_spg_nft_contract_invalid(self, mock_web3): - with pytest.raises(Exception): - get_public_minting("invalid_address", mock_web3) - - -class TestValidateLicenseTermsData: - def test_validates_license_terms_with_dataclass_input( - self, - mock_web3, - mock_royalty_module_client, - mock_module_registry_client, - ): - with ( - mock_royalty_module_client(), - mock_module_registry_client, - ): - result = validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) - assert isinstance(result, list) - assert len(result) == len(LICENSE_TERMS_DATA) - assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE - - def test_validates_license_terms_with_dict_input( - self, - mock_web3, - mock_royalty_module_client, - mock_module_registry_client, - ): - with ( - mock_royalty_module_client(), - mock_module_registry_client, - ): - result = validate_license_terms_data( - [ - { - "terms": asdict(LICENSE_TERMS_DATA[0].terms), - "licensing_config": LICENSE_TERMS_DATA[0].licensing_config, - } - ], - mock_web3, - ) - assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE +def mock_multicall3_client(): + """Mock Multicall3Client.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.utils.registration.registration_utils.Multicall3Client", + return_value=MagicMock( + contract=MagicMock( + address="multicall3", + ), + ), + ) - def test_throws_error_when_royalty_policy_not_whitelisted( - self, - mock_web3, - mock_royalty_module_client, - mock_module_registry_client, - ): - with ( - mock_royalty_module_client(is_whitelisted_policy=False), - mock_module_registry_client, - pytest.raises(ValueError, match="The royalty_policy is not whitelisted."), - ): - validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) - - def test_throws_error_when_currency_not_whitelisted( - self, - mock_web3, - mock_royalty_module_client, - mock_module_registry_client, - ): - with ( - mock_royalty_module_client(is_whitelisted_token=False), - mock_module_registry_client, - pytest.raises(ValueError, match="The currency is not whitelisted."), - ): - validate_license_terms_data(LICENSE_TERMS_DATA, mock_web3) - - def test_validates_multiple_license_terms( - self, - mock_web3, - mock_royalty_module_client, - mock_module_registry_client, - ): - # Use LICENSE_TERMS_DATA twice to test multiple terms - license_terms_data = LICENSE_TERMS_DATA + [ - replace( - LICENSE_TERMS_DATA[0], - terms=replace(LICENSE_TERMS_DATA[0].terms, commercial_rev_share=20), - ) - ] - - with ( - mock_royalty_module_client(), - mock_module_registry_client, - ): - result = validate_license_terms_data(license_terms_data, mock_web3) - assert result[0] == LICENSE_TERMS_DATA_CAMEL_CASE - assert result[1] == { - "terms": { - **LICENSE_TERMS_DATA_CAMEL_CASE["terms"], - "commercialRevShare": 20 * 10**6, - }, - "licensingConfig": LICENSE_TERMS_DATA_CAMEL_CASE["licensingConfig"], - } + return _mock class TestAggregateMulticallRequests: - def test_aggregates_single_request(self): + def test_aggregates_single_request(self, mock_web3, mock_multicall3_client): """Test aggregating a single request.""" - multicall3_address = "multicall3" - encoded_data = b"encoded_data_1" - contract_call = MagicMock(return_value=HexStr("0x123")) - - request = TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=False, - workflow_address=ADDRESS, - validated_request=[], - contract_call=contract_call, - ) - - result = aggregate_multicall_requests( - requests=[request], - is_use_multicall3=False, - multicall_address=multicall3_address, - ) - - assert len(result) == 1 - assert ADDRESS in result - assert result[ADDRESS]["encoded_tx_data"] == [encoded_data] - assert result[ADDRESS]["contract_calls"] == [contract_call] - - def test_aggregates_multiple_requests_same_address(self): + with mock_multicall3_client(): + encoded_data = b"encoded_data_1" + contract_call = MagicMock(return_value=HexStr("0x123")) + request = TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=contract_call, + ) + result = aggregate_multicall_requests( + requests=[request], + is_use_multicall3=False, + web3=mock_web3, + ) + assert len(result) == 1 + assert ADDRESS in result + aggregated_request_data = result[ADDRESS] + assert aggregated_request_data["call_data"] == [encoded_data] + assert aggregated_request_data["license_terms_data"] == [[]] + assert aggregated_request_data["method_reference"] == contract_call + + def test_aggregates_multiple_requests_same_address( + self, mock_web3, mock_multicall3_client + ): """Test aggregating multiple requests to the same address.""" - encoded_data_1 = b"encoded_data_1" - encoded_data_2 = b"encoded_data_2" - contract_call_1 = MagicMock(return_value=HexStr("0x111")) - contract_call_2 = MagicMock(return_value=HexStr("0x222")) - - request_1 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_1, - is_use_multicall3=False, - workflow_address=ADDRESS, - validated_request=[], - contract_call=contract_call_1, - ) - request_2 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_2, - is_use_multicall3=False, - workflow_address=ADDRESS, - validated_request=[], - contract_call=contract_call_2, - ) - - multicall3_address = "multicall3" - result = aggregate_multicall_requests( - requests=[request_1, request_2], - is_use_multicall3=False, - multicall_address=multicall3_address, - ) + with mock_multicall3_client(): + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + contract_call = MagicMock(return_value=HexStr("0x111")) + + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=contract_call, + ) + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=contract_call, + extra_data=ExtraData( + license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], + ), + ) - assert len(result) == 1 - assert ADDRESS in result - assert result[ADDRESS]["encoded_tx_data"] == [encoded_data_1, encoded_data_2] - assert result[ADDRESS]["contract_calls"] == [contract_call_1, contract_call_2] + result = aggregate_multicall_requests( + requests=[request_1, request_2], + is_use_multicall3=False, + web3=mock_web3, + ) - def test_aggregates_multiple_requests_different_addresses(self): + assert len(result) == 1 + assert ADDRESS in result + aggregated_request_data = result[ADDRESS] + assert aggregated_request_data["call_data"] == [ + encoded_data_1, + encoded_data_2, + ] + assert aggregated_request_data["license_terms_data"] == [ + [], + [LICENSE_TERMS_DATA_CAMEL_CASE], + ] + assert aggregated_request_data["method_reference"] == contract_call + + def test_aggregates_multiple_requests_different_addresses( + self, mock_web3, mock_multicall3_client + ): """Test aggregating multiple requests to different addresses.""" - workflow_address_1 = ADDRESS - workflow_address_2 = "0x" - encoded_data_1 = b"encoded_data_1" - encoded_data_2 = b"encoded_data_2" - contract_call_1 = MagicMock(return_value=HexStr("0x111")) - contract_call_2 = MagicMock(return_value=HexStr("0x222")) - - request_1 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_1, - is_use_multicall3=False, - workflow_address=workflow_address_1, - validated_request=[], - contract_call=contract_call_1, - ) - request_2 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_2, - is_use_multicall3=False, - workflow_address=workflow_address_2, - validated_request=[], - contract_call=contract_call_2, - ) - - result = aggregate_multicall_requests( - requests=[request_1, request_2], - is_use_multicall3=False, - multicall_address="0xmulticall", - ) + with mock_multicall3_client(): + workflow_address_1 = ADDRESS + workflow_address_2 = "0xDifferentAddress" + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + encoded_data_3 = b"encoded_data_3" + contract_call_1 = MagicMock(return_value=HexStr("0x111")) + contract_call_2 = MagicMock(return_value=HexStr("0x222")) + royalty_shares = [RoyaltyShareInput(recipient=ADDRESS, percentage=50)] + + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=False, + workflow_address=workflow_address_1, + validated_request=[], + original_method_reference=contract_call_1, + ) + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=False, + workflow_address=workflow_address_2, + validated_request=[], + original_method_reference=contract_call_2, + extra_data=ExtraData( + license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], + ), + ) + request_3 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_3, + is_use_multicall3=False, + workflow_address=workflow_address_2, + validated_request=[], + original_method_reference=contract_call_2, + extra_data=ExtraData( + royalty_shares=royalty_shares, + ), + ) - assert len(result) == 2 - assert workflow_address_1 in result - assert workflow_address_2 in result - assert result[workflow_address_1]["encoded_tx_data"] == [encoded_data_1] - assert result[workflow_address_1]["contract_calls"] == [contract_call_1] - assert result[workflow_address_2]["encoded_tx_data"] == [encoded_data_2] - assert result[workflow_address_2]["contract_calls"] == [contract_call_2] + result = aggregate_multicall_requests( + requests=[request_1, request_2, request_3], + is_use_multicall3=False, + web3=mock_web3, + ) - def test_uses_multicall3_address_when_enabled(self): + assert len(result) == 2 + assert workflow_address_1 in result + assert workflow_address_2 in result + + aggregated_request_data = result[workflow_address_1] + assert aggregated_request_data["call_data"] == [encoded_data_1] + assert aggregated_request_data["license_terms_data"] == [[]] + assert aggregated_request_data["method_reference"] == contract_call_1 + + aggregated_request_data = result[workflow_address_2] + assert aggregated_request_data["call_data"] == [ + encoded_data_2, + encoded_data_3, + ] + assert aggregated_request_data["license_terms_data"] == [ + [LICENSE_TERMS_DATA_CAMEL_CASE], + [], + ] + assert aggregated_request_data["method_reference"] == contract_call_2 + + def test_uses_multicall3_address_when_enabled( + self, mock_web3, mock_multicall3_client + ): """Test using multicall3 address when is_use_multicall3 is True.""" - multicall3_address = "multicall3" - encoded_data = b"encoded_data" - contract_call = MagicMock(return_value=HexStr("0x123")) - - request = TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=True, - workflow_address=ADDRESS, - validated_request=[], - contract_call=contract_call, - ) + with mock_multicall3_client() as mock_patch: + multicall3_instance = mock_patch.return_value + encoded_data_1 = b"encoded_data1" + encoded_data_2 = b"encoded_data2" + contract_call1 = MagicMock(return_value=HexStr("0x111")) + contract_call2 = MagicMock(return_value=HexStr("0x222")) + + request1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=True, + workflow_address="workflow1", + validated_request=[], + original_method_reference=contract_call1, + ) + request2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=True, + workflow_address="workflow2", + validated_request=[], + original_method_reference=contract_call2, + extra_data=ExtraData( + license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], + ), + ) - result = aggregate_multicall_requests( - requests=[request], - is_use_multicall3=True, - multicall_address=multicall3_address, - ) + result = aggregate_multicall_requests( + requests=[request1, request2], + is_use_multicall3=True, + web3=mock_web3, + ) - assert len(result) == 1 - assert multicall3_address in result - assert ADDRESS not in result - assert result[multicall3_address]["encoded_tx_data"] == [encoded_data] - assert result[multicall3_address]["contract_calls"] == [contract_call] + assert len(result) == 1 + assert "multicall3" in result + assert "workflow1" not in result + assert "workflow2" not in result + + aggregated_request_data = result["multicall3"] + # When using multicall3, call_data should be Multicall3Call structure + expected_call_data = [ + { + "target": "workflow1", + "allowFailure": False, + "value": 0, + "callData": encoded_data_1, + }, + { + "target": "workflow2", + "allowFailure": False, + "value": 0, + "callData": encoded_data_2, + }, + ] + assert aggregated_request_data["call_data"] == expected_call_data + assert aggregated_request_data["license_terms_data"] == [ + [], + [LICENSE_TERMS_DATA_CAMEL_CASE], + ] + # Method reference should be multicall3's method + assert ( + aggregated_request_data["method_reference"] + == multicall3_instance.build_aggregate3_transaction + ) - def test_uses_workflow_address_when_multicall3_disabled(self): + def test_uses_workflow_address_when_multicall3_disabled( + self, mock_web3, mock_multicall3_client + ): """Test using workflow address when is_use_multicall3 is False.""" - multicall3_address = "multicall3" - encoded_data = b"encoded_data" - contract_call = MagicMock(return_value=HexStr("0x123")) - - request = TransformedRegistrationRequest( - encoded_tx_data=encoded_data, - is_use_multicall3=True, - workflow_address=ADDRESS, - validated_request=[], - contract_call=contract_call, - ) - - result = aggregate_multicall_requests( - requests=[request], - is_use_multicall3=False, - multicall_address=multicall3_address, - ) - - assert len(result) == 1 - assert ADDRESS in result - assert multicall3_address not in result - assert result[ADDRESS]["encoded_tx_data"] == [encoded_data] - assert result[ADDRESS]["contract_calls"] == [contract_call] + with mock_multicall3_client() as mock_patch: + multicall3_instance = mock_patch.return_value + multicall3_address = multicall3_instance.contract.address + + encoded_data_1 = b"encoded_data1" + encoded_data_2 = b"encoded_data2" + contract_call1 = MagicMock(return_value=HexStr("0x111")) + contract_call2 = MagicMock(return_value=HexStr("0x222")) + + request1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=True, # Request wants to use multicall3 + workflow_address="workflow1", + validated_request=[], + original_method_reference=contract_call1, + extra_data=ExtraData( + license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], + ), + ) + request2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=True, + workflow_address="workflow2", + validated_request=[], + original_method_reference=contract_call2, + ) + result = aggregate_multicall_requests( + requests=[request1, request2], + is_use_multicall3=False, + web3=mock_web3, + ) - def test_aggregates_mixed_requests_with_multicall3(self): + assert len(result) == 2 + assert "workflow1" in result + assert "workflow2" in result + assert multicall3_address not in result + + aggregated_request_data = result["workflow1"] + assert aggregated_request_data["call_data"] == [encoded_data_1] + assert aggregated_request_data["license_terms_data"] == [ + [LICENSE_TERMS_DATA_CAMEL_CASE] + ] + assert aggregated_request_data["method_reference"] == contract_call1 + + aggregated_request_data = result["workflow2"] + assert aggregated_request_data["call_data"] == [encoded_data_2] + assert aggregated_request_data["license_terms_data"] == [[]] + assert aggregated_request_data["method_reference"] == contract_call2 + + def test_aggregates_mixed_requests_with_multicall3( + self, mock_web3, mock_multicall3_client + ): """Test aggregating mixed requests where some use multicall3 and some don't.""" - multicall3_address = "multicall3" - workflow_address_1 = ADDRESS - workflow_address_2 = "0x" - - encoded_data_1 = b"encoded_data_1" - encoded_data_2 = b"encoded_data_2" - encoded_data_3 = b"encoded_data_3" - contract_call_1 = MagicMock(return_value=HexStr("0x111")) - contract_call_2 = MagicMock(return_value=HexStr("0x222")) - contract_call_3 = MagicMock(return_value=HexStr("0x333")) - - # Request 1: uses multicall3 - request_1 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_1, - is_use_multicall3=True, - workflow_address=workflow_address_1, - validated_request=[], - contract_call=contract_call_1, - ) - # Request 2: uses multicall3 - request_2 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_2, - is_use_multicall3=True, - workflow_address=workflow_address_2, - validated_request=[], - contract_call=contract_call_2, - ) - # Request 3: doesn't use multicall3 - request_3 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_3, - is_use_multicall3=False, - workflow_address=workflow_address_1, - validated_request=[], - contract_call=contract_call_3, - ) - - result = aggregate_multicall_requests( - requests=[request_1, request_2, request_3], - is_use_multicall3=True, - multicall_address=multicall3_address, - ) + with mock_multicall3_client() as mock_patch: + multicall3_instance = mock_patch.return_value + multicall3_address = multicall3_instance.contract.address + + workflow_address_1 = ADDRESS + workflow_address_2 = "0xDifferentWorkflow" + + encoded_data_1 = b"encoded_data_1" + encoded_data_2 = b"encoded_data_2" + encoded_data_3 = b"encoded_data_3" + contract_call_1 = MagicMock(return_value=HexStr("0x111")) + contract_call_2 = MagicMock(return_value=HexStr("0x222")) + contract_call_3 = MagicMock(return_value=HexStr("0x333")) + + # Request 1: uses multicall3 + request_1 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_1, + is_use_multicall3=True, + workflow_address=workflow_address_1, + validated_request=[], + original_method_reference=contract_call_1, + extra_data=ExtraData( + license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], + ), + ) + # Request 2: uses multicall3 + request_2 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_2, + is_use_multicall3=True, + workflow_address=workflow_address_2, + validated_request=[], + original_method_reference=contract_call_2, + ) + # Request 3: doesn't use multicall3 + request_3 = TransformedRegistrationRequest( + encoded_tx_data=encoded_data_3, + is_use_multicall3=False, + workflow_address=workflow_address_1, + validated_request=[], + original_method_reference=contract_call_3, + ) - # Request 1 and 2 should be aggregated to multicall3_address - # Request 3 should use its workflow_address - assert len(result) == 2 - assert multicall3_address in result - assert workflow_address_1 in result + result = aggregate_multicall_requests( + requests=[request_1, request_2, request_3], + is_use_multicall3=True, + web3=mock_web3, + ) - # Check multicall3 aggregation (request_1 and request_2) - assert len(result[multicall3_address]["encoded_tx_data"]) == 2 - assert encoded_data_1 in result[multicall3_address]["encoded_tx_data"] - assert encoded_data_2 in result[multicall3_address]["encoded_tx_data"] - assert contract_call_1 in result[multicall3_address]["contract_calls"] - assert contract_call_2 in result[multicall3_address]["contract_calls"] + # Request 1 and 2 should be aggregated to multicall3_address + # Request 3 should use its workflow_address + assert len(result) == 2 + assert multicall3_address in result + assert workflow_address_1 in result + + # Check multicall3 aggregation (request_1 and request_2) + multicall_data = result[multicall3_address]["call_data"] + assert len(multicall_data) == 2 + # Check that multicall structures are correct + assert multicall_data == [ + { + "target": workflow_address_1, + "allowFailure": False, + "value": 0, + "callData": encoded_data_1, + }, + { + "target": workflow_address_2, + "allowFailure": False, + "value": 0, + "callData": encoded_data_2, + }, + ] + assert result[multicall3_address]["license_terms_data"] == [ + [LICENSE_TERMS_DATA_CAMEL_CASE], + [], + ] + assert ( + result[multicall3_address]["method_reference"] + == multicall3_instance.build_aggregate3_transaction + ) - # Check workflow address (request_3) - assert result[workflow_address_1]["encoded_tx_data"] == [encoded_data_3] - assert result[workflow_address_1]["contract_calls"] == [contract_call_3] + # Check workflow address (request_3) + aggregated_request_data = result[workflow_address_1] + assert aggregated_request_data["call_data"] == [encoded_data_3] + assert aggregated_request_data["license_terms_data"] == [[]] + assert aggregated_request_data["method_reference"] == contract_call_3 - def test_aggregates_empty_requests_list(self): + def test_aggregates_empty_requests_list(self, mock_web3, mock_multicall3_client): """Test aggregating an empty list of requests.""" - multicall3_address = "multicall3" - result = aggregate_multicall_requests( - requests=[], - is_use_multicall3=False, - multicall_address=multicall3_address, - ) - - assert len(result) == 0 - assert isinstance(result, dict) - - def test_aggregates_multiple_requests_to_multicall3(self): - """Test aggregating multiple requests that all use multicall3.""" - multicall3_address = "0xmulticall3" - workflow_address_1 = "0x1" - workflow_address_2 = "0x2" - workflow_address_3 = "0x33333333" - encoded_data_1 = b"encoded_data_1" - encoded_data_2 = b"encoded_data_2" - encoded_data_3 = b"encoded_data_3" - contract_call_1 = MagicMock(return_value=HexStr("0x111")) - contract_call_2 = MagicMock(return_value=HexStr("0x222")) - contract_call_3 = MagicMock(return_value=HexStr("0x333")) - - request_1 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_1, - is_use_multicall3=True, - workflow_address=workflow_address_1, - validated_request=[], - contract_call=contract_call_1, - ) - request_2 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_2, - is_use_multicall3=True, - workflow_address=workflow_address_2, - validated_request=[], - contract_call=contract_call_2, - ) - request_3 = TransformedRegistrationRequest( - encoded_tx_data=encoded_data_3, - is_use_multicall3=True, - workflow_address=workflow_address_3, - validated_request=[], - contract_call=contract_call_3, - ) - - result = aggregate_multicall_requests( - requests=[request_1, request_2, request_3], - is_use_multicall3=True, - multicall_address=multicall3_address, - ) + with mock_multicall3_client(): + result = aggregate_multicall_requests( + requests=[], + is_use_multicall3=False, + web3=mock_web3, + ) - assert len(result) == 1 - assert multicall3_address in result - assert len(result[multicall3_address]["encoded_tx_data"]) == 3 - assert len(result[multicall3_address]["contract_calls"]) == 3 - assert encoded_data_1 in result[multicall3_address]["encoded_tx_data"] - assert encoded_data_2 in result[multicall3_address]["encoded_tx_data"] - assert encoded_data_3 in result[multicall3_address]["encoded_tx_data"] - assert contract_call_1 in result[multicall3_address]["contract_calls"] - assert contract_call_2 in result[multicall3_address]["contract_calls"] - assert contract_call_3 in result[multicall3_address]["contract_calls"] + assert len(result) == 0 + assert isinstance(result, dict) From d7e725c21b749bbd1c1e1d12a71b0c7d6ad8aecf Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 15:50:32 +0800 Subject: [PATCH 38/52] test:prepare_distribute_royalty_tokens_requests --- tests/unit/utils/test_registration_utils.py | 246 +++++++++++++++++++- 1 file changed, 245 insertions(+), 1 deletion(-) diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index 74aa0ea0..c504b5e4 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -3,13 +3,15 @@ import pytest from ens.ens import HexStr +from story_protocol_python_sdk import RoyaltyShareInput from story_protocol_python_sdk.types.resource.IPAsset import ( ExtraData, + IPRoyaltyVault, TransformedRegistrationRequest, ) -from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput from story_protocol_python_sdk.utils.registration.registration_utils import ( aggregate_multicall_requests, + prepare_distribute_royalty_tokens_requests, ) from tests.unit.fixtures.data import ADDRESS, LICENSE_TERMS_DATA_CAMEL_CASE @@ -392,3 +394,245 @@ def test_aggregates_empty_requests_list(self, mock_web3, mock_multicall3_client) assert len(result) == 0 assert isinstance(result, dict) + + +@pytest.fixture +def mock_transform_distribute_royalty_tokens_request(): + """Mock dependencies needed by transform_distribute_royalty_tokens_request.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.utils.registration.registration_utils.transform_distribute_royalty_tokens_request", + return_value=TransformedRegistrationRequest( + encoded_tx_data=b"mock_encoded_data", + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + ), + ) + + return _mock + + +class TestPrepareDistributeRoyaltyTokensRequests: + def test_returns_empty_lists_when_extra_data_list_is_empty( + self, mock_web3, mock_account + ): + """Test that empty lists are returned when extra_data_list is empty.""" + result = prepare_distribute_royalty_tokens_requests( + extra_data_list=[], + web3=mock_web3, + ip_registered=[], + royalty_vault=[], + account=mock_account, + chain_id=1, + ) + + transformed_requests, matching_vaults = result + assert transformed_requests == [] + assert matching_vaults == [] + + def test_filters_and_matches_ip_and_vault_data( + self, mock_web3, mock_account, mock_transform_distribute_royalty_tokens_request + ): + """Test successful filtering and matching of IP and vault data.""" + with mock_transform_distribute_royalty_tokens_request(): + nft_contract = "0xNFTContract" + token_id = 123 + ip_id = "ip_id" + ip_registered = [ + { + "tokenContract": nft_contract, + "tokenId": token_id, + "ipId": ip_id, + } + ] + ip_royalty_vault = "ip_royalty_vault" + royalty_vault = [ + { + "ipId": ip_id, + "ipRoyaltyVault": ip_royalty_vault, + } + ] + result = prepare_distribute_royalty_tokens_requests( + extra_data_list=[ + ExtraData( + nft_contract=nft_contract, + token_id=token_id, + deadline=1000, + royalty_total_amount=5000, + royalty_shares=[ + RoyaltyShareInput(recipient="0xRecipient", percentage=50) + ], + ) + ], + web3=mock_web3, + ip_registered=ip_registered, + royalty_vault=royalty_vault, + account=mock_account, + chain_id=1, + ) + transformed_requests, matching_vaults = result + assert len(transformed_requests) == 1 + assert len(matching_vaults) == 1 + assert matching_vaults == [ + IPRoyaltyVault(ip_id=ip_id, royalty_vault=ip_royalty_vault) + ] + + def test_skips_when_no_matching_ip_registered( + self, mock_web3, mock_account, mock_transform_distribute_royalty_tokens_request + ): + """Test that items are skipped when no matching IP is registered.""" + with mock_transform_distribute_royalty_tokens_request() as mock_transform: + nft_contract = "0xNonExistentContract" + token_id = 999 + ip_registered = [ + { + "tokenContract": "0xDifferentContract", + "tokenId": 123, + "ipId": "0xIPID", + } + ] + royalty_vault = [ + { + "ipId": "0xIPID", + "ipRoyaltyVault": "0xRoyaltyVault", + } + ] + result = prepare_distribute_royalty_tokens_requests( + extra_data_list=[ + ExtraData( + nft_contract=nft_contract, + token_id=token_id, + deadline=1000, + royalty_total_amount=5000, + royalty_shares=[ + RoyaltyShareInput(recipient="0xRecipient", percentage=50) + ], + ) + ], + web3=mock_web3, + ip_registered=ip_registered, + royalty_vault=royalty_vault, + account=mock_account, + chain_id=1, + ) + transformed_requests, matching_vaults = result + assert transformed_requests == [] + assert matching_vaults == [] + # Verify transform was not called since no IP matched + mock_transform.assert_not_called() + + def test_skips_when_no_matching_vault( + self, mock_web3, mock_account, mock_transform_distribute_royalty_tokens_request + ): + """Test that items are skipped when no matching vault is found.""" + with mock_transform_distribute_royalty_tokens_request() as mock_transform: + nft_contract = "0xNFTContract" + token_id = 123 + ip_id = "0xIPID" + ip_registered = [ + { + "tokenContract": nft_contract, + "tokenId": token_id, + "ipId": ip_id, + } + ] + # Vault for different IP ID + royalty_vault = [ + { + "ipId": "0xDifferentIPID", + "ipRoyaltyVault": "0xRoyaltyVault", + } + ] + result = prepare_distribute_royalty_tokens_requests( + extra_data_list=[ + ExtraData( + nft_contract=nft_contract, + token_id=token_id, + deadline=1000, + royalty_total_amount=5000, + royalty_shares=[ + RoyaltyShareInput(recipient="0xRecipient", percentage=50) + ], + ) + ], + web3=mock_web3, + ip_registered=ip_registered, + royalty_vault=royalty_vault, + account=mock_account, + chain_id=1, + ) + transformed_requests, matching_vaults = result + assert transformed_requests == [] + assert matching_vaults == [] + # Verify transform was not called since no vault matched + mock_transform.assert_not_called() + + def test_processes_multiple_extra_data_items( + self, mock_web3, mock_account, mock_transform_distribute_royalty_tokens_request + ): + """Test processing multiple extra_data items with mixed matching results.""" + with mock_transform_distribute_royalty_tokens_request() as mock_transform: + # Test data - 3 items: 2 should match, 1 should not + ip_registered = [ + {"tokenContract": "0xContract1", "tokenId": 1, "ipId": "0xIPID1"}, + {"tokenContract": "0xContract2", "tokenId": 2, "ipId": "0xIPID2"}, + {"tokenContract": "0xContract3", "tokenId": 3, "ipId": "0xIPID3"}, + ] + # Only vaults for first two items + royalty_vault = [ + {"ipId": "0xIPID1", "ipRoyaltyVault": "0xVault1"}, + {"ipId": "0xIPID2", "ipRoyaltyVault": "0xVault2"}, + # No vault for 0xIPID3 + ] + result = prepare_distribute_royalty_tokens_requests( + extra_data_list=[ + # Item 1: Should match + ExtraData( + nft_contract="0xContract1", + token_id=1, + deadline=1000, + royalty_total_amount=5000, + royalty_shares=[ + RoyaltyShareInput(recipient="0xRecipient1", percentage=30) + ], + ), + # Item 2: Should match + ExtraData( + nft_contract="0xContract2", + token_id=2, + deadline=2000, + royalty_total_amount=6000, + royalty_shares=[ + RoyaltyShareInput(recipient="0xRecipient2", percentage=40) + ], + ), + # Item 3: Should not match (no vault) + ExtraData( + nft_contract="0xContract3", + token_id=3, + deadline=3000, + royalty_total_amount=7000, + royalty_shares=[ + RoyaltyShareInput(recipient="0xRecipient3", percentage=50) + ], + ), + ], + web3=mock_web3, + ip_registered=ip_registered, + royalty_vault=royalty_vault, + account=mock_account, + chain_id=1, + ) + transformed_requests, matching_vaults = result + # Should have 2 results (first two items matched) + assert len(transformed_requests) == 2 + assert len(matching_vaults) == 2 + assert matching_vaults == [ + IPRoyaltyVault(ip_id="0xIPID1", royalty_vault="0xVault1"), + IPRoyaltyVault(ip_id="0xIPID2", royalty_vault="0xVault2"), + ] + # Verify transform was called twice (once for each matched item) + assert mock_transform.call_count == 2 From 13e06bdcb5340e7a4c9fcefe99ae6b6ccb018d25 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 16:07:22 +0800 Subject: [PATCH 39/52] test:send_transactions --- tests/unit/utils/test_registration_utils.py | 196 ++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index c504b5e4..b9b4f827 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -12,6 +12,7 @@ from story_protocol_python_sdk.utils.registration.registration_utils import ( aggregate_multicall_requests, prepare_distribute_royalty_tokens_requests, + send_transactions, ) from tests.unit.fixtures.data import ADDRESS, LICENSE_TERMS_DATA_CAMEL_CASE @@ -636,3 +637,198 @@ def test_processes_multiple_extra_data_items( ] # Verify transform was called twice (once for each matched item) assert mock_transform.call_count == 2 + + +@pytest.fixture +def mock_build_and_send_transaction(): + """Mock build_and_send_transaction function.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.utils.registration.registration_utils.build_and_send_transaction", + ) + + return _mock + + +class TestSendTransactions: + def test_sends_single_transaction( + self, + mock_web3, + mock_account, + mock_multicall3_client, + mock_build_and_send_transaction, + ): + """Test sending a single transaction.""" + with mock_multicall3_client(), mock_build_and_send_transaction() as mock_build: + # Setup test data + encoded_data = b"encoded_data" + method_reference = MagicMock() + workflow_address = ADDRESS + + transformed_request = TransformedRegistrationRequest( + encoded_tx_data=encoded_data, + is_use_multicall3=False, + workflow_address=workflow_address, + validated_request=[], + original_method_reference=method_reference, + ) + + # Mock build_and_send_transaction return value + tx_hash = "0xTxHash" + tx_receipt = {"status": 1} + mock_build.return_value = { + "tx_hash": tx_hash, + "tx_receipt": tx_receipt, + } + + result = send_transactions( + transformed_requests=[transformed_request], + is_use_multicall3=False, + web3=mock_web3, + account=mock_account, + ) + + tx_results, aggregated_requests = result + + # Verify results + assert len(tx_results) == 1 + assert tx_results[0]["tx_hash"] == tx_hash + assert tx_results[0]["tx_receipt"] == tx_receipt + + # Verify aggregated_requests structure (from real aggregate function) + assert len(aggregated_requests) == 1 + assert workflow_address in aggregated_requests + assert aggregated_requests[workflow_address]["call_data"] == [encoded_data] + assert ( + aggregated_requests[workflow_address]["method_reference"] + == method_reference + ) + + # Verify build_and_send_transaction was called correctly + mock_build.assert_called_once_with( + mock_web3, + mock_account, + method_reference, + [encoded_data], + tx_options=None, + ) + + def test_sends_multiple_transactions_to_different_addresses( + self, + mock_web3, + mock_account, + mock_multicall3_client, + mock_build_and_send_transaction, + ): + """Test sending multiple transactions to different addresses.""" + with mock_multicall3_client(), mock_build_and_send_transaction() as mock_build: + # Setup test data + workflow_address_1 = ADDRESS + workflow_address_2 = "0xWorkflowAddress2" + method_reference_1 = MagicMock() + method_reference_2 = MagicMock() + + transformed_request_1 = TransformedRegistrationRequest( + encoded_tx_data=b"data1", + is_use_multicall3=True, + workflow_address=workflow_address_1, + validated_request=[], + original_method_reference=method_reference_1, + ) + transformed_request_2 = TransformedRegistrationRequest( + encoded_tx_data=b"data2", + is_use_multicall3=False, + workflow_address=workflow_address_2, + validated_request=[], + original_method_reference=method_reference_2, + ) + transformed_request_3 = TransformedRegistrationRequest( + encoded_tx_data=b"data3", + is_use_multicall3=True, + workflow_address=workflow_address_1, + validated_request=[], + original_method_reference=method_reference_1, + extra_data=ExtraData( + license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], + ), + ) + + # Mock build_and_send_transaction return values + mock_build.side_effect = [ + {"tx_hash": "0xHash1", "tx_receipt": {"status": 1}}, + {"tx_hash": "0xHash2", "tx_receipt": {"status": 1}}, + ] + + result = send_transactions( + transformed_requests=[ + transformed_request_1, + transformed_request_2, + transformed_request_3, + ], + is_use_multicall3=True, + web3=mock_web3, + account=mock_account, + ) + + tx_results, aggregated_requests = result + + # Verify results + assert len(tx_results) == 2 + assert tx_results[0]["tx_hash"] == "0xHash1" + assert tx_results[1]["tx_hash"] == "0xHash2" + + # Verify aggregated_requests structure (from real aggregate function) + assert len(aggregated_requests) == 2 + assert "multicall3" in aggregated_requests + assert aggregated_requests["multicall3"]["call_data"] == [ + { + "target": workflow_address_1, + "allowFailure": False, + "value": 0, + "callData": b"data1", + }, + { + "target": workflow_address_1, + "allowFailure": False, + "value": 0, + "callData": b"data3", + }, + ] + assert aggregated_requests["multicall3"]["license_terms_data"] == [ + [], + [LICENSE_TERMS_DATA_CAMEL_CASE], + ] + assert workflow_address_2 in aggregated_requests + workflow_address_2_data = aggregated_requests[workflow_address_2] + assert workflow_address_2_data["call_data"] == [b"data2"] + assert workflow_address_2_data["method_reference"] == method_reference_2 + assert workflow_address_2_data["license_terms_data"] == [[]] + + # Verify build_and_send_transaction was called twice + assert mock_build.call_count == 2 + + def test_sends_empty_requests_list( + self, + mock_web3, + mock_account, + mock_multicall3_client, + mock_build_and_send_transaction, + ): + """Test sending empty requests list.""" + with mock_multicall3_client(), mock_build_and_send_transaction() as mock_build: + result = send_transactions( + transformed_requests=[], + is_use_multicall3=False, + web3=mock_web3, + account=mock_account, + ) + + tx_results, aggregated_requests = result + + # Verify results + assert tx_results == [] + assert aggregated_requests == {} + + # Verify build_and_send_transaction was not called + mock_build.assert_not_called() From 4d5fbc912e94004475854bb98f269a0ac2c8912d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 16:26:02 +0800 Subject: [PATCH 40/52] test:transform_distribute_royalty_tokens_request --- .../test_transform_registration_request.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index 225ad091..097aae0d 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -24,6 +24,7 @@ from story_protocol_python_sdk.utils.registration.transform_registration_request import ( get_allow_duplicates, get_public_minting, + transform_distribute_royalty_tokens_request, transform_request, validate_license_terms_data, ) @@ -203,6 +204,7 @@ def _mock(deadline: int = 1000, signature: bytes = b"signature"): mock_sign.get_permission_signature = MagicMock( return_value={"signature": signature} ) + mock_sign.get_signature = MagicMock(return_value={"signature": signature}) return patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.Sign", return_value=mock_sign, @@ -991,3 +993,106 @@ def test_raises_error_when_ip_not_registered( ), ): transform_request(request, mock_web3, account, CHAIN_ID) + + +class TestTransformDistributeRoyaltyTokensRequest: + @pytest.fixture + def mock_ip_account_impl_client(self): + """Mock IPAccountImplClient.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.IPAccountImplClient", + return_value=MagicMock(state=MagicMock(return_value=123)), + ) + + return _mock + + @pytest.fixture + def mock_ip_royalty_vault_client(self): + """Mock IpRoyaltyVaultImplClient.""" + + def _mock(): + mock_contract = MagicMock() + mock_contract.encode_abi.return_value = b"encoded_approve" + return patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.IpRoyaltyVaultImplClient", + return_value=MagicMock(contract=mock_contract), + ) + + return _mock + + @pytest.fixture + def mock_royalty_token_distribution_workflows_client(self): + """Mock RoyaltyTokenDistributionWorkflowsClient.""" + + def _mock(): + mock_contract = MagicMock() + mock_contract.address = ( + "royalty_token_distribution_workflows_client_address" + ) + mock_contract.encode_abi.return_value = b"encoded_distribute" + mock_build_multicall = MagicMock() + return patch( + "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyTokenDistributionWorkflowsClient", + return_value=MagicMock( + contract=mock_contract, + build_multicall_transaction=mock_build_multicall, + ), + ) + + return _mock + + def test_transforms_distribute_royalty_tokens_request_successfully( + self, + mock_web3, + mock_ip_account_impl_client, + mock_ip_royalty_vault_client, + mock_royalty_token_distribution_workflows_client, + mock_sign_util, + ): + """Test successful transformation of distribute royalty tokens request.""" + account = Account.create() + ip_id = IP_ID + royalty_vault = "0xRoyaltyVault" + royalty_shares = [ + RoyaltyShareInput(recipient="0xRecipient1", percentage=50), + RoyaltyShareInput(recipient="0xRecipient2", percentage=50), + ] + deadline = 1000 + total_amount = 100 + + with ( + mock_ip_account_impl_client(), + mock_ip_royalty_vault_client(), + mock_royalty_token_distribution_workflows_client(), + mock_sign_util(), + ): + result = transform_distribute_royalty_tokens_request( + ip_id=ip_id, + royalty_vault=royalty_vault, + deadline=deadline, + web3=mock_web3, + account=account, + chain_id=CHAIN_ID, + royalty_shares=royalty_shares, + total_amount=total_amount, + ) + + # Verify result structure + assert result.encoded_tx_data == b"encoded_distribute" + assert result.is_use_multicall3 is False + assert ( + result.workflow_address + == "royalty_token_distribution_workflows_client_address" + ) + assert result.extra_data is None + assert result.original_method_reference is not None + + # Verify validated_request structure + assert result.validated_request[0] == ip_id + assert result.validated_request[1] == royalty_shares + signature_data = cast(dict, result.validated_request[2]) + assert signature_data["signer"] == account.address + assert signature_data["deadline"] == deadline + assert signature_data["signature"] == b"signature" From e0e64fda0c671ab6ade7ac21b70ecbaec8252750 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 16:51:31 +0800 Subject: [PATCH 41/52] test:fix other tests --- tests/unit/resources/test_ip_asset.py | 13 +++---------- .../utils/test_transform_registration_request.py | 6 +++--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 3a8ccecd..f9e3af33 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -71,7 +71,7 @@ def mock_parse_ip_registered_event(ip_asset): def _mock(): return patch.object( ip_asset, - "_parse_tx_ip_registered_event", + "_get_registered_ips", return_value=[ {"ip_id": IP_ID, "token_id": 3}, {"ip_id": ADDRESS, "token_id": 4}, @@ -121,7 +121,7 @@ def mock_get_royalty_vault_address_by_ip_id(ip_asset): def _mock(royalty_vault=ADDRESS): return patch.object( ip_asset, - "get_royalty_vault_address_by_ip_id", + "_get_royalty_vault_address_by_ip_id", return_value=royalty_vault, ) @@ -215,9 +215,6 @@ def _mock( return_value=True ) - # Mock ModuleRegistryClient (for validate_license_terms_data) - mock_module_registry_client = MagicMock() - # Create patches patches = [ patch( @@ -241,13 +238,9 @@ def _mock( return_value=mock_royalty_workflows_client, ), patch( - "story_protocol_python_sdk.utils.registration.registration_utils.RoyaltyModuleClient", + "story_protocol_python_sdk.utils.registration.transform_registration_request.RoyaltyModuleClient", return_value=mock_royalty_module_client, ), - patch( - "story_protocol_python_sdk.utils.registration.registration_utils.ModuleRegistryClient", - return_value=mock_module_registry_client, - ), patch( "story_protocol_python_sdk.utils.registration.transform_registration_request.get_function_signature", return_value="", diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index 097aae0d..5f200192 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -1050,9 +1050,9 @@ def test_transforms_distribute_royalty_tokens_request_successfully( mock_ip_royalty_vault_client, mock_royalty_token_distribution_workflows_client, mock_sign_util, + mock_account, ): """Test successful transformation of distribute royalty tokens request.""" - account = Account.create() ip_id = IP_ID royalty_vault = "0xRoyaltyVault" royalty_shares = [ @@ -1073,7 +1073,7 @@ def test_transforms_distribute_royalty_tokens_request_successfully( royalty_vault=royalty_vault, deadline=deadline, web3=mock_web3, - account=account, + account=mock_account, chain_id=CHAIN_ID, royalty_shares=royalty_shares, total_amount=total_amount, @@ -1093,6 +1093,6 @@ def test_transforms_distribute_royalty_tokens_request_successfully( assert result.validated_request[0] == ip_id assert result.validated_request[1] == royalty_shares signature_data = cast(dict, result.validated_request[2]) - assert signature_data["signer"] == account.address + assert signature_data["signer"] == ADDRESS assert signature_data["deadline"] == deadline assert signature_data["signature"] == b"signature" From 7599082a8663de6ba01f1110eb614e1e00bf6eaa Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 26 Jan 2026 18:02:08 +0800 Subject: [PATCH 42/52] test: batch_ip_asset_with_optimized_workflows --- .../resources/IPAsset.py | 21 +- tests/unit/resources/test_ip_asset.py | 514 +++++++++++++++++- 2 files changed, 526 insertions(+), 9 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 39598a35..dccc44bc 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -1630,14 +1630,17 @@ def batch_ip_asset_with_optimized_workflows( ip_royalty_vaults=matching_vaults, ) ) - # Send distribute royalty tokens requests - distribute_royalty_tokens_tx_responses, _ = send_transactions( - transformed_requests=distribute_royalty_tokens_requests, - is_use_multicall3=is_use_multicall, - web3=self.web3, - account=self.account, - tx_options=tx_options, + distribute_royalty_tokens_tx_responses, _ = ( + send_transactions( + transformed_requests=distribute_royalty_tokens_requests, + is_use_multicall3=is_use_multicall, + web3=self.web3, + account=self.account, + tx_options=tx_options, + ) + if distribute_royalty_tokens_requests + else ([], {}) ) # Populate the license terms ids into the response @@ -1655,7 +1658,9 @@ def batch_ip_asset_with_optimized_workflows( ], ) except ValueError as e: - raise ValueError(f"Failed to batch register IP assets: {str(e)}") from e + raise ValueError( + f"Failed to batch register IP assets with optimized workflows: {str(e)}" + ) from e def _populate_license_terms_ids_into_response( self, diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index f9e3af33..2c8d748c 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -1,3 +1,4 @@ +from dataclasses import asdict from unittest.mock import MagicMock, patch import pytest @@ -6,21 +7,27 @@ from story_protocol_python_sdk import ( MAX_ROYALTY_TOKEN, + IpRegistrationWorkflowRequest, LicenseTermsDataInput, LicenseTermsOverride, LicensingConfig, + MintAndRegisterRequest, MintedNFT, MintNFT, NativeRoyaltyPolicy, PILFlavor, PILFlavorError, + RegisterRegistrationRequest, RoyaltyShareInput, ) from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( IPAccountImplClient, ) from story_protocol_python_sdk.resources.IPAsset import IPAsset -from story_protocol_python_sdk.types.resource.IPAsset import BatchMintAndRegisterIPInput +from story_protocol_python_sdk.types.resource.IPAsset import ( + BatchMintAndRegisterIPInput, + TransformedRegistrationRequest, +) from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput from story_protocol_python_sdk.utils.ip_metadata import IPMetadata, IPMetadataInput from story_protocol_python_sdk.utils.royalty import get_royalty_shares @@ -3679,3 +3686,508 @@ def test_throw_error_when_license_token_ids_are_not_owned_by_caller( match="Failed to link derivative: Failed to register derivative with license tokens: License token id 1 must be owned by the caller.", ): ip_asset.link_derivative(license_token_ids=[1], child_ip_id=IP_ID) + + +class TestBatchIpAssetWithOptimizedWorkflows: + """Test batch_ip_asset_with_optimized_workflows method.""" + + @pytest.fixture + def mock_transform_request(self): + """Mock transform_request function.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.resources.IPAsset.transform_request" + ) + + return _mock + + @pytest.fixture + def mock_send_transactions(self): + """Mock send_transactions function.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.resources.IPAsset.send_transactions" + ) + + return _mock + + @pytest.fixture + def mock_prepare_distribute_royalty_tokens_requests(self): + """Mock prepare_distribute_royalty_tokens_requests function.""" + + def _mock(): + return patch( + "story_protocol_python_sdk.resources.IPAsset.prepare_distribute_royalty_tokens_requests" + ) + + return _mock + + def test_batch_mint_and_register_with_license_terms( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + mock_prepare_distribute_royalty_tokens_requests, + ): + """Test batch registration with MintAndRegisterRequest and license terms.""" + requests = [ + MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + license_terms_data=LICENSE_TERMS_DATA, + ip_metadata=IP_METADATA, + recipient=ACCOUNT_ADDRESS, + allow_duplicates=True, + ), + MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + license_terms_data=LICENSE_TERMS_DATA, + ), + ] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + mock_prepare_distribute_royalty_tokens_requests() as mock_prepare, + patch.object( + ip_asset, + "_parse_tx_ip_registered_event", + return_value=[ + {"ipId": IP_ID, "tokenId": 1}, + {"ipId": ADDRESS, "tokenId": 2}, + ], + ), + patch.object( + ip_asset, "_parse_all_ip_royalty_vault_deployed_events", return_value=[] + ), + patch.object( + ip_asset.pi_license_template_client, + "getLicenseTermsId", + side_effect=[1, 2], + ), + ): + # Mock transform_request to return TransformedRegistrationRequest + mock_transformed_1 = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data_1", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transformed_2 = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data_2", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transform.side_effect = [mock_transformed_1, mock_transformed_2] + + license_terms_dict = asdict(LICENSE_TERMS_DATA[0]) + mock_send.side_effect = [ + ( + [{"tx_hash": TX_HASH.hex(), "tx_receipt": {"logs": []}}], + { + ADDRESS: { + "license_terms_data": [ + [license_terms_dict], + [license_terms_dict], + ] + } + }, + ), + ([], {}), # No distribute royalty tokens requests + ] + + # Mock prepare_distribute_royalty_tokens_requests + mock_prepare.return_value = ([], []) + + result = ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=True + ) + + # Verify transform_request was called for each request + assert mock_transform.call_count == 2 + + assert mock_send.call_count == 1 + + # Verify response structure + assert isinstance(result, dict) + assert "registration_results" in result + assert "distribute_royalty_tokens_tx_hashes" in result + assert len(result["registration_results"]) == 1 + assert result["registration_results"][0]["tx_hash"] == TX_HASH.hex() + assert len(result["distribute_royalty_tokens_tx_hashes"]) == 0 + + assert len(result["registration_results"][0]["registered_ips"]) == 2 + assert result["registration_results"][0]["registered_ips"][0][ + "license_terms_ids" + ] == [1] + # Second IP gets license_terms_ids [2] from the second LICENSE_TERMS_DATA + assert result["registration_results"][0]["registered_ips"][1][ + "license_terms_ids" + ] == [2] + + def test_batch_register_with_royalty_shares( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + mock_prepare_distribute_royalty_tokens_requests, + mock_parse_ip_registered_event, + ): + """Test batch registration with RegisterRegistrationRequest and royalty shares.""" + royalty_shares = [ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=50.0), + RoyaltyShareInput(recipient=ADDRESS, percentage=30.0), + ] + + requests = [ + RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=royalty_shares, + ip_metadata=IP_METADATA, + deadline=1000, + ), + ] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + mock_prepare_distribute_royalty_tokens_requests() as mock_prepare, + mock_parse_ip_registered_event(), + ): + # Mock transform_request with extra_data containing royalty_shares + mock_transformed = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data", + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data={"royalty_shares": royalty_shares}, + ) + mock_transform.return_value = mock_transformed + + # Mock send_transactions + mock_send.side_effect = [ + ( + [{"tx_hash": TX_HASH.hex(), "tx_receipt": {"logs": []}}], + {ADDRESS: {"license_terms_data": [LICENSE_TERMS_DATA]}}, + ), + ( + [{"tx_hash": "0xDistributeTxHash", "tx_receipt": {"logs": []}}], + {}, + ), + ] + + # Mock prepare_distribute_royalty_tokens_requests + mock_distribute_request = TransformedRegistrationRequest( + encoded_tx_data=b"distribute_data", + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_prepare.return_value = ([mock_distribute_request], []) + + result = ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=True + ) + + # Verify response + assert len(result["registration_results"]) == 1 + assert len(result["distribute_royalty_tokens_tx_hashes"]) == 1 + assert ( + result["distribute_royalty_tokens_tx_hashes"][0] == "0xDistributeTxHash" + ) + + def test_batch_mixed_requests( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + mock_prepare_distribute_royalty_tokens_requests, + mock_parse_ip_registered_event, + ): + """Test batch registration with mixed MintAndRegisterRequest and RegisterRegistrationRequest.""" + requests: list[IpRegistrationWorkflowRequest] = [ + MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[1], + ), + ), + RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=2, + license_terms_data=LICENSE_TERMS_DATA, + ), + ] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + mock_prepare_distribute_royalty_tokens_requests() as mock_prepare, + mock_parse_ip_registered_event(), + ): + mock_transformed_1 = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data_1", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transformed_2 = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data_2", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transform.side_effect = [mock_transformed_1, mock_transformed_2] + + mock_send.side_effect = [ + ( + [ + {"tx_hash": TX_HASH.hex(), "tx_receipt": {"logs": []}}, + {"tx_hash": "0xTxHash2", "tx_receipt": {"logs": []}}, + ], + { + ADDRESS: { + "license_terms_data": [[], LICENSE_TERMS_DATA], + } + }, + ), + ([], {}), + ] + + mock_prepare.return_value = ([], []) + + result = ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=True + ) + + # Verify multiple registrations + assert len(result["registration_results"]) == 2 + assert result["registration_results"][0]["tx_hash"] == TX_HASH.hex() + assert result["registration_results"][1]["tx_hash"] == "0xTxHash2" + + def test_batch_with_multicall_disabled( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + mock_prepare_distribute_royalty_tokens_requests, + mock_parse_ip_registered_event, + ): + """Test batch registration with is_use_multicall=False.""" + requests: list[IpRegistrationWorkflowRequest] = [ + MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + license_terms_data=LICENSE_TERMS_DATA, + ), + RegisterRegistrationRequest( + nft_contract=ADDRESS, + token_id=1, + license_terms_data=LICENSE_TERMS_DATA, + ), + ] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + mock_prepare_distribute_royalty_tokens_requests() as mock_prepare, + mock_parse_ip_registered_event(), + ): + mock_transformed_1 = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data_1", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transformed_2 = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data_2", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transform.side_effect = [mock_transformed_1, mock_transformed_2] + + mock_send.side_effect = [ + ( + [{"tx_hash": TX_HASH.hex(), "tx_receipt": {"logs": []}}], + {ADDRESS: {"license_terms_data": [LICENSE_TERMS_DATA]}}, + ), + ([], {}), + ] + + mock_prepare.return_value = ([], []) + + result = ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=False + ) + + # Verify is_use_multicall3 was passed correctly + assert mock_send.call_args_list[0][1]["is_use_multicall3"] is False + # Verify result + assert len(result["registration_results"]) == 1 + + def test_batch_with_royalty_shares_and_license_terms( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + mock_prepare_distribute_royalty_tokens_requests, + mock_parse_ip_registered_event, + ): + """Test batch registration with both royalty shares and license terms.""" + royalty_shares = [ + RoyaltyShareInput(recipient=ACCOUNT_ADDRESS, percentage=60.0), + ] + + requests = [ + MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + license_terms_data=LICENSE_TERMS_DATA, + royalty_shares=royalty_shares, + ip_metadata=IP_METADATA, + ), + ] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + mock_prepare_distribute_royalty_tokens_requests() as mock_prepare, + mock_parse_ip_registered_event(), + ): + mock_transformed = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data", + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data={"royalty_shares": royalty_shares}, + ) + mock_transform.return_value = mock_transformed + + mock_send.side_effect = [ + ( + [{"tx_hash": TX_HASH.hex(), "tx_receipt": {"logs": []}}], + {ADDRESS: {"license_terms_data": [LICENSE_TERMS_DATA]}}, + ), + ( + [{"tx_hash": "0xDistributeTxHash", "tx_receipt": {"logs": []}}], + {}, + ), + ] + + mock_distribute_request = TransformedRegistrationRequest( + encoded_tx_data=b"distribute_data", + is_use_multicall3=False, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_prepare.return_value = ( + [mock_distribute_request], + [{"ip_id": IP_ID, "royalty_vault": ADDRESS}], + ) + + result = ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=True + ) + + # Verify royalty distribution was handled + assert len(result["distribute_royalty_tokens_tx_hashes"]) == 1 + assert len(result["registration_results"][0]["ip_royalty_vaults"]) == 1 + assert ( + result["registration_results"][0]["ip_royalty_vaults"][0][ + "royalty_vault" + ] + == ADDRESS + ) + + def test_batch_empty_requests( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + mock_prepare_distribute_royalty_tokens_requests, + ): + """Test batch registration with empty requests list.""" + requests: list[MintAndRegisterRequest | RegisterRegistrationRequest] = [] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + mock_prepare_distribute_royalty_tokens_requests() as mock_prepare, + ): + mock_send.side_effect = [ + ([], {}), + ([], {}), + ] + + mock_prepare.return_value = ([], []) + + result = ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=True + ) + + # Verify no transform was called + mock_transform.assert_not_called() + # Verify empty response + assert len(result["registration_results"]) == 0 + assert len(result["distribute_royalty_tokens_tx_hashes"]) == 0 + + def test_batch_transaction_failure( + self, + ip_asset: IPAsset, + mock_transform_request, + mock_send_transactions, + ): + """Test batch registration when transaction fails.""" + requests = [ + MintAndRegisterRequest( + spg_nft_contract=ADDRESS, + license_terms_data=LICENSE_TERMS_DATA, + ), + ] + + with ( + mock_transform_request() as mock_transform, + mock_send_transactions() as mock_send, + ): + mock_transformed = TransformedRegistrationRequest( + encoded_tx_data=b"encoded_data", + is_use_multicall3=True, + workflow_address=ADDRESS, + validated_request=[], + original_method_reference=MagicMock(), + extra_data=None, + ) + mock_transform.return_value = mock_transformed + + # Mock send_transactions to raise an error + mock_send.side_effect = ValueError("Transaction failed") + + with pytest.raises( + ValueError, + match="Failed to batch register IP assets with optimized workflows: Transaction failed", + ): + ip_asset.batch_ip_asset_with_optimized_workflows( + requests=requests, is_use_multicall=True + ) From e23cf56d886f4b0d95d562f57a0d832ba5f309f8 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 10:55:51 +0800 Subject: [PATCH 43/52] refactor: move AggregatedRequestData to utils and clean up imports in registration_utils --- .../resources/IPAsset.py | 2 +- src/story_protocol_python_sdk/types/utils.py | 19 +++++++++++++++++++ .../utils/registration/registration_utils.py | 19 +------------------ 3 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 src/story_protocol_python_sdk/types/utils.py diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index dccc44bc..70f7a399 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -78,6 +78,7 @@ TransformedRegistrationRequest, ) from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput +from story_protocol_python_sdk.types.utils import AggregatedRequestData from story_protocol_python_sdk.utils.constants import ( DEADLINE, MAX_ROYALTY_TOKEN, @@ -96,7 +97,6 @@ is_initial_ip_metadata, ) from story_protocol_python_sdk.utils.registration.registration_utils import ( - AggregatedRequestData, prepare_distribute_royalty_tokens_requests, send_transactions, ) diff --git a/src/story_protocol_python_sdk/types/utils.py b/src/story_protocol_python_sdk/types/utils.py new file mode 100644 index 00000000..b80ca91c --- /dev/null +++ b/src/story_protocol_python_sdk/types/utils.py @@ -0,0 +1,19 @@ +from typing import TypedDict + +from ens.ens import Address, HexStr +from typing_extensions import Callable + + +class Multicall3Call(TypedDict): + target: Address + allowFailure: bool + value: int + callData: bytes + + +class AggregatedRequestData(TypedDict): + """Aggregated request data structure.""" + + call_data: list[bytes | Multicall3Call] + license_terms_data: list[list[dict]] + method_reference: Callable[[list[bytes], dict], HexStr] diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 62f21bcb..1f8f31b4 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -1,8 +1,5 @@ """Registration utilities for IP asset operations.""" -from collections.abc import Callable -from typing import TypedDict - from ens.ens import Address, HexStr from eth_account.signers.local import LocalAccount from web3 import Web3 @@ -13,27 +10,13 @@ IPRoyaltyVault, TransformedRegistrationRequest, ) +from story_protocol_python_sdk.types.utils import AggregatedRequestData from story_protocol_python_sdk.utils.registration.transform_registration_request import ( transform_distribute_royalty_tokens_request, ) from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction -class Multicall3Call(TypedDict): - target: Address - allowFailure: bool - value: int - callData: bytes - - -class AggregatedRequestData(TypedDict): - """Aggregated request data structure.""" - - call_data: list[bytes | Multicall3Call] - license_terms_data: list[list[dict]] - method_reference: Callable[[list[bytes], dict], HexStr] - - def aggregate_multicall_requests( requests: list[TransformedRegistrationRequest], is_use_multicall3: bool, From 7f6312037dc8f08c210f995df725f73926cf7427 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 14:13:10 +0800 Subject: [PATCH 44/52] refactor: clean up IPAsset and registration utility files by removing unused imports and updating comments for clarity --- src/story_protocol_python_sdk/__init__.py | 1 - .../resources/IPAsset.py | 33 +++++----- .../types/resource/IPAsset.py | 64 +++---------------- src/story_protocol_python_sdk/types/utils.py | 50 +++++++++++++++ .../utils/registration/registration_utils.py | 24 +------ .../transform_registration_request.py | 45 ++----------- 6 files changed, 82 insertions(+), 135 deletions(-) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index ef932f81..30589db6 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -87,7 +87,6 @@ "RegisterIpAssetResponse", "RegisterDerivativeIpAssetResponse", "LinkDerivativeResponse", - # Types for batch_register_ip_assets_with_optimized_workflows "MintAndRegisterRequest", "RegisterRegistrationRequest", "IpRegistrationWorkflowRequest", diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 70f7a399..52c4c8f6 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -57,7 +57,6 @@ BatchMintAndRegisterIPResponse, BatchRegisterIpAssetsWithOptimizedWorkflowsResponse, BatchRegistrationResult, - ExtraData, IpRegistrationWorkflowRequest, LicenseTermsDataInput, LinkDerivativeResponse, @@ -75,10 +74,13 @@ RegistrationResponse, RegistrationWithRoyaltyVaultAndLicenseTermsResponse, RegistrationWithRoyaltyVaultResponse, - TransformedRegistrationRequest, ) from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput -from story_protocol_python_sdk.types.utils import AggregatedRequestData +from story_protocol_python_sdk.types.utils import ( + AggregatedRequestData, + ExtraData, + TransformedRegistrationRequest, +) from story_protocol_python_sdk.utils.constants import ( DEADLINE, MAX_ROYALTY_TOKEN, @@ -1508,8 +1510,8 @@ def batch_ip_asset_with_optimized_workflows( **Request Types:** - - `MintAndRegisterRequest`: Mint a new NFT from an SPG NFT contract and register as IP - - `RegisterRegistrationRequest`: Register an already minted NFT as IP + - `MintAndRegisterRequest`: Mint a new NFT from an SPG NFT contract and register as IP ID + - `RegisterRegistrationRequest`: Register an already minted NFT as IP ID **Workflow Selection:** @@ -1530,7 +1532,7 @@ def batch_ip_asset_with_optimized_workflows( **Multicall Strategy:** - - Multicall3: Used when `is_use_multicall=True`, request is `MintAndRegisterRequest`, `spg_nft_contract` has public minting, + - Multicall3: Used when `is_use_multicall=True`, request is `MintAndRegisterRequest`, `spg_nft_contract` has public minting except for `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens`. - Workflow's native multicall: Used for all other cases - Requests using the same workflow are aggregated into a single multicall transaction @@ -1539,15 +1541,14 @@ def batch_ip_asset_with_optimized_workflows( Royalty token distribution is handled in a separate transaction because it requires a signature with the royalty vault address, which is only available after initial registration completes. - :param requests Sequence[IpRegistrationWorkflowRequest]: The list of registration requests. - :param is_use_multicall bool: [Optional] Whether to use multicall3 for eligible workflows. (default: True) - :param tx_options dict: [Optional] Transaction options. + :param requests `Sequence[IpRegistrationWorkflowRequest]`: The list of registration requests. + :param is_use_multicall `bool`: [Optional] Whether to use multicall3 for eligible workflows. (default: True) + :param tx_options `dict`: [Optional] Transaction options. :return `BatchRegisterIpAssetsWithOptimizedWorkflowsResponse`: Response with registration results and distribute royalty tokens transaction hashes. **Example:** ```python - # Mint and register with PIL terms (supports multicall3 if public minting enabled) response = client.ip_asset.batch_ip_asset_with_optimized_workflows( requests=[ MintAndRegisterRequest( @@ -1559,15 +1560,13 @@ def batch_ip_asset_with_optimized_workflows( nft_contract="0x...", token_id=123, deriv_data={...}, - ip_metadata={...} ) ], - is_use_multicall=True ) ``` """ try: - # Transform registration requests to transformed registration requests and send them into transaction + # Transform registration requests and send them into transaction transformed_requests: list[TransformedRegistrationRequest] = [ transform_request(request, self.web3, self.account, self.chain_id) for request in requests @@ -1591,7 +1590,7 @@ def batch_ip_asset_with_optimized_workflows( if tr.extra_data is not None and tr.extra_data.get("royalty_shares", None) is not None ] - # Parse the response of the registration requests and collect distribute royalty tokens requests + # Parse the response of the registration responses and collect distribute royalty tokens requests response_list: list[BatchRegistrationResult] = [] for tx_response in tx_responses: ip_registered_events = self._parse_tx_ip_registered_event( @@ -1653,8 +1652,8 @@ def batch_ip_asset_with_optimized_workflows( return BatchRegisterIpAssetsWithOptimizedWorkflowsResponse( registration_results=response_list_with_license_terms_ids, distribute_royalty_tokens_tx_hashes=[ - tx_hash["tx_hash"] - for tx_hash in distribute_royalty_tokens_tx_responses + response["tx_hash"] + for response in distribute_royalty_tokens_tx_responses ], ) except ValueError as e: @@ -2245,7 +2244,7 @@ def _parse_all_ip_royalty_vault_deployed_events( Parse all IpRoyaltyVaultDeployed events from a transaction receipt. :param tx_receipt dict: The transaction receipt. - :return list[tuple[Address, Address]]: List of (ip_id, ip_royalty_vault) tuples. + :return list[dict[str, Address]]: List of dicts with keys "ipId" and "ipRoyaltyVault". """ event_signature = Web3.keccak( text="IpRoyaltyVaultDeployed(address,address)" diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 6c8281df..27063bbe 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Callable, Literal, TypedDict +from typing import Literal, TypedDict from ens.ens import Address, HexStr @@ -251,7 +251,7 @@ class MintAndRegisterRequest: """ Request for mint and register IP operations. - Used for: + Used for(contract method): - mintAndRegisterIpAssetWithPilTerms - mintAndRegisterIpAndMakeDerivative - mintAndRegisterIpAndAttachPilTermsAndDistributeRoyaltyTokens @@ -264,11 +264,12 @@ class MintAndRegisterRequest: ip_metadata: [Optional] The metadata for the newly minted NFT and registered IP. license_terms_data: [Optional] The license terms data to attach. Required if not using deriv_data. deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using license_terms_data. - royalty_shares: [Optional] The royalty shares for distributing royalty tokens. + royalty_shares: [Optional] The royalty shares for distributing royalty tokens. Must be specified together with either `license_terms_data` or `deriv_data`. """ spg_nft_contract: Address recipient: Address | None = None + # TODO: need to consider how to handle new method and existing method allow_duplicates: bool | None = None ip_metadata: IPMetadataInput | None = None license_terms_data: list[LicenseTermsDataInput] | None = None @@ -280,11 +281,10 @@ class MintAndRegisterRequest: class RegisterRegistrationRequest: """ Request for register IP operations (already minted NFT). - license_terms_data, deriv_data and royalty_shares at least one of them is required,otherwise it will raise `invalid register request type`. - Used for: + Used for(contract method): - registerIpAndAttachPilTerms - - registerIpAndMakeDerivative (registerDerivativeIp) + - registerIpAndMakeDerivative - registerIpAndAttachPilTermsAndDeployRoyaltyVault - registerIpAndMakeDerivativeAndDeployRoyaltyVault @@ -295,7 +295,7 @@ class RegisterRegistrationRequest: deadline: [Optional] The deadline for the signature in seconds. (default: 1000) license_terms_data: [Optional] The license terms data to attach. Required if not using deriv_data. deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using license_terms_data. - royalty_shares: [Optional] The royalty shares for distributing royalty tokens. + royalty_shares: [Optional] The royalty shares for distributing royalty tokens. Must be specified together with either `license_terms_data` or `deriv_data`. """ nft_contract: Address @@ -330,7 +330,6 @@ class RegisteredIPWithLicenseTermsIds(RegisteredIP): Attributes: license_terms_ids: The license terms IDs of the registered IP asset. - If the license terms are not attached, the value is None. """ license_terms_ids: list[int] @@ -342,7 +341,7 @@ class BatchRegistrationResult(TypedDict, total=False): Attributes: tx_hash: The transaction hash. - registered_ips: List of registered IP assets (ip_id, token_id). + registered_ips: List of registered IP assets (ip_id, token_id, license_terms_ids). ip_royalty_vaults: [Optional] List of IP royalty vaults for deployed royalty vaults. """ @@ -362,50 +361,3 @@ class BatchRegisterIpAssetsWithOptimizedWorkflowsResponse(TypedDict, total=False registration_results: list[BatchRegistrationResult] distribute_royalty_tokens_tx_hashes: list[HexStr] - - -# ============================================================================= -# Transform Registration Request Types -# ============================================================================= -class ExtraData(TypedDict, total=False): - """ - Extra data for post-processing after registration. - - Attributes: - royalty_shares: [Optional] The royalty shares for distribution. - deadline: [Optional] The deadline for the signature. - royalty_total_amount: [Optional] The total amount of royalty tokens to distribute. - nft_contract: [Optional] The NFT contract address. - token_id: [Optional] The token ID. - license_terms_data: [Optional] The license terms data. - """ - - royalty_shares: list[RoyaltyShareInput] - deadline: int - royalty_total_amount: int - nft_contract: Address - token_id: int - license_terms_data: list[dict] | None - - -@dataclass -class TransformedRegistrationRequest: - """ - Transformed registration request with encoded data and multicall info. - - Attributes: - encoded_tx_data: The encoded transaction data. - is_use_multicall3: Whether to use multicall3 or SPG's native multicall. - workflow_address: The workflow contract address. - validated_request: The validated request arguments for the contract method. - original_method_reference: The original method reference for building transactions. - extra_data: [Optional] Extra data for post-processing. - """ - - encoded_tx_data: bytes - is_use_multicall3: bool - workflow_address: Address - validated_request: list[Address | int | str | bytes | dict | bool] - # TODO: need to rename with multicall3 method reference - original_method_reference: Callable[..., HexStr] - extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/types/utils.py b/src/story_protocol_python_sdk/types/utils.py index b80ca91c..3c9e6783 100644 --- a/src/story_protocol_python_sdk/types/utils.py +++ b/src/story_protocol_python_sdk/types/utils.py @@ -1,8 +1,11 @@ +from dataclasses import dataclass from typing import TypedDict from ens.ens import Address, HexStr from typing_extensions import Callable +from story_protocol_python_sdk import RoyaltyShareInput + class Multicall3Call(TypedDict): target: Address @@ -17,3 +20,50 @@ class AggregatedRequestData(TypedDict): call_data: list[bytes | Multicall3Call] license_terms_data: list[list[dict]] method_reference: Callable[[list[bytes], dict], HexStr] + + +# ============================================================================= +# Transform Registration Request Types +# ============================================================================= +class ExtraData(TypedDict, total=False): + """ + Extra data for post-processing after registration. + + Attributes: + royalty_shares: [Optional] The royalty shares for distribution. + deadline: [Optional] The deadline for the signature. + royalty_total_amount: [Optional] The total amount of royalty tokens to distribute. + nft_contract: [Optional] The NFT contract address. + token_id: [Optional] The token ID. + license_terms_data: [Optional] The license terms data. + """ + + royalty_shares: list[RoyaltyShareInput] + deadline: int + royalty_total_amount: int + nft_contract: Address + token_id: int + license_terms_data: list[dict] | None + + +@dataclass +class TransformedRegistrationRequest: + """ + Transformed registration request with encoded data and multicall info. + + Attributes: + encoded_tx_data: The encoded transaction data. + is_use_multicall3: Whether to use multicall3 or SPG's native multicall. + workflow_address: The workflow contract address. + validated_request: The validated request arguments for the contract method. + original_method_reference: The original method reference for building transactions. + extra_data: [Optional] Extra data for post-processing. + """ + + encoded_tx_data: bytes + is_use_multicall3: bool + workflow_address: Address + validated_request: list[Address | int | str | bytes | dict | bool] + # TODO: need to rename with multicall3 method reference + original_method_reference: Callable[..., HexStr] + extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 1f8f31b4..da48fbd5 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -27,18 +27,6 @@ def aggregate_multicall_requests( Groups requests that should be sent to the same multicall address together, collecting their encoded transaction data and method references. - - Args: - requests: List of transformed registration requests to aggregate. - is_use_multicall3: Whether to use multicall3 for aggregation. - web3: Web3 instance. - - Returns: - Dictionary mapping target addresses to aggregated request data: - - Key: Address (multicall address or workflow address) - - Value: AggregatedRequestData with: - - "call_data": List of encoded transaction data (bytes) - - "method_reference": The method to build the transaction """ aggregated_requests: dict[Address, AggregatedRequestData] = {} multicall3_client = Multicall3Client(web3) @@ -56,6 +44,7 @@ def aggregate_multicall_requests( aggregated_requests[target_address] = { "call_data": [], "license_terms_data": [], + # TODO: rename with multicall3 method reference "method_reference": ( multicall3_client.build_aggregate3_transaction if target_address == multicall3_client.contract.address @@ -95,17 +84,6 @@ def prepare_distribute_royalty_tokens_requests( account: LocalAccount, chain_id: int, ) -> tuple[list[TransformedRegistrationRequest], list[IPRoyaltyVault]]: - """ - Prepare distribute royalty tokens requests. - - Args: - extra_data_list: The extra data for distribute royalty tokens. - web3: Web3 instance. - ip_registered: The IP registered. - royalty_vault: The royalty vault addresses. - account: The account for signing and recipient fallback. - chain_id: The chain ID for IP ID calculation. - """ if not extra_data_list: return [], [] transformed_requests: list[TransformedRegistrationRequest] = [] diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 37659603..14905554 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -40,15 +40,17 @@ from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.types.resource.IPAsset import ( - ExtraData, IpRegistrationWorkflowRequest, LicenseTermsDataInput, MintAndRegisterRequest, RegisterRegistrationRequest, - TransformedRegistrationRequest, ) from story_protocol_python_sdk.types.resource.License import LicenseTermsInput from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput +from story_protocol_python_sdk.types.utils import ( + ExtraData, + TransformedRegistrationRequest, +) from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH from story_protocol_python_sdk.utils.derivative_data import DerivativeData from story_protocol_python_sdk.utils.function_signature import get_function_signature @@ -178,19 +180,7 @@ def transform_request( 3. Encodes the transaction data 4. Determines whether to use multicall3 or SPG's native multicall - Args: - request: The registration request (`IpRegistrationWorkflowRequest`) - web3: Web3 instance for contract interaction - account: The account for signing and recipient fallback - chain_id: The chain ID for IP ID calculation - - Returns: - TransformedRegistrationRequest with encoded data and multicall strategy - - Raises: - ValueError: If the request is invalid """ - # Check request type by attribute presence (following TypeScript SDK pattern) if hasattr(request, "spg_nft_contract"): return _handle_mint_and_register_request( cast(MintAndRegisterRequest, request), web3, account.address @@ -211,22 +201,6 @@ def transform_distribute_royalty_tokens_request( royalty_shares: list[RoyaltyShareInput], total_amount: int, ) -> TransformedRegistrationRequest: - """ - Transform a distribute royalty tokens request into encoded transaction data with multicall info. - distributeRoyaltyTokens method don't support multicall3 due to `msg.sender` check. - Args: - ip_id: The IP ID - royalty_vault: The royalty vault address - deadline: The deadline for the transaction - web3: The web3 instance - account: The account for signing and recipient fallback - chain_id: The chain ID for IP ID calculation - royalty_shares: The validated royalty shares with recipient and percentage. - Returns: - TransformedRegistrationRequest with encoded data and multicall strategy - Raises: - ValueError: If the request is invalid - """ ip_account_impl_client = IPAccountImplClient(web3, ip_id) state = ip_account_impl_client.state() royalty_token_distribution_workflows_client = ( @@ -282,7 +256,7 @@ def _handle_mint_and_register_request( """ Handle mintAndRegister* workflow requests. - Supports: + Supports (contract method): - mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens - mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens - mintAndRegisterIpAndAttachPILTerms @@ -374,7 +348,6 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( royalty_shares: list[dict], allow_duplicates: bool | None, ) -> TransformedRegistrationRequest: - """Handle mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens.""" royalty_token_distribution_workflows_client = ( RoyaltyTokenDistributionWorkflowsClient(web3) ) @@ -420,7 +393,6 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( allow_duplicates: bool | None, is_public_minting: bool, ) -> TransformedRegistrationRequest: - """Handle mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens.""" royalty_token_distribution_workflows_client = ( RoyaltyTokenDistributionWorkflowsClient(web3) ) @@ -462,7 +434,6 @@ def _handle_mint_and_register_with_license_terms( allow_duplicates: bool | None, is_public_minting: bool, ) -> TransformedRegistrationRequest: - """Handle mintAndRegisterIpAndAttachPILTerms.""" license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) license_attachment_workflows_address = ( license_attachment_workflows_client.contract.address @@ -501,7 +472,6 @@ def _handle_mint_and_register_with_derivative( allow_duplicates: bool | None, is_public_minting: bool, ) -> TransformedRegistrationRequest: - """Handle mintAndRegisterIpAndMakeDerivative.""" derivative_workflows_client = DerivativeWorkflowsClient(web3) derivative_workflows_address = derivative_workflows_client.contract.address abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" @@ -541,7 +511,7 @@ def _handle_register_request( """ Handle register* workflow requests (already minted NFTs). - Supports: + Supports (contract method): - registerIpAndAttachPILTermsAndDeployRoyaltyVault - registerIpAndMakeDerivativeAndDeployRoyaltyVault - registerIpAndAttachPILTerms @@ -568,6 +538,7 @@ def _handle_register_request( if request.license_terms_data else None ) + # TODO:consider some validation to extract in common place deriv_data = ( DerivativeData.from_input( web3=web3, input_data=request.deriv_data @@ -670,7 +641,6 @@ def _handle_register_with_license_terms_and_royalty_vault( state: bytes, royalty_total_amount: int, ) -> TransformedRegistrationRequest: - """Handle registerIpAndAttachPILTermsAndDeployRoyaltyVault.""" royalty_token_distribution_workflows_client = ( RoyaltyTokenDistributionWorkflowsClient(web3) ) @@ -738,7 +708,6 @@ def _handle_register_with_derivative_and_royalty_vault( state: bytes, royalty_total_amount: int, ) -> TransformedRegistrationRequest: - """Handle registerIpAndMakeDerivativeAndDeployRoyaltyVault.""" royalty_token_distribution_workflows_client = ( RoyaltyTokenDistributionWorkflowsClient(web3) ) From b71f8501f74241adf3202264090cf6cbf949875d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 14:14:08 +0800 Subject: [PATCH 45/52] test: update TestCollectRoyaltyAndClaimReward to adjust amount and max fees for improved accuracy --- tests/integration/test_integration_group.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index 6f8f3001..896546d0 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -231,10 +231,8 @@ def test_claim_reward(self, story_client: StoryClient, nft_collection: Address): licensor_ip_id=ip_id, license_template=PIL_LICENSE_TEMPLATE, license_terms_id=license_terms_id, - amount=100, + amount=1, receiver=ip_id, - max_minting_fee=1, - max_revenue_share=100, ) # Claim reward From 59bd4446379087e7bd030d5b18d564997adc8412 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 14:20:27 +0800 Subject: [PATCH 46/52] refactor: update royalty_shares type in ExtraData and clean up imports in registration_utils and test files --- src/story_protocol_python_sdk/types/utils.py | 4 +--- .../utils/registration/registration_utils.py | 6 +++--- tests/unit/resources/test_ip_asset.py | 6 ++---- tests/unit/utils/test_registration_utils.py | 4 ++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/story_protocol_python_sdk/types/utils.py b/src/story_protocol_python_sdk/types/utils.py index 3c9e6783..210180ac 100644 --- a/src/story_protocol_python_sdk/types/utils.py +++ b/src/story_protocol_python_sdk/types/utils.py @@ -4,8 +4,6 @@ from ens.ens import Address, HexStr from typing_extensions import Callable -from story_protocol_python_sdk import RoyaltyShareInput - class Multicall3Call(TypedDict): target: Address @@ -38,7 +36,7 @@ class ExtraData(TypedDict, total=False): license_terms_data: [Optional] The license terms data. """ - royalty_shares: list[RoyaltyShareInput] + royalty_shares: list[dict] deadline: int royalty_total_amount: int nft_contract: Address diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index da48fbd5..23034680 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -5,12 +5,12 @@ from web3 import Web3 from story_protocol_python_sdk.abi.Multicall3.Multicall3_client import Multicall3Client -from story_protocol_python_sdk.types.resource.IPAsset import ( +from story_protocol_python_sdk.types.resource.IPAsset import IPRoyaltyVault +from story_protocol_python_sdk.types.utils import ( + AggregatedRequestData, ExtraData, - IPRoyaltyVault, TransformedRegistrationRequest, ) -from story_protocol_python_sdk.types.utils import AggregatedRequestData from story_protocol_python_sdk.utils.registration.transform_registration_request import ( transform_distribute_royalty_tokens_request, ) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 2c8d748c..1c0fc5ba 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -24,10 +24,8 @@ IPAccountImplClient, ) from story_protocol_python_sdk.resources.IPAsset import IPAsset -from story_protocol_python_sdk.types.resource.IPAsset import ( - BatchMintAndRegisterIPInput, - TransformedRegistrationRequest, -) +from story_protocol_python_sdk.types.resource.IPAsset import BatchMintAndRegisterIPInput +from story_protocol_python_sdk.types.utils import TransformedRegistrationRequest from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput from story_protocol_python_sdk.utils.ip_metadata import IPMetadata, IPMetadataInput from story_protocol_python_sdk.utils.royalty import get_royalty_shares diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index b9b4f827..e784ab41 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -4,9 +4,9 @@ from ens.ens import HexStr from story_protocol_python_sdk import RoyaltyShareInput -from story_protocol_python_sdk.types.resource.IPAsset import ( +from story_protocol_python_sdk.types.resource.IPAsset import IPRoyaltyVault +from story_protocol_python_sdk.types.utils import ( ExtraData, - IPRoyaltyVault, TransformedRegistrationRequest, ) from story_protocol_python_sdk.utils.registration.registration_utils import ( From 95416cc0fe64599347d2ac86a8feea64a529e254 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 14:30:19 +0800 Subject: [PATCH 47/52] refactor: update royalty_shares type in ExtraData to use RoyaltyShareInput for improved type safety --- src/story_protocol_python_sdk/types/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/types/utils.py b/src/story_protocol_python_sdk/types/utils.py index 210180ac..55267bae 100644 --- a/src/story_protocol_python_sdk/types/utils.py +++ b/src/story_protocol_python_sdk/types/utils.py @@ -4,6 +4,8 @@ from ens.ens import Address, HexStr from typing_extensions import Callable +from story_protocol_python_sdk.types.resource.Royalty import RoyaltyShareInput + class Multicall3Call(TypedDict): target: Address @@ -36,7 +38,7 @@ class ExtraData(TypedDict, total=False): license_terms_data: [Optional] The license terms data. """ - royalty_shares: list[dict] + royalty_shares: list[RoyaltyShareInput] deadline: int royalty_total_amount: int nft_contract: Address From 50a9c92bac0d5204e9f8a326497d96735bd5a805 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 14:37:51 +0800 Subject: [PATCH 48/52] refactor: rename original_method_reference to workflow_multicall_reference for consistency across registration requests --- src/story_protocol_python_sdk/types/utils.py | 3 +- .../utils/registration/registration_utils.py | 3 +- .../transform_registration_request.py | 18 +++++----- tests/unit/resources/test_ip_asset.py | 22 ++++++------ tests/unit/utils/test_registration_utils.py | 36 +++++++++---------- .../test_transform_registration_request.py | 20 +++++------ 6 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/story_protocol_python_sdk/types/utils.py b/src/story_protocol_python_sdk/types/utils.py index 55267bae..9f508351 100644 --- a/src/story_protocol_python_sdk/types/utils.py +++ b/src/story_protocol_python_sdk/types/utils.py @@ -64,6 +64,5 @@ class TransformedRegistrationRequest: is_use_multicall3: bool workflow_address: Address validated_request: list[Address | int | str | bytes | dict | bool] - # TODO: need to rename with multicall3 method reference - original_method_reference: Callable[..., HexStr] + workflow_multicall_reference: Callable[..., HexStr] extra_data: ExtraData | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 23034680..6c6a3d29 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -44,11 +44,10 @@ def aggregate_multicall_requests( aggregated_requests[target_address] = { "call_data": [], "license_terms_data": [], - # TODO: rename with multicall3 method reference "method_reference": ( multicall3_client.build_aggregate3_transaction if target_address == multicall3_client.contract.address - else request.original_method_reference + else request.workflow_multicall_reference ), } if target_address == multicall3_client.contract.address: diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 14905554..f6e156aa 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -238,7 +238,7 @@ def transform_distribute_royalty_tokens_request( is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_client.contract.address, validated_request=validated_request, - original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, + workflow_multicall_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, extra_data=None, ) @@ -375,7 +375,7 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( # Because mint tokens is given `msg.sender` as the recipient, so we need to set `useMulticall3` to false. is_use_multicall3=False, workflow_address=royalty_token_distribution_workflows_address, - original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, + workflow_multicall_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, validated_request=validated_request, extra_data=ExtraData( license_terms_data=license_terms_data, @@ -421,7 +421,7 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( workflow_address=royalty_token_distribution_workflows_address, validated_request=validated_request, extra_data=None, - original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, + workflow_multicall_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -459,7 +459,7 @@ def _handle_mint_and_register_with_license_terms( extra_data=ExtraData( license_terms_data=license_terms_data, ), - original_method_reference=license_attachment_workflows_client.build_multicall_transaction, + workflow_multicall_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -493,7 +493,7 @@ def _handle_mint_and_register_with_derivative( workflow_address=derivative_workflows_address, validated_request=validated_request, extra_data=None, - original_method_reference=derivative_workflows_client.build_multicall_transaction, + workflow_multicall_reference=derivative_workflows_client.build_multicall_transaction, ) @@ -688,7 +688,7 @@ def _handle_register_with_license_terms_and_royalty_vault( token_id=token_id, license_terms_data=license_terms_data, ), - original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, + workflow_multicall_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -754,7 +754,7 @@ def _handle_register_with_derivative_and_royalty_vault( nft_contract=nft_contract, token_id=token_id, ), - original_method_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, + workflow_multicall_reference=royalty_token_distribution_workflows_client.build_multicall_transaction, ) @@ -813,7 +813,7 @@ def _handle_register_with_license_terms( extra_data=ExtraData( license_terms_data=license_terms_data, ), - original_method_reference=license_attachment_workflows_client.build_multicall_transaction, + workflow_multicall_reference=license_attachment_workflows_client.build_multicall_transaction, ) @@ -868,7 +868,7 @@ def _handle_register_with_derivative( workflow_address=derivative_workflows_address, validated_request=validated_request, extra_data=None, - original_method_reference=derivative_workflows_client.build_multicall_transaction, + workflow_multicall_reference=derivative_workflows_client.build_multicall_transaction, ) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 1c0fc5ba..5dbfe15d 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -3771,7 +3771,7 @@ def test_batch_mint_and_register_with_license_terms( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transformed_2 = TransformedRegistrationRequest( @@ -3779,7 +3779,7 @@ def test_batch_mint_and_register_with_license_terms( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transform.side_effect = [mock_transformed_1, mock_transformed_2] @@ -3866,7 +3866,7 @@ def test_batch_register_with_royalty_shares( is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data={"royalty_shares": royalty_shares}, ) mock_transform.return_value = mock_transformed @@ -3889,7 +3889,7 @@ def test_batch_register_with_royalty_shares( is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_prepare.return_value = ([mock_distribute_request], []) @@ -3940,7 +3940,7 @@ def test_batch_mixed_requests( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transformed_2 = TransformedRegistrationRequest( @@ -3948,7 +3948,7 @@ def test_batch_mixed_requests( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transform.side_effect = [mock_transformed_1, mock_transformed_2] @@ -4011,7 +4011,7 @@ def test_batch_with_multicall_disabled( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transformed_2 = TransformedRegistrationRequest( @@ -4019,7 +4019,7 @@ def test_batch_with_multicall_disabled( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transform.side_effect = [mock_transformed_1, mock_transformed_2] @@ -4076,7 +4076,7 @@ def test_batch_with_royalty_shares_and_license_terms( is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data={"royalty_shares": royalty_shares}, ) mock_transform.return_value = mock_transformed @@ -4097,7 +4097,7 @@ def test_batch_with_royalty_shares_and_license_terms( is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_prepare.return_value = ( @@ -4174,7 +4174,7 @@ def test_batch_transaction_failure( is_use_multicall3=True, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), extra_data=None, ) mock_transform.return_value = mock_transformed diff --git a/tests/unit/utils/test_registration_utils.py b/tests/unit/utils/test_registration_utils.py index e784ab41..fa818fab 100644 --- a/tests/unit/utils/test_registration_utils.py +++ b/tests/unit/utils/test_registration_utils.py @@ -45,7 +45,7 @@ def test_aggregates_single_request(self, mock_web3, mock_multicall3_client): is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=contract_call, + workflow_multicall_reference=contract_call, ) result = aggregate_multicall_requests( requests=[request], @@ -73,14 +73,14 @@ def test_aggregates_multiple_requests_same_address( is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=contract_call, + workflow_multicall_reference=contract_call, ) request_2 = TransformedRegistrationRequest( encoded_tx_data=encoded_data_2, is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=contract_call, + workflow_multicall_reference=contract_call, extra_data=ExtraData( license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], ), @@ -124,14 +124,14 @@ def test_aggregates_multiple_requests_different_addresses( is_use_multicall3=False, workflow_address=workflow_address_1, validated_request=[], - original_method_reference=contract_call_1, + workflow_multicall_reference=contract_call_1, ) request_2 = TransformedRegistrationRequest( encoded_tx_data=encoded_data_2, is_use_multicall3=False, workflow_address=workflow_address_2, validated_request=[], - original_method_reference=contract_call_2, + workflow_multicall_reference=contract_call_2, extra_data=ExtraData( license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], ), @@ -141,7 +141,7 @@ def test_aggregates_multiple_requests_different_addresses( is_use_multicall3=False, workflow_address=workflow_address_2, validated_request=[], - original_method_reference=contract_call_2, + workflow_multicall_reference=contract_call_2, extra_data=ExtraData( royalty_shares=royalty_shares, ), @@ -189,14 +189,14 @@ def test_uses_multicall3_address_when_enabled( is_use_multicall3=True, workflow_address="workflow1", validated_request=[], - original_method_reference=contract_call1, + workflow_multicall_reference=contract_call1, ) request2 = TransformedRegistrationRequest( encoded_tx_data=encoded_data_2, is_use_multicall3=True, workflow_address="workflow2", validated_request=[], - original_method_reference=contract_call2, + workflow_multicall_reference=contract_call2, extra_data=ExtraData( license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], ), @@ -258,7 +258,7 @@ def test_uses_workflow_address_when_multicall3_disabled( is_use_multicall3=True, # Request wants to use multicall3 workflow_address="workflow1", validated_request=[], - original_method_reference=contract_call1, + workflow_multicall_reference=contract_call1, extra_data=ExtraData( license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], ), @@ -268,7 +268,7 @@ def test_uses_workflow_address_when_multicall3_disabled( is_use_multicall3=True, workflow_address="workflow2", validated_request=[], - original_method_reference=contract_call2, + workflow_multicall_reference=contract_call2, ) result = aggregate_multicall_requests( requests=[request1, request2], @@ -317,7 +317,7 @@ def test_aggregates_mixed_requests_with_multicall3( is_use_multicall3=True, workflow_address=workflow_address_1, validated_request=[], - original_method_reference=contract_call_1, + workflow_multicall_reference=contract_call_1, extra_data=ExtraData( license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], ), @@ -328,7 +328,7 @@ def test_aggregates_mixed_requests_with_multicall3( is_use_multicall3=True, workflow_address=workflow_address_2, validated_request=[], - original_method_reference=contract_call_2, + workflow_multicall_reference=contract_call_2, ) # Request 3: doesn't use multicall3 request_3 = TransformedRegistrationRequest( @@ -336,7 +336,7 @@ def test_aggregates_mixed_requests_with_multicall3( is_use_multicall3=False, workflow_address=workflow_address_1, validated_request=[], - original_method_reference=contract_call_3, + workflow_multicall_reference=contract_call_3, ) result = aggregate_multicall_requests( @@ -409,7 +409,7 @@ def _mock(): is_use_multicall3=False, workflow_address=ADDRESS, validated_request=[], - original_method_reference=MagicMock(), + workflow_multicall_reference=MagicMock(), ), ) @@ -671,7 +671,7 @@ def test_sends_single_transaction( is_use_multicall3=False, workflow_address=workflow_address, validated_request=[], - original_method_reference=method_reference, + workflow_multicall_reference=method_reference, ) # Mock build_and_send_transaction return value @@ -734,21 +734,21 @@ def test_sends_multiple_transactions_to_different_addresses( is_use_multicall3=True, workflow_address=workflow_address_1, validated_request=[], - original_method_reference=method_reference_1, + workflow_multicall_reference=method_reference_1, ) transformed_request_2 = TransformedRegistrationRequest( encoded_tx_data=b"data2", is_use_multicall3=False, workflow_address=workflow_address_2, validated_request=[], - original_method_reference=method_reference_2, + workflow_multicall_reference=method_reference_2, ) transformed_request_3 = TransformedRegistrationRequest( encoded_tx_data=b"data3", is_use_multicall3=True, workflow_address=workflow_address_1, validated_request=[], - original_method_reference=method_reference_1, + workflow_multicall_reference=method_reference_1, extra_data=ExtraData( license_terms_data=[LICENSE_TERMS_DATA_CAMEL_CASE], ), diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index 5f200192..cd8aad8c 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -454,7 +454,7 @@ def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_pres license_terms_data = result.extra_data.get("license_terms_data") assert license_terms_data is not None assert license_terms_data[0] == LICENSE_TERMS_DATA_CAMEL_CASE - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_id_present( self, @@ -509,7 +509,7 @@ def test_routes_to_register_ip_and_attach_pil_terms_when_nft_contract_and_token_ assert license_terms_data is not None assert license_terms_data[0] == LICENSE_TERMS_DATA_CAMEL_CASE assert result.is_use_multicall3 is False - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_raises_error_for_invalid_request_type( self, mock_web3, mock_ip_asset_registry_client @@ -594,7 +594,7 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens assert args[4][0]["recipient"] == ADDRESS assert args[4][0]["percentage"] == 50 * 10**6 assert args[5] is True # allow_duplicates (default for this method) - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( self, @@ -649,7 +649,7 @@ def test_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( assert args[4][0]["recipient"] == ADDRESS # royalty_shares assert args[4][0]["percentage"] == 50 * 10**6 # royalty_shares assert args[5] is True # allow_duplicates (default for this method) - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_mint_and_register_ip_and_make_derivative( self, @@ -699,7 +699,7 @@ def test_mint_and_register_ip_and_make_derivative( assert ( call_args[1]["args"][4] is False ) # allow_duplicates (default for this method) - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_raises_error_for_invalid_mint_and_register_request_type( self, @@ -792,7 +792,7 @@ def test_register_ip_and_attach_pil_terms_and_deploy_royalty_vault( assert royalty_share_dict["recipient"] == ADDRESS assert royalty_share_dict["percentage"] == 50 * 10**6 assert result.extra_data["deadline"] == 2000 - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_register_ip_and_make_derivative_and_deploy_royalty_vault( self, @@ -857,7 +857,7 @@ def test_register_ip_and_make_derivative_and_deploy_royalty_vault( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_register_ip_and_attach_pil_terms( self, @@ -915,7 +915,7 @@ def test_register_ip_and_attach_pil_terms( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_register_ip_and_make_derivative( self, @@ -973,7 +973,7 @@ def test_register_ip_and_make_derivative( assert args[4]["signer"] == ACCOUNT_ADDRESS assert args[4]["deadline"] == 1000 assert args[4]["signature"] == b"signature" - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None def test_raises_error_when_ip_not_registered( self, @@ -1087,7 +1087,7 @@ def test_transforms_distribute_royalty_tokens_request_successfully( == "royalty_token_distribution_workflows_client_address" ) assert result.extra_data is None - assert result.original_method_reference is not None + assert result.workflow_multicall_reference is not None # Verify validated_request structure assert result.validated_request[0] == ip_id From ad4897141e3795f72d8282dc1650be706c54f62d Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 14:48:43 +0800 Subject: [PATCH 49/52] refactor:remove todo to maintance --- .../utils/registration/transform_registration_request.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index f6e156aa..2eaa1032 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -538,7 +538,6 @@ def _handle_register_request( if request.license_terms_data else None ) - # TODO:consider some validation to extract in common place deriv_data = ( DerivativeData.from_input( web3=web3, input_data=request.deriv_data From 7267493bd8d933a7f6822a3a2a044fd7f2cde9b6 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 15:06:48 +0800 Subject: [PATCH 50/52] refactor: simplify allow_duplicates handling by removing legacy function and setting default value in MintAndRegisterRequest --- .../types/resource/IPAsset.py | 3 +- .../transform_registration_request.py | 32 ++------------ .../test_transform_registration_request.py | 42 +------------------ 3 files changed, 7 insertions(+), 70 deletions(-) diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index 27063bbe..f8187a5b 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -269,8 +269,7 @@ class MintAndRegisterRequest: spg_nft_contract: Address recipient: Address | None = None - # TODO: need to consider how to handle new method and existing method - allow_duplicates: bool | None = None + allow_duplicates: bool | None = True ip_metadata: IPMetadataInput | None = None license_terms_data: list[LicenseTermsDataInput] | None = None deriv_data: DerivativeDataInput | None = None diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index 2eaa1032..ccba0077 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -141,30 +141,6 @@ def validate_license_terms_data( return validated_license_terms_data -def get_allow_duplicates(allow_duplicates: bool | None, request_type: str) -> bool: - """ - Get the allow duplicates value based on the request type. - Due to history reasons, we need to use different allow duplicates values for different request types. - In the future, we need to unified the allow duplicates logic for all request types, but it can cause breaking changes. - Args: - allow_duplicates: The allow duplicates value. - request_type: The request type. - Returns: - The allow duplicates value. - """ - ALLOW_DUPLICATES_MAP = { - "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens": True, - "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens": True, - "mintAndRegisterIpAndAttachPILTerms": False, - "mintAndRegisterIpAndMakeDerivative": True, - } - return ( - allow_duplicates - if allow_duplicates is not None - else ALLOW_DUPLICATES_MAP[request_type] - ) - - def transform_request( request: IpRegistrationWorkflowRequest, web3: Web3, @@ -363,7 +339,7 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( metadata, license_terms_data, royalty_shares, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + allow_duplicates, ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, @@ -408,7 +384,7 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( metadata, deriv_data, royalty_shares, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + allow_duplicates, ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, @@ -444,7 +420,7 @@ def _handle_mint_and_register_with_license_terms( recipient, metadata, license_terms_data, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + allow_duplicates, ] encoded_data = license_attachment_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, @@ -480,7 +456,7 @@ def _handle_mint_and_register_with_derivative( deriv_data, metadata, recipient, - get_allow_duplicates(allow_duplicates, abi_element_identifier), + allow_duplicates, ] encoded_data = derivative_workflows_client.contract.encode_abi( abi_element_identifier=abi_element_identifier, diff --git a/tests/unit/utils/test_transform_registration_request.py b/tests/unit/utils/test_transform_registration_request.py index cd8aad8c..01505ec8 100644 --- a/tests/unit/utils/test_transform_registration_request.py +++ b/tests/unit/utils/test_transform_registration_request.py @@ -22,7 +22,6 @@ ) from story_protocol_python_sdk.utils.ip_metadata import IPMetadata from story_protocol_python_sdk.utils.registration.transform_registration_request import ( - get_allow_duplicates, get_public_minting, transform_distribute_royalty_tokens_request, transform_request, @@ -263,39 +262,6 @@ def _mock(public_minting: bool = True): ACCOUNT_ADDRESS = account.address -class TestGetAllowDuplicates: - def test_returns_default_for_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( - self, - ): - result = get_allow_duplicates( - None, "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" - ) - assert result is True - - def test_returns_default_for_mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens( - self, - ): - result = get_allow_duplicates( - None, "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" - ) - assert result is True - - def test_returns_default_for_mint_and_register_ip_and_attach_pil_terms(self): - result = get_allow_duplicates(None, "mintAndRegisterIpAndAttachPILTerms") - assert result is False - - def test_returns_default_for_mint_and_register_ip_and_make_derivative(self): - result = get_allow_duplicates(None, "mintAndRegisterIpAndMakeDerivative") - assert result is True - - def test_returns_provided_value_when_not_none(self): - result = get_allow_duplicates(False, "mintAndRegisterIpAndAttachPILTerms") - assert result is False - - result = get_allow_duplicates(True, "mintAndRegisterIpAndAttachPILTerms") - assert result is True - - class TestGetPublicMinting: def test_returns_true_when_public_minting_enabled( self, mock_web3, mock_spg_nft_client @@ -447,7 +413,7 @@ def test_routes_to_mint_and_register_attach_pil_terms_when_spg_nft_contract_pres args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() ) # metadata assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data - assert args[4] is False # allow_duplicates (default for this method) + assert args[4] is True # allow_duplicates assert result.workflow_address == "license_attachment_client_address" assert result.is_use_multicall3 is True assert result.extra_data is not None @@ -588,9 +554,7 @@ def test_mint_and_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens assert ( args[2] == IPMetadata.from_input(IP_METADATA).get_validated_data() ) # metadata - # license_terms_data is validated, so we check it's not None assert args[3][0] == LICENSE_TERMS_DATA_CAMEL_CASE # license_terms_data - # royalty_shares is processed, so we check it's a list with correct structure assert args[4][0]["recipient"] == ADDRESS assert args[4][0]["percentage"] == 50 * 10**6 assert args[5] is True # allow_duplicates (default for this method) @@ -696,9 +660,7 @@ def test_mint_and_register_ip_and_make_derivative( == IPMetadata.from_input(IP_METADATA).get_validated_data() ) # metadata assert call_args[1]["args"][3] == ACCOUNT_ADDRESS # recipient - assert ( - call_args[1]["args"][4] is False - ) # allow_duplicates (default for this method) + assert call_args[1]["args"][4] is False # allow_duplicates assert result.workflow_multicall_reference is not None def test_raises_error_for_invalid_mint_and_register_request_type( From f2028746aca2a53f882ea65a431556d60b00a22c Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 17:27:20 +0800 Subject: [PATCH 51/52] refactor: streamline IPAsset methods and improve code readability by replacing loops with comprehensions and updating comments for clarity --- .../resources/IPAsset.py | 103 +++++++++--------- .../types/resource/IPAsset.py | 8 +- src/story_protocol_python_sdk/types/utils.py | 2 +- .../utils/registration/registration_utils.py | 64 ++++++----- .../transform_registration_request.py | 30 ++--- 5 files changed, 103 insertions(+), 104 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 52c4c8f6..a69a0d77 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -547,14 +547,10 @@ def mint_and_register_ip_asset_with_pil_terms( recipient=recipient, ip_metadata=( IPMetadataInput( - ip_metadata_uri=ip_metadata.get("ip_metadata_uri", ""), - ip_metadata_hash=ip_metadata.get( - "ip_metadata_hash", ZERO_HASH - ), - nft_metadata_uri=ip_metadata.get("nft_metadata_uri", ""), - nft_metadata_hash=ip_metadata.get( - "nft_metadata_hash", ZERO_HASH - ), + ip_metadata_uri=ip_metadata["ip_metadata_uri"], + ip_metadata_hash=ip_metadata["ip_metadata_hash"], + nft_metadata_uri=ip_metadata["nft_metadata_uri"], + nft_metadata_hash=ip_metadata["nft_metadata_hash"], ) if ip_metadata else None @@ -1516,18 +1512,18 @@ def batch_ip_asset_with_optimized_workflows( **Workflow Selection:** For `MintAndRegisterRequest` (supports Multicall3 when `spg_nft_contract` has public minting enabled): - 1. `license_terms_data` + `royalty_shares` → `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens` (RoyaltyTokenDistributionWorkflows) + 1. `license_terms_data` + `royalty_shares` → `mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens` (contract method) - Note: Always uses workflow's native multicall due to `msg.sender` limitation - 2. `license_terms_data` → `mintAndRegisterIpAndAttachPILTerms` (LicenseAttachmentWorkflows) - 3. `deriv_data` + `royalty_shares` → `mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens` (RoyaltyTokenDistributionWorkflows) - 4. `deriv_data` → `mintAndRegisterIpAndMakeDerivative` (DerivativeWorkflows) + 2. `license_terms_data` → `mintAndRegisterIpAndAttachPILTerms` + 3. `deriv_data` + `royalty_shares` → `mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens` (contract method) + 4. `deriv_data` → `mintAndRegisterIpAndMakeDerivative` (contract method) 5. Other combinations throw `Invalid mint and register request type` error For `RegisterRegistrationRequest` (always uses workflow's native multicall due to signature requirements): - 1. `license_terms_data` + `royalty_shares` → `registerIpAndAttachPILTermsAndDeployRoyaltyVault` (RoyaltyTokenDistributionWorkflows) - 2. `deriv_data` + `royalty_shares` → `registerIpAndMakeDerivativeAndDeployRoyaltyVault` (RoyaltyTokenDistributionWorkflows) - 3. `license_terms_data` → `registerIpAndAttachPILTerms` (LicenseAttachmentWorkflows) - 4. `deriv_data` → `registerIpAndMakeDerivative` (DerivativeWorkflows) + 1. `license_terms_data` + `royalty_shares` → `registerIpAndAttachPILTermsAndDeployRoyaltyVault` (contract method) + 2. `deriv_data` + `royalty_shares` → `registerIpAndMakeDerivativeAndDeployRoyaltyVault` (contract method) + 3. `license_terms_data` → `registerIpAndAttachPILTerms` (contract method) + 4. `deriv_data` → `registerIpAndMakeDerivative` (contract method) 5. Other combinations throw `Invalid register request type` error **Multicall Strategy:** @@ -1672,17 +1668,19 @@ def _populate_license_terms_ids_into_response( for value in aggregated_requests.values() for license_terms_data in value["license_terms_data"] ] + + # Create an iterator to automatically consume license_terms_data + license_terms_iter = iter(all_license_terms_data) + # Populate license terms ids for each registered IP - license_terms_index = 0 for registration_result in registration_results: for registered_ip in registration_result["registered_ips"]: - if license_terms_index < len(all_license_terms_data): - license_terms_data = all_license_terms_data[license_terms_index] - if license_terms_data: - registered_ip["license_terms_ids"] = self._get_license_terms_id( - license_terms_data - ) - license_terms_index += 1 + license_terms_data = next(license_terms_iter, None) + if license_terms_data: + registered_ip["license_terms_ids"] = self._get_license_terms_id( + license_terms_data + ) + return registration_results def _get_license_terms_id(self, license_terms_data: list[dict]) -> list[int]: @@ -1691,13 +1689,10 @@ def _get_license_terms_id(self, license_terms_data: list[dict]) -> list[int]: :param license_terms_data: The license terms data. :return: The license terms ids. """ - license_terms_ids = [] - for license_terms in license_terms_data: - license_terms_id = self.pi_license_template_client.getLicenseTermsId( - license_terms["terms"] - ) - license_terms_ids.append(license_terms_id) - return license_terms_ids + return [ + self.pi_license_template_client.getLicenseTermsId(license_terms["terms"]) + for license_terms in license_terms_data + ] def _handle_minted_nft_registration( self, @@ -2145,13 +2140,13 @@ def _parse_tx_ip_registered_event( event_signature = self.web3.keccak( text="IPRegistered(address,uint256,address,uint256,string,string,uint256)" ).hex() - registered_ip_logs: list[dict[str, int | Address]] = [] - for log in tx_receipt["logs"]: - if log["topics"][0].hex() == event_signature: - event_result = self.ip_asset_registry_client.contract.events.IPRegistered.process_log( - log - ) - registered_ip_logs.append(event_result["args"]) + registered_ip_logs: list[dict[str, int | Address]] = [ + self.ip_asset_registry_client.contract.events.IPRegistered.process_log(log)[ + "args" + ] + for log in tx_receipt["logs"] + if log["topics"][0].hex() == event_signature + ] return registered_ip_logs def _get_registered_ips(self, tx_receipt: dict) -> list[RegisteredIP]: @@ -2162,11 +2157,10 @@ def _get_registered_ips(self, tx_receipt: dict) -> list[RegisteredIP]: :return list[RegisteredIP]: The list of registered IPs. """ registered_ip_logs = self._parse_tx_ip_registered_event(tx_receipt) - registered_ips = [ + return [ RegisteredIP(ip_id=log["ipId"], token_id=log["tokenId"]) for log in registered_ip_logs ] - return registered_ips def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int | None: """ @@ -2219,12 +2213,14 @@ def _get_royalty_vault_address_by_ip_id( ip_royalty_vault_deployed_events = ( self._parse_all_ip_royalty_vault_deployed_events(tx_receipt) ) - filtered_ip_royalty_vault_deployed_events: list[dict] = list( - filter(lambda x: x["ipId"] == ip_id, ip_royalty_vault_deployed_events) + return next( + ( + event["ipRoyaltyVault"] + for event in ip_royalty_vault_deployed_events + if event["ipId"] == ip_id + ), + None, ) - if filtered_ip_royalty_vault_deployed_events: - return filtered_ip_royalty_vault_deployed_events[0]["ipRoyaltyVault"] - return None def _validate_recipient(self, recipient: Address | None) -> Address: """ @@ -2249,11 +2245,12 @@ def _parse_all_ip_royalty_vault_deployed_events( event_signature = Web3.keccak( text="IpRoyaltyVaultDeployed(address,address)" ).hex() - results: list[dict[str, Address]] = [] - for log in tx_receipt["logs"]: - if log["topics"][0].hex() == event_signature: - event_result = self.royalty_module_client.contract.events.IpRoyaltyVaultDeployed.process_log( - log - ) - results.append(event_result["args"]) - return results + return [ + self.royalty_module_client.contract.events.IpRoyaltyVaultDeployed.process_log( + log + )[ + "args" + ] + for log in tx_receipt["logs"] + if log["topics"][0].hex() == event_signature + ] diff --git a/src/story_protocol_python_sdk/types/resource/IPAsset.py b/src/story_protocol_python_sdk/types/resource/IPAsset.py index f8187a5b..2751c20c 100644 --- a/src/story_protocol_python_sdk/types/resource/IPAsset.py +++ b/src/story_protocol_python_sdk/types/resource/IPAsset.py @@ -262,8 +262,8 @@ class MintAndRegisterRequest: recipient: [Optional] The address to receive the NFT. Defaults to caller's wallet address. allow_duplicates: [Optional] Set to true to allow minting an NFT with a duplicate metadata hash. (default: True) ip_metadata: [Optional] The metadata for the newly minted NFT and registered IP. - license_terms_data: [Optional] The license terms data to attach. Required if not using deriv_data. - deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using license_terms_data. + license_terms_data: [Optional] The license terms data to attach. Required if not using `deriv_data`. + deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using `license_terms_data`. royalty_shares: [Optional] The royalty shares for distributing royalty tokens. Must be specified together with either `license_terms_data` or `deriv_data`. """ @@ -292,8 +292,8 @@ class RegisterRegistrationRequest: token_id: The token ID of the NFT. ip_metadata: [Optional] The metadata for the registered IP. deadline: [Optional] The deadline for the signature in seconds. (default: 1000) - license_terms_data: [Optional] The license terms data to attach. Required if not using deriv_data. - deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using license_terms_data. + license_terms_data: [Optional] The license terms data to attach. Required if not using `deriv_data`. + deriv_data: [Optional] The derivative data for creating derivative IP. Required if not using `license_terms_data`. royalty_shares: [Optional] The royalty shares for distributing royalty tokens. Must be specified together with either `license_terms_data` or `deriv_data`. """ diff --git a/src/story_protocol_python_sdk/types/utils.py b/src/story_protocol_python_sdk/types/utils.py index 9f508351..a4e97e10 100644 --- a/src/story_protocol_python_sdk/types/utils.py +++ b/src/story_protocol_python_sdk/types/utils.py @@ -56,7 +56,7 @@ class TransformedRegistrationRequest: is_use_multicall3: Whether to use multicall3 or SPG's native multicall. workflow_address: The workflow contract address. validated_request: The validated request arguments for the contract method. - original_method_reference: The original method reference for building transactions. + workflow_multicall_reference: The multicall reference for the workflow. extra_data: [Optional] Extra data for post-processing. """ diff --git a/src/story_protocol_python_sdk/utils/registration/registration_utils.py b/src/story_protocol_python_sdk/utils/registration/registration_utils.py index 6c6a3d29..6ef57ae7 100644 --- a/src/story_protocol_python_sdk/utils/registration/registration_utils.py +++ b/src/story_protocol_python_sdk/utils/registration/registration_utils.py @@ -88,32 +88,44 @@ def prepare_distribute_royalty_tokens_requests( transformed_requests: list[TransformedRegistrationRequest] = [] matching_vaults: list[IPRoyaltyVault] = [] for extra_data in extra_data_list: - filtered_ip_registered = [ - x - for x in ip_registered - if x["tokenContract"] == extra_data["nft_contract"] - and x["tokenId"] == extra_data["token_id"] - ] - if filtered_ip_registered: - ip_id = filtered_ip_registered[0]["ipId"] - matching_vault = [x for x in royalty_vault if x["ipId"] == ip_id] - if not matching_vault: - continue - ip_royalty_vault = matching_vault[0]["ipRoyaltyVault"] - matching_vaults.append( - IPRoyaltyVault(ip_id=ip_id, royalty_vault=ip_royalty_vault) - ) - transformed_request = transform_distribute_royalty_tokens_request( - ip_id=ip_id, - royalty_vault=ip_royalty_vault, - deadline=extra_data["deadline"], - web3=web3, - account=account, - chain_id=chain_id, - royalty_shares=extra_data["royalty_shares"], - total_amount=extra_data["royalty_total_amount"], - ) - transformed_requests.append(transformed_request) + # Find matching IP registration + ip_registered_match = next( + ( + x + for x in ip_registered + if x["tokenContract"] == extra_data["nft_contract"] + and x["tokenId"] == extra_data["token_id"] + ), + None, + ) + if not ip_registered_match: + continue + + ip_id = ip_registered_match["ipId"] + + # Find matching royalty vault + matching_vault = next( + (x for x in royalty_vault if x["ipId"] == ip_id), + None, + ) + if not matching_vault: + continue + + ip_royalty_vault = matching_vault["ipRoyaltyVault"] + matching_vaults.append( + IPRoyaltyVault(ip_id=ip_id, royalty_vault=ip_royalty_vault) + ) + transformed_request = transform_distribute_royalty_tokens_request( + ip_id=ip_id, + royalty_vault=ip_royalty_vault, + deadline=extra_data["deadline"], + web3=web3, + account=account, + chain_id=chain_id, + royalty_shares=extra_data["royalty_shares"], + total_amount=extra_data["royalty_total_amount"], + ) + transformed_requests.append(transformed_request) return transformed_requests, matching_vaults diff --git a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py index ccba0077..0cd079e5 100644 --- a/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py +++ b/src/story_protocol_python_sdk/utils/registration/transform_registration_request.py @@ -330,9 +330,7 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( royalty_token_distribution_workflows_address = ( royalty_token_distribution_workflows_client.contract.address ) - abi_element_identifier = ( - "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens" - ) + validated_request = [ spg_nft_contract, recipient, @@ -342,7 +340,7 @@ def _handle_mint_and_register_with_license_terms_and_royalty_tokens( allow_duplicates, ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", args=validated_request, ) @@ -375,9 +373,7 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( royalty_token_distribution_workflows_address = ( royalty_token_distribution_workflows_client.contract.address ) - abi_element_identifier = ( - "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens" - ) + validated_request = [ spg_nft_contract, recipient, @@ -387,7 +383,7 @@ def _handle_mint_and_register_with_derivative_and_royalty_tokens( allow_duplicates, ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", args=validated_request, ) @@ -414,7 +410,6 @@ def _handle_mint_and_register_with_license_terms( license_attachment_workflows_address = ( license_attachment_workflows_client.contract.address ) - abi_element_identifier = "mintAndRegisterIpAndAttachPILTerms" validated_request = [ spg_nft_contract, recipient, @@ -423,7 +418,7 @@ def _handle_mint_and_register_with_license_terms( allow_duplicates, ] encoded_data = license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="mintAndRegisterIpAndAttachPILTerms", args=validated_request, ) @@ -450,7 +445,6 @@ def _handle_mint_and_register_with_derivative( ) -> TransformedRegistrationRequest: derivative_workflows_client = DerivativeWorkflowsClient(web3) derivative_workflows_address = derivative_workflows_client.contract.address - abi_element_identifier = "mintAndRegisterIpAndMakeDerivative" validated_request = [ spg_nft_contract, deriv_data, @@ -459,7 +453,7 @@ def _handle_mint_and_register_with_derivative( allow_duplicates, ] encoded_data = derivative_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="mintAndRegisterIpAndMakeDerivative", args=validated_request, ) @@ -633,7 +627,6 @@ def _handle_register_with_license_terms_and_royalty_vault( licensing_module_client=licensing_module_client, ), ) - abi_element_identifier = "registerIpAndAttachPILTermsAndDeployRoyaltyVault" validated_request = [ nft_contract, token_id, @@ -646,7 +639,7 @@ def _handle_register_with_license_terms_and_royalty_vault( }, ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="registerIpAndAttachPILTermsAndDeployRoyaltyVault", args=validated_request, ) @@ -700,7 +693,6 @@ def _handle_register_with_derivative_and_royalty_vault( licensing_module_client=licensing_module_client, ), ) - abi_element_identifier = "registerIpAndMakeDerivativeAndDeployRoyaltyVault" validated_request = [ nft_contract, token_id, @@ -713,7 +705,7 @@ def _handle_register_with_derivative_and_royalty_vault( }, ] encoded_data = royalty_token_distribution_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="registerIpAndMakeDerivativeAndDeployRoyaltyVault", args=validated_request, ) @@ -763,7 +755,6 @@ def _handle_register_with_license_terms( licensing_module_client=licensing_module_client, ), ) - abi_element_identifier = "registerIpAndAttachPILTerms" validated_request = [ nft_contract, token_id, @@ -776,7 +767,7 @@ def _handle_register_with_license_terms( }, ] encoded_data = license_attachment_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="registerIpAndAttachPILTerms", args=validated_request, ) @@ -820,7 +811,6 @@ def _handle_register_with_derivative( licensing_module_client=licensing_module_client, ), ) - abi_element_identifier = "registerIpAndMakeDerivative" validated_request = [ nft_contract, token_id, @@ -833,7 +823,7 @@ def _handle_register_with_derivative( }, ] encoded_data = derivative_workflows_client.contract.encode_abi( - abi_element_identifier=abi_element_identifier, + abi_element_identifier="registerIpAndMakeDerivative", args=validated_request, ) From b5639823c3e4eddbdf77233835836ce567262feb Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 27 Jan 2026 17:31:00 +0800 Subject: [PATCH 52/52] refactor: update default minting fees in TestBatchRegisterIpAssetsWithOptimizedWorkflows for consistency and accuracy --- .../integration/test_integration_ip_asset.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index ee1f6134..5b4cecf6 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -1806,13 +1806,13 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_register_registr license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.commercial_use( - default_minting_fee=1000000000000000000, + default_minting_fee=1, currency=MockERC20, royalty_policy=NativeRoyaltyPolicy.LAP, ), licensing_config=LicensingConfig( is_set=True, - minting_fee=1000000000000000000, + minting_fee=1, licensing_hook=ZERO_ADDRESS, hook_data=ZERO_HASH, commercial_rev_share=50, @@ -1899,13 +1899,13 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_register_registr license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.commercial_use( - default_minting_fee=1000000000000000000, + default_minting_fee=10, currency=MockERC20, royalty_policy=NativeRoyaltyPolicy.LAP, ), licensing_config=LicensingConfig( is_set=True, - minting_fee=1000000000000000000, + minting_fee=10, licensing_hook=ZERO_ADDRESS, hook_data=ZERO_HASH, commercial_rev_share=50, @@ -1990,13 +1990,13 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.commercial_use( - default_minting_fee=1000000000000000000, + default_minting_fee=10, currency=MockERC20, royalty_policy=NativeRoyaltyPolicy.LAP, ), licensing_config=LicensingConfig( is_set=True, - minting_fee=1000000000000000000, + minting_fee=10, licensing_hook=ZERO_ADDRESS, hook_data=ZERO_HASH, commercial_rev_share=50, @@ -2263,13 +2263,13 @@ def test_batch_register_ip_assets_with_optimized_workflows_with_mint_and_registe license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.commercial_use( - default_minting_fee=1000000000000000000, + default_minting_fee=10, currency=MockERC20, royalty_policy=NativeRoyaltyPolicy.LAP, ), licensing_config=LicensingConfig( is_set=True, - minting_fee=1000000000000000000, + minting_fee=10, licensing_hook=ZERO_ADDRESS, hook_data=ZERO_HASH, commercial_rev_share=50, @@ -2509,13 +2509,13 @@ def test_batch_register_ip_assets_with_optimized_workflows_without_multicall( license_terms_data=[ LicenseTermsDataInput( terms=PILFlavor.commercial_use( - default_minting_fee=1000000000000000000, + default_minting_fee=10, currency=MockERC20, royalty_policy=NativeRoyaltyPolicy.LAP, ), licensing_config=LicensingConfig( is_set=True, - minting_fee=1000000000000000000, + minting_fee=10, licensing_hook=ZERO_ADDRESS, hook_data=ZERO_HASH, commercial_rev_share=50,