From 5ab7d597e83446811c4d540ca4dea085f50990b6 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 30 Jul 2025 15:31:53 +0800 Subject: [PATCH 1/4] chore: update formatting and documentation --- README.md | 10 +- setup.py | 41 +- src/story_protocol_python_sdk/__init__.py | 10 +- .../AccessController_client.py | 77 +- .../ArbitrationPolicyUMA_client.py | 43 +- .../CoreMetadataModule_client.py | 32 +- .../CoreMetadataViewModule_client.py | 32 +- .../DerivativeWorkflows_client.py | 32 +- .../abi/DisputeModule/DisputeModule_client.py | 62 +- .../GroupingModule/GroupingModule_client.py | 52 +- .../GroupingWorkflows_client.py | 187 +++- .../abi/IPAccountImpl/IPAccountImpl_client.py | 56 +- .../IPAssetRegistry/IPAssetRegistry_client.py | 49 +- .../IpRoyaltyVaultImpl_client.py | 21 +- .../LicenseAttachmentWorkflows_client.py | 118 ++- .../LicenseRegistry/LicenseRegistry_client.py | 47 +- .../abi/LicenseToken/LicenseToken_client.py | 30 +- .../LicensingModule/LicensingModule_client.py | 212 ++++- .../abi/MockERC20/MockERC20_client.py | 30 +- .../ModuleRegistry/ModuleRegistry_client.py | 30 +- .../PILicenseTemplate_client.py | 47 +- .../RegistrationWorkflows_client.py | 77 +- .../abi/RoyaltyModule/RoyaltyModule_client.py | 50 +- .../RoyaltyPolicyLAP_client.py | 45 +- .../RoyaltyPolicyLRP_client.py | 45 +- ...oyaltyTokenDistributionWorkflows_client.py | 204 ++++- .../RoyaltyWorkflows_client.py | 59 +- .../abi/SPGNFTImpl/SPGNFTImpl_client.py | 14 +- .../abi/WIP/WIP_client.py | 56 +- .../resources/Dispute.py | 219 +++-- .../resources/Group.py | 515 ++++++----- .../resources/IPAccount.py | 116 ++- .../resources/IPAsset.py | 700 ++++++++------- .../resources/License.py | 353 ++++---- .../resources/NFTClient.py | 94 +- .../resources/Permission.py | 137 +-- .../resources/Royalty.py | 218 +++-- .../resources/WIP.py | 42 +- .../scripts/archive/generate_client.py | 112 ++- .../archive/generate_client_from_abi.py | 64 +- .../scripts/archive/generate_client_impl.py | 89 +- .../archive/generate_client_impl_from_abi.py | 60 +- .../scripts/generate_clients.py | 152 ++-- src/story_protocol_python_sdk/story_client.py | 24 +- src/story_protocol_python_sdk/utils/ipfs.py | 30 +- .../utils/license_terms.py | 301 ++++--- src/story_protocol_python_sdk/utils/oov3.py | 34 +- src/story_protocol_python_sdk/utils/sign.py | 94 +- .../utils/transaction_utils.py | 38 +- .../utils/validation.py | 5 +- tests/__init__.py | 2 +- tests/demo/demo.py | 82 +- tests/demo/demo_utils.py | 85 +- tests/integration/setup_for_integration.py | 74 +- tests/integration/test_integration_dispute.py | 76 +- tests/integration/test_integration_group.py | 278 +++--- .../test_integration_ip_account.py | 308 ++++--- .../integration/test_integration_ip_asset.py | 526 +++++------ tests/integration/test_integration_license.py | 335 ++++--- .../test_integration_nft_client.py | 98 ++- .../test_integration_permission.py | 244 +++--- tests/integration/test_integration_royalty.py | 473 +++++----- tests/integration/test_integration_wip.py | 73 +- tests/integration/utils.py | 167 ++-- tests/unit/__init__.py | 2 +- tests/unit/resources/__init__.py | 2 +- tests/unit/resources/test_ip_account.py | 356 +++++--- tests/unit/resources/test_ip_asset.py | 112 ++- tests/unit/resources/test_license.py | 822 ++++++++++++------ tests/unit/resources/test_royalty.py | 113 ++- 70 files changed, 5610 insertions(+), 3783 deletions(-) diff --git a/README.md b/README.md index 4202e7c..7eef81c 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,16 @@ coverage run -m pytest tests/unit -v -ra -q coverage report ``` +## Formatting + +``` +black . +``` + ## Release -| Package | Description | -| :------------------------------ | :--------------------------------------------- | +| Package | Description | +| :----------------------------------------------------------- | :---------------------------------------------------- | | [story_protocol_python_sdk](./src/story_protocol_python_sdk) | A Python SDK for interacting with the Story Protocol. | ## Contributing diff --git a/setup.py b/setup.py index 33387e6..bf76f4f 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,26 @@ from setuptools import setup, find_packages -#python setup.py sdist -#twine upload dist/* +# python setup.py sdist +# twine upload dist/* setup( - name='story_protocol_python_sdk', - version='0.3.14', - packages=find_packages(where='src', exclude=["tests"]), - package_dir={'': 'src'}, - install_requires=[ - 'web3>=7.0.0', - 'pytest', - 'python-dotenv', - 'base58' - ], + name="story_protocol_python_sdk", + version="0.3.14", + packages=find_packages(where="src", exclude=["tests"]), + package_dir={"": "src"}, + install_requires=["web3>=7.0.0", "pytest", "python-dotenv", "base58"], include_package_data=True, # Ensure package data is included - url='https://github.com/storyprotocol/python-sdk', - license='MIT', - author='Andrew Chung', - author_email='andrew@storyprotocol.xyz', - description='A Python SDK for interacting with Story.', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', + url="https://github.com/storyprotocol/python-sdk", + license="MIT", + author="Andrew Chung", + author_email="andrew@storyprotocol.xyz", + description="A Python SDK for interacting with Story.", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ], - python_requires='>=3.10', + python_requires=">=3.10", ) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 6449ae4..4530bff 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -9,4 +9,12 @@ from .resources.WIP import WIP -__all__ = ['StoryClient', 'IPAsset', 'License', 'Royalty', 'IPAccount', 'Dispute', 'WIP'] \ No newline at end of file +__all__ = [ + "StoryClient", + "IPAsset", + "License", + "Royalty", + "IPAccount", + "Dispute", + "WIP", +] diff --git a/src/story_protocol_python_sdk/abi/AccessController/AccessController_client.py b/src/story_protocol_python_sdk/abi/AccessController/AccessController_client.py index fdf2b8b..d1956b2 100644 --- a/src/story_protocol_python_sdk/abi/AccessController/AccessController_client.py +++ b/src/story_protocol_python_sdk/abi/AccessController/AccessController_client.py @@ -1,43 +1,70 @@ - import json import os from web3 import Web3 + class AccessControllerClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'AccessController': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "AccessController": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for AccessController not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'AccessController.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for AccessController not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "AccessController.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def setAllPermissions(self, ipAccount, signer, permission): - return self.contract.functions.setAllPermissions(ipAccount, signer, permission).transact() - - def build_setAllPermissions_transaction(self, ipAccount, signer, permission, tx_params): - return self.contract.functions.setAllPermissions(ipAccount, signer, permission).build_transaction(tx_params) - + return self.contract.functions.setAllPermissions( + ipAccount, signer, permission + ).transact() + + def build_setAllPermissions_transaction( + self, ipAccount, signer, permission, tx_params + ): + return self.contract.functions.setAllPermissions( + ipAccount, signer, permission + ).build_transaction(tx_params) + def setTransientBatchPermissions(self, permissions): - return self.contract.functions.setTransientBatchPermissions(permissions).transact() - + return self.contract.functions.setTransientBatchPermissions( + permissions + ).transact() + def build_setTransientBatchPermissions_transaction(self, permissions, tx_params): - return self.contract.functions.setTransientBatchPermissions(permissions).build_transaction(tx_params) - + return self.contract.functions.setTransientBatchPermissions( + permissions + ).build_transaction(tx_params) + def setTransientPermission(self, ipAccount, signer, to, func, permission): - return self.contract.functions.setTransientPermission(ipAccount, signer, to, func, permission).transact() - - def build_setTransientPermission_transaction(self, ipAccount, signer, to, func, permission, tx_params): - return self.contract.functions.setTransientPermission(ipAccount, signer, to, func, permission).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.setTransientPermission( + ipAccount, signer, to, func, permission + ).transact() + + def build_setTransientPermission_transaction( + self, ipAccount, signer, to, func, permission, tx_params + ): + return self.contract.functions.setTransientPermission( + ipAccount, signer, to, func, permission + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/ArbitrationPolicyUMA/ArbitrationPolicyUMA_client.py b/src/story_protocol_python_sdk/abi/ArbitrationPolicyUMA/ArbitrationPolicyUMA_client.py index 6b885a3..b20b96a 100644 --- a/src/story_protocol_python_sdk/abi/ArbitrationPolicyUMA/ArbitrationPolicyUMA_client.py +++ b/src/story_protocol_python_sdk/abi/ArbitrationPolicyUMA/ArbitrationPolicyUMA_client.py @@ -1,40 +1,51 @@ - import json import os from web3 import Web3 + class ArbitrationPolicyUMAClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'ArbitrationPolicyUMA': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "ArbitrationPolicyUMA": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for ArbitrationPolicyUMA not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'ArbitrationPolicyUMA.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for ArbitrationPolicyUMA not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "ArbitrationPolicyUMA.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def disputeIdToAssertionId(self, disputeId): return self.contract.functions.disputeIdToAssertionId(disputeId).call() - + def maxBonds(self, token): return self.contract.functions.maxBonds(token).call() - + def maxLiveness(self): return self.contract.functions.maxLiveness().call() - + def minLiveness(self): return self.contract.functions.minLiveness().call() - + def oov3(self): return self.contract.functions.oov3().call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/CoreMetadataModule/CoreMetadataModule_client.py b/src/story_protocol_python_sdk/abi/CoreMetadataModule/CoreMetadataModule_client.py index f60656a..4e90177 100644 --- a/src/story_protocol_python_sdk/abi/CoreMetadataModule/CoreMetadataModule_client.py +++ b/src/story_protocol_python_sdk/abi/CoreMetadataModule/CoreMetadataModule_client.py @@ -1,24 +1,36 @@ - import json import os from web3 import Web3 + class CoreMetadataModuleClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'CoreMetadataModule': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "CoreMetadataModule": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for CoreMetadataModule not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'CoreMetadataModule.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for CoreMetadataModule not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "CoreMetadataModule.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py b/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py index 0fdaab5..e7f8ebd 100644 --- a/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py +++ b/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py @@ -1,24 +1,36 @@ - import json import os from web3 import Web3 + class CoreMetadataViewModuleClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'CoreMetadataViewModule': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "CoreMetadataViewModule": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for CoreMetadataViewModule not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'CoreMetadataViewModule.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for CoreMetadataViewModule not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "CoreMetadataViewModule.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - \ No newline at end of file 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 ababa9a..bee3fb5 100644 --- a/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py @@ -1,24 +1,36 @@ - import json import os from web3 import Web3 + class DerivativeWorkflowsClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'DerivativeWorkflows': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "DerivativeWorkflows": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for DerivativeWorkflows not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'DerivativeWorkflows.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for DerivativeWorkflows not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "DerivativeWorkflows.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/DisputeModule/DisputeModule_client.py b/src/story_protocol_python_sdk/abi/DisputeModule/DisputeModule_client.py index 19774b0..0dbb24f 100644 --- a/src/story_protocol_python_sdk/abi/DisputeModule/DisputeModule_client.py +++ b/src/story_protocol_python_sdk/abi/DisputeModule/DisputeModule_client.py @@ -1,46 +1,62 @@ - import json import os from web3 import Web3 + class DisputeModuleClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'DisputeModule': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "DisputeModule": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for DisputeModule not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'DisputeModule.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for DisputeModule not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "DisputeModule.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def cancelDispute(self, disputeId, data): return self.contract.functions.cancelDispute(disputeId, data).transact() - + def build_cancelDispute_transaction(self, disputeId, data, tx_params): - return self.contract.functions.cancelDispute(disputeId, data).build_transaction(tx_params) - + return self.contract.functions.cancelDispute(disputeId, data).build_transaction( + tx_params + ) + def raiseDispute(self, targetIpId, disputeEvidenceHash, targetTag, data): - return self.contract.functions.raiseDispute(targetIpId, disputeEvidenceHash, targetTag, data).transact() - - def build_raiseDispute_transaction(self, targetIpId, disputeEvidenceHash, targetTag, data, tx_params): - return self.contract.functions.raiseDispute(targetIpId, disputeEvidenceHash, targetTag, data).build_transaction(tx_params) - + return self.contract.functions.raiseDispute( + targetIpId, disputeEvidenceHash, targetTag, data + ).transact() + + def build_raiseDispute_transaction( + self, targetIpId, disputeEvidenceHash, targetTag, data, tx_params + ): + return self.contract.functions.raiseDispute( + targetIpId, disputeEvidenceHash, targetTag, data + ).build_transaction(tx_params) + def resolveDispute(self, disputeId, data): return self.contract.functions.resolveDispute(disputeId, data).transact() - + def build_resolveDispute_transaction(self, disputeId, data, tx_params): - return self.contract.functions.resolveDispute(disputeId, data).build_transaction(tx_params) - + return self.contract.functions.resolveDispute( + disputeId, data + ).build_transaction(tx_params) + def isWhitelistedDisputeTag(self, tag): return self.contract.functions.isWhitelistedDisputeTag(tag).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py b/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py index 01f3150..0c988d6 100644 --- a/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py +++ b/src/story_protocol_python_sdk/abi/GroupingModule/GroupingModule_client.py @@ -1,37 +1,51 @@ - import json import os from web3 import Web3 + class GroupingModuleClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'GroupingModule': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "GroupingModule": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for GroupingModule not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'GroupingModule.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for GroupingModule not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "GroupingModule.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def addIp(self, groupIpId, ipIds, maxAllowedRewardShare): - return self.contract.functions.addIp(groupIpId, ipIds, maxAllowedRewardShare).transact() - - def build_addIp_transaction(self, groupIpId, ipIds, maxAllowedRewardShare, tx_params): - return self.contract.functions.addIp(groupIpId, ipIds, maxAllowedRewardShare).build_transaction(tx_params) - + return self.contract.functions.addIp( + groupIpId, ipIds, maxAllowedRewardShare + ).transact() + + def build_addIp_transaction( + self, groupIpId, ipIds, maxAllowedRewardShare, tx_params + ): + return self.contract.functions.addIp( + groupIpId, ipIds, maxAllowedRewardShare + ).build_transaction(tx_params) + def registerGroup(self, groupPool): return self.contract.functions.registerGroup(groupPool).transact() - + def build_registerGroup_transaction(self, groupPool, tx_params): - return self.contract.functions.registerGroup(groupPool).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.registerGroup(groupPool).build_transaction( + tx_params + ) diff --git a/src/story_protocol_python_sdk/abi/GroupingWorkflows/GroupingWorkflows_client.py b/src/story_protocol_python_sdk/abi/GroupingWorkflows/GroupingWorkflows_client.py index 04db79b..1c1b0bb 100644 --- a/src/story_protocol_python_sdk/abi/GroupingWorkflows/GroupingWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/GroupingWorkflows/GroupingWorkflows_client.py @@ -1,55 +1,164 @@ - import json import os from web3 import Web3 + class GroupingWorkflowsClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'GroupingWorkflows': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "GroupingWorkflows": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for GroupingWorkflows not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'GroupingWorkflows.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for GroupingWorkflows not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "GroupingWorkflows.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def collectRoyaltiesAndClaimReward(self, groupIpId, currencyTokens, memberIpIds): - return self.contract.functions.collectRoyaltiesAndClaimReward(groupIpId, currencyTokens, memberIpIds).transact() - - def build_collectRoyaltiesAndClaimReward_transaction(self, groupIpId, currencyTokens, memberIpIds, tx_params): - return self.contract.functions.collectRoyaltiesAndClaimReward(groupIpId, currencyTokens, memberIpIds).build_transaction(tx_params) - - def mintAndRegisterIpAndAttachLicenseAndAddToGroup(self, spgNftContract, groupId, recipient, maxAllowedRewardShare, licensesData, ipMetadata, sigAddToGroup, allowDuplicates): - return self.contract.functions.mintAndRegisterIpAndAttachLicenseAndAddToGroup(spgNftContract, groupId, recipient, maxAllowedRewardShare, licensesData, ipMetadata, sigAddToGroup, allowDuplicates).transact() - - def build_mintAndRegisterIpAndAttachLicenseAndAddToGroup_transaction(self, spgNftContract, groupId, recipient, maxAllowedRewardShare, licensesData, ipMetadata, sigAddToGroup, allowDuplicates, tx_params): - return self.contract.functions.mintAndRegisterIpAndAttachLicenseAndAddToGroup(spgNftContract, groupId, recipient, maxAllowedRewardShare, licensesData, ipMetadata, sigAddToGroup, allowDuplicates).build_transaction(tx_params) - + return self.contract.functions.collectRoyaltiesAndClaimReward( + groupIpId, currencyTokens, memberIpIds + ).transact() + + def build_collectRoyaltiesAndClaimReward_transaction( + self, groupIpId, currencyTokens, memberIpIds, tx_params + ): + return self.contract.functions.collectRoyaltiesAndClaimReward( + groupIpId, currencyTokens, memberIpIds + ).build_transaction(tx_params) + + def mintAndRegisterIpAndAttachLicenseAndAddToGroup( + self, + spgNftContract, + groupId, + recipient, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigAddToGroup, + allowDuplicates, + ): + return self.contract.functions.mintAndRegisterIpAndAttachLicenseAndAddToGroup( + spgNftContract, + groupId, + recipient, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigAddToGroup, + allowDuplicates, + ).transact() + + def build_mintAndRegisterIpAndAttachLicenseAndAddToGroup_transaction( + self, + spgNftContract, + groupId, + recipient, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigAddToGroup, + allowDuplicates, + tx_params, + ): + return self.contract.functions.mintAndRegisterIpAndAttachLicenseAndAddToGroup( + spgNftContract, + groupId, + recipient, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigAddToGroup, + allowDuplicates, + ).build_transaction(tx_params) + def registerGroupAndAttachLicense(self, groupPool, licenseData): - return self.contract.functions.registerGroupAndAttachLicense(groupPool, licenseData).transact() - - def build_registerGroupAndAttachLicense_transaction(self, groupPool, licenseData, tx_params): - return self.contract.functions.registerGroupAndAttachLicense(groupPool, licenseData).build_transaction(tx_params) - - def registerGroupAndAttachLicenseAndAddIps(self, groupPool, ipIds, maxAllowedRewardShare, licenseData): - return self.contract.functions.registerGroupAndAttachLicenseAndAddIps(groupPool, ipIds, maxAllowedRewardShare, licenseData).transact() - - def build_registerGroupAndAttachLicenseAndAddIps_transaction(self, groupPool, ipIds, maxAllowedRewardShare, licenseData, tx_params): - return self.contract.functions.registerGroupAndAttachLicenseAndAddIps(groupPool, ipIds, maxAllowedRewardShare, licenseData).build_transaction(tx_params) - - def registerIpAndAttachLicenseAndAddToGroup(self, nftContract, tokenId, groupId, maxAllowedRewardShare, licensesData, ipMetadata, sigMetadataAndAttachAndConfig, sigAddToGroup): - return self.contract.functions.registerIpAndAttachLicenseAndAddToGroup(nftContract, tokenId, groupId, maxAllowedRewardShare, licensesData, ipMetadata, sigMetadataAndAttachAndConfig, sigAddToGroup).transact() - - def build_registerIpAndAttachLicenseAndAddToGroup_transaction(self, nftContract, tokenId, groupId, maxAllowedRewardShare, licensesData, ipMetadata, sigMetadataAndAttachAndConfig, sigAddToGroup, tx_params): - return self.contract.functions.registerIpAndAttachLicenseAndAddToGroup(nftContract, tokenId, groupId, maxAllowedRewardShare, licensesData, ipMetadata, sigMetadataAndAttachAndConfig, sigAddToGroup).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.registerGroupAndAttachLicense( + groupPool, licenseData + ).transact() + + def build_registerGroupAndAttachLicense_transaction( + self, groupPool, licenseData, tx_params + ): + return self.contract.functions.registerGroupAndAttachLicense( + groupPool, licenseData + ).build_transaction(tx_params) + + def registerGroupAndAttachLicenseAndAddIps( + self, groupPool, ipIds, maxAllowedRewardShare, licenseData + ): + return self.contract.functions.registerGroupAndAttachLicenseAndAddIps( + groupPool, ipIds, maxAllowedRewardShare, licenseData + ).transact() + + def build_registerGroupAndAttachLicenseAndAddIps_transaction( + self, groupPool, ipIds, maxAllowedRewardShare, licenseData, tx_params + ): + return self.contract.functions.registerGroupAndAttachLicenseAndAddIps( + groupPool, ipIds, maxAllowedRewardShare, licenseData + ).build_transaction(tx_params) + + def registerIpAndAttachLicenseAndAddToGroup( + self, + nftContract, + tokenId, + groupId, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigMetadataAndAttachAndConfig, + sigAddToGroup, + ): + return self.contract.functions.registerIpAndAttachLicenseAndAddToGroup( + nftContract, + tokenId, + groupId, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigMetadataAndAttachAndConfig, + sigAddToGroup, + ).transact() + + def build_registerIpAndAttachLicenseAndAddToGroup_transaction( + self, + nftContract, + tokenId, + groupId, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigMetadataAndAttachAndConfig, + sigAddToGroup, + tx_params, + ): + return self.contract.functions.registerIpAndAttachLicenseAndAddToGroup( + nftContract, + tokenId, + groupId, + maxAllowedRewardShare, + licensesData, + ipMetadata, + sigMetadataAndAttachAndConfig, + sigAddToGroup, + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/IPAccountImpl/IPAccountImpl_client.py b/src/story_protocol_python_sdk/abi/IPAccountImpl/IPAccountImpl_client.py index d982241..d676f41 100644 --- a/src/story_protocol_python_sdk/abi/IPAccountImpl/IPAccountImpl_client.py +++ b/src/story_protocol_python_sdk/abi/IPAccountImpl/IPAccountImpl_client.py @@ -1,47 +1,59 @@ - import json import os from web3 import Web3 + class IPAccountImplClient: def __init__(self, web3: Web3, contract_address=None): self.web3 = web3 - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'IPAccountImpl.json') - with open(abi_path, 'r') as abi_file: + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "IPAccountImpl.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def execute(self, to, value, data, operation): return self.contract.functions.execute(to, value, data, operation).transact() - + def build_execute_transaction(self, to, value, data, operation, tx_params): - return self.contract.functions.execute(to, value, data, operation).build_transaction(tx_params) - + return self.contract.functions.execute( + to, value, data, operation + ).build_transaction(tx_params) + def execute2(self, to, value, data): return self.contract.functions.execute(to, value, data).transact() - + def build_execute2_transaction(self, to, value, data, tx_params): - return self.contract.functions.execute(to, value, data).build_transaction(tx_params) - + return self.contract.functions.execute(to, value, data).build_transaction( + tx_params + ) + def executeBatch(self, calls, operation): return self.contract.functions.executeBatch(calls, operation).transact() - + def build_executeBatch_transaction(self, calls, operation, tx_params): - return self.contract.functions.executeBatch(calls, operation).build_transaction(tx_params) - + return self.contract.functions.executeBatch(calls, operation).build_transaction( + tx_params + ) + def executeWithSig(self, to, value, data, signer, deadline, signature): - return self.contract.functions.executeWithSig(to, value, data, signer, deadline, signature).transact() - - def build_executeWithSig_transaction(self, to, value, data, signer, deadline, signature, tx_params): - return self.contract.functions.executeWithSig(to, value, data, signer, deadline, signature).build_transaction(tx_params) - + return self.contract.functions.executeWithSig( + to, value, data, signer, deadline, signature + ).transact() + + def build_executeWithSig_transaction( + self, to, value, data, signer, deadline, signature, tx_params + ): + return self.contract.functions.executeWithSig( + to, value, data, signer, deadline, signature + ).build_transaction(tx_params) + def owner(self): return self.contract.functions.owner().call() - + def state(self): return self.contract.functions.state().call() - + def token(self): return self.contract.functions.token().call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/IPAssetRegistry/IPAssetRegistry_client.py b/src/story_protocol_python_sdk/abi/IPAssetRegistry/IPAssetRegistry_client.py index 5cbe5eb..eb1a457 100644 --- a/src/story_protocol_python_sdk/abi/IPAssetRegistry/IPAssetRegistry_client.py +++ b/src/story_protocol_python_sdk/abi/IPAssetRegistry/IPAssetRegistry_client.py @@ -1,37 +1,52 @@ - import json import os from web3 import Web3 + class IPAssetRegistryClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'IPAssetRegistry': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "IPAssetRegistry": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for IPAssetRegistry not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'IPAssetRegistry.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for IPAssetRegistry not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "IPAssetRegistry.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def register(self, chainid, tokenContract, tokenId): - return self.contract.functions.register(chainid, tokenContract, tokenId).transact() - + return self.contract.functions.register( + chainid, tokenContract, tokenId + ).transact() + def build_register_transaction(self, chainid, tokenContract, tokenId, tx_params): - return self.contract.functions.register(chainid, tokenContract, tokenId).build_transaction(tx_params) - + return self.contract.functions.register( + chainid, tokenContract, tokenId + ).build_transaction(tx_params) + def ipId(self, chainId, tokenContract, tokenId): return self.contract.functions.ipId(chainId, tokenContract, tokenId).call() - + def isRegistered(self, id): return self.contract.functions.isRegistered(id).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/IpRoyaltyVaultImpl/IpRoyaltyVaultImpl_client.py b/src/story_protocol_python_sdk/abi/IpRoyaltyVaultImpl/IpRoyaltyVaultImpl_client.py index 6e40cd0..4fff409 100644 --- a/src/story_protocol_python_sdk/abi/IpRoyaltyVaultImpl/IpRoyaltyVaultImpl_client.py +++ b/src/story_protocol_python_sdk/abi/IpRoyaltyVaultImpl/IpRoyaltyVaultImpl_client.py @@ -1,23 +1,28 @@ - import json import os from web3 import Web3 + class IpRoyaltyVaultImplClient: def __init__(self, web3: Web3, contract_address=None): self.web3 = web3 - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'IpRoyaltyVaultImpl.json') - with open(abi_path, 'r') as abi_file: + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "IpRoyaltyVaultImpl.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def balanceOf(self, account): return self.contract.functions.balanceOf(account).call() - + def claimableRevenue(self, claimer, token): return self.contract.functions.claimableRevenue(claimer, token).call() - + def ipId(self): return self.contract.functions.ipId().call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/LicenseAttachmentWorkflows/LicenseAttachmentWorkflows_client.py b/src/story_protocol_python_sdk/abi/LicenseAttachmentWorkflows/LicenseAttachmentWorkflows_client.py index 85c6575..3bbe154 100644 --- a/src/story_protocol_python_sdk/abi/LicenseAttachmentWorkflows/LicenseAttachmentWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/LicenseAttachmentWorkflows/LicenseAttachmentWorkflows_client.py @@ -1,49 +1,107 @@ - import json import os from web3 import Web3 + class LicenseAttachmentWorkflowsClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'LicenseAttachmentWorkflows': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "LicenseAttachmentWorkflows": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for LicenseAttachmentWorkflows not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'LicenseAttachmentWorkflows.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for LicenseAttachmentWorkflows not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "LicenseAttachmentWorkflows.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - - def mintAndRegisterIpAndAttachPILTerms(self, spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates): - return self.contract.functions.mintAndRegisterIpAndAttachPILTerms(spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates).transact() - - def build_mintAndRegisterIpAndAttachPILTerms_transaction(self, spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates, tx_params): - return self.contract.functions.mintAndRegisterIpAndAttachPILTerms(spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates).build_transaction(tx_params) - + + def mintAndRegisterIpAndAttachPILTerms( + self, spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates + ): + return self.contract.functions.mintAndRegisterIpAndAttachPILTerms( + spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates + ).transact() + + def build_mintAndRegisterIpAndAttachPILTerms_transaction( + self, + spgNftContract, + recipient, + ipMetadata, + licenseTermsData, + allowDuplicates, + tx_params, + ): + return self.contract.functions.mintAndRegisterIpAndAttachPILTerms( + spgNftContract, recipient, ipMetadata, licenseTermsData, allowDuplicates + ).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 registerIpAndAttachPILTerms(self, nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig): - return self.contract.functions.registerIpAndAttachPILTerms(nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig).transact() - - def build_registerIpAndAttachPILTerms_transaction(self, nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig, tx_params): - return self.contract.functions.registerIpAndAttachPILTerms(nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig).build_transaction(tx_params) - + + def registerIpAndAttachPILTerms( + self, + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + ): + return self.contract.functions.registerIpAndAttachPILTerms( + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + ).transact() + + def build_registerIpAndAttachPILTerms_transaction( + self, + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + tx_params, + ): + return self.contract.functions.registerIpAndAttachPILTerms( + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + ).build_transaction(tx_params) + def registerPILTermsAndAttach(self, ipId, licenseTermsData, sigAttachAndConfig): - return self.contract.functions.registerPILTermsAndAttach(ipId, licenseTermsData, sigAttachAndConfig).transact() - - def build_registerPILTermsAndAttach_transaction(self, ipId, licenseTermsData, sigAttachAndConfig, tx_params): - return self.contract.functions.registerPILTermsAndAttach(ipId, licenseTermsData, sigAttachAndConfig).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.registerPILTermsAndAttach( + ipId, licenseTermsData, sigAttachAndConfig + ).transact() + + def build_registerPILTermsAndAttach_transaction( + self, ipId, licenseTermsData, sigAttachAndConfig, tx_params + ): + return self.contract.functions.registerPILTermsAndAttach( + ipId, licenseTermsData, sigAttachAndConfig + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py b/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py index f9135a7..824a47e 100644 --- a/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py +++ b/src/story_protocol_python_sdk/abi/LicenseRegistry/LicenseRegistry_client.py @@ -1,34 +1,49 @@ - import json import os from web3 import Web3 + class LicenseRegistryClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'LicenseRegistry': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "LicenseRegistry": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for LicenseRegistry not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'LicenseRegistry.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for LicenseRegistry not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "LicenseRegistry.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def exists(self, licenseTemplate, licenseTermsId): return self.contract.functions.exists(licenseTemplate, licenseTermsId).call() - + def getRoyaltyPercent(self, ipId, licenseTemplate, licenseTermsId): - return self.contract.functions.getRoyaltyPercent(ipId, licenseTemplate, licenseTermsId).call() - + return self.contract.functions.getRoyaltyPercent( + ipId, licenseTemplate, licenseTermsId + ).call() + def hasIpAttachedLicenseTerms(self, ipId, licenseTemplate, licenseTermsId): - return self.contract.functions.hasIpAttachedLicenseTerms(ipId, licenseTemplate, licenseTermsId).call() - - \ No newline at end of file + return self.contract.functions.hasIpAttachedLicenseTerms( + ipId, licenseTemplate, licenseTermsId + ).call() diff --git a/src/story_protocol_python_sdk/abi/LicenseToken/LicenseToken_client.py b/src/story_protocol_python_sdk/abi/LicenseToken/LicenseToken_client.py index 40f098e..1720e0a 100644 --- a/src/story_protocol_python_sdk/abi/LicenseToken/LicenseToken_client.py +++ b/src/story_protocol_python_sdk/abi/LicenseToken/LicenseToken_client.py @@ -1,28 +1,34 @@ - import json import os from web3 import Web3 + class LicenseTokenClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'LicenseToken': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "LicenseToken": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for LicenseToken not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'LicenseToken.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for LicenseToken not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "LicenseToken.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def ownerOf(self, tokenId): return self.contract.functions.ownerOf(tokenId).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/LicensingModule/LicensingModule_client.py b/src/story_protocol_python_sdk/abi/LicensingModule/LicensingModule_client.py index 9914d28..cf01b15 100644 --- a/src/story_protocol_python_sdk/abi/LicensingModule/LicensingModule_client.py +++ b/src/story_protocol_python_sdk/abi/LicensingModule/LicensingModule_client.py @@ -1,58 +1,184 @@ - import json import os from web3 import Web3 + class LicensingModuleClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'LicensingModule': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "LicensingModule": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for LicensingModule not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'LicensingModule.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for LicensingModule not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "LicensingModule.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def attachLicenseTerms(self, ipId, licenseTemplate, licenseTermsId): - return self.contract.functions.attachLicenseTerms(ipId, licenseTemplate, licenseTermsId).transact() - - def build_attachLicenseTerms_transaction(self, ipId, licenseTemplate, licenseTermsId, tx_params): - return self.contract.functions.attachLicenseTerms(ipId, licenseTemplate, licenseTermsId).build_transaction(tx_params) - - def mintLicenseTokens(self, licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, royaltyContext, maxMintingFee, maxRevenueShare): - return self.contract.functions.mintLicenseTokens(licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, royaltyContext, maxMintingFee, maxRevenueShare).transact() - - def build_mintLicenseTokens_transaction(self, licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, royaltyContext, maxMintingFee, maxRevenueShare, tx_params): - return self.contract.functions.mintLicenseTokens(licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, royaltyContext, maxMintingFee, maxRevenueShare).build_transaction(tx_params) - - def registerDerivative(self, childIpId, parentIpIds, licenseTermsIds, licenseTemplate, royaltyContext, maxMintingFee, maxRts, maxRevenueShare): - return self.contract.functions.registerDerivative(childIpId, parentIpIds, licenseTermsIds, licenseTemplate, royaltyContext, maxMintingFee, maxRts, maxRevenueShare).transact() - - def build_registerDerivative_transaction(self, childIpId, parentIpIds, licenseTermsIds, licenseTemplate, royaltyContext, maxMintingFee, maxRts, maxRevenueShare, tx_params): - return self.contract.functions.registerDerivative(childIpId, parentIpIds, licenseTermsIds, licenseTemplate, royaltyContext, maxMintingFee, maxRts, maxRevenueShare).build_transaction(tx_params) - - def registerDerivativeWithLicenseTokens(self, childIpId, licenseTokenIds, royaltyContext, maxRts): - return self.contract.functions.registerDerivativeWithLicenseTokens(childIpId, licenseTokenIds, royaltyContext, maxRts).transact() - - def build_registerDerivativeWithLicenseTokens_transaction(self, childIpId, licenseTokenIds, royaltyContext, maxRts, tx_params): - return self.contract.functions.registerDerivativeWithLicenseTokens(childIpId, licenseTokenIds, royaltyContext, maxRts).build_transaction(tx_params) - - def setLicensingConfig(self, ipId, licenseTemplate, licenseTermsId, licensingConfig): - return self.contract.functions.setLicensingConfig(ipId, licenseTemplate, licenseTermsId, licensingConfig).transact() - - def build_setLicensingConfig_transaction(self, ipId, licenseTemplate, licenseTermsId, licensingConfig, tx_params): - return self.contract.functions.setLicensingConfig(ipId, licenseTemplate, licenseTermsId, licensingConfig).build_transaction(tx_params) - - def predictMintingLicenseFee(self, licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, royaltyContext): - return self.contract.functions.predictMintingLicenseFee(licensorIpId, licenseTemplate, licenseTermsId, amount, receiver, royaltyContext).call() - - \ No newline at end of file + return self.contract.functions.attachLicenseTerms( + ipId, licenseTemplate, licenseTermsId + ).transact() + + def build_attachLicenseTerms_transaction( + self, ipId, licenseTemplate, licenseTermsId, tx_params + ): + return self.contract.functions.attachLicenseTerms( + ipId, licenseTemplate, licenseTermsId + ).build_transaction(tx_params) + + def mintLicenseTokens( + self, + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + royaltyContext, + maxMintingFee, + maxRevenueShare, + ): + return self.contract.functions.mintLicenseTokens( + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + royaltyContext, + maxMintingFee, + maxRevenueShare, + ).transact() + + def build_mintLicenseTokens_transaction( + self, + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + royaltyContext, + maxMintingFee, + maxRevenueShare, + tx_params, + ): + return self.contract.functions.mintLicenseTokens( + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + royaltyContext, + maxMintingFee, + maxRevenueShare, + ).build_transaction(tx_params) + + def registerDerivative( + self, + childIpId, + parentIpIds, + licenseTermsIds, + licenseTemplate, + royaltyContext, + maxMintingFee, + maxRts, + maxRevenueShare, + ): + return self.contract.functions.registerDerivative( + childIpId, + parentIpIds, + licenseTermsIds, + licenseTemplate, + royaltyContext, + maxMintingFee, + maxRts, + maxRevenueShare, + ).transact() + + def build_registerDerivative_transaction( + self, + childIpId, + parentIpIds, + licenseTermsIds, + licenseTemplate, + royaltyContext, + maxMintingFee, + maxRts, + maxRevenueShare, + tx_params, + ): + return self.contract.functions.registerDerivative( + childIpId, + parentIpIds, + licenseTermsIds, + licenseTemplate, + royaltyContext, + maxMintingFee, + maxRts, + maxRevenueShare, + ).build_transaction(tx_params) + + def registerDerivativeWithLicenseTokens( + self, childIpId, licenseTokenIds, royaltyContext, maxRts + ): + return self.contract.functions.registerDerivativeWithLicenseTokens( + childIpId, licenseTokenIds, royaltyContext, maxRts + ).transact() + + def build_registerDerivativeWithLicenseTokens_transaction( + self, childIpId, licenseTokenIds, royaltyContext, maxRts, tx_params + ): + return self.contract.functions.registerDerivativeWithLicenseTokens( + childIpId, licenseTokenIds, royaltyContext, maxRts + ).build_transaction(tx_params) + + def setLicensingConfig( + self, ipId, licenseTemplate, licenseTermsId, licensingConfig + ): + return self.contract.functions.setLicensingConfig( + ipId, licenseTemplate, licenseTermsId, licensingConfig + ).transact() + + def build_setLicensingConfig_transaction( + self, ipId, licenseTemplate, licenseTermsId, licensingConfig, tx_params + ): + return self.contract.functions.setLicensingConfig( + ipId, licenseTemplate, licenseTermsId, licensingConfig + ).build_transaction(tx_params) + + def predictMintingLicenseFee( + self, + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + royaltyContext, + ): + return self.contract.functions.predictMintingLicenseFee( + licensorIpId, + licenseTemplate, + licenseTermsId, + amount, + receiver, + royaltyContext, + ).call() diff --git a/src/story_protocol_python_sdk/abi/MockERC20/MockERC20_client.py b/src/story_protocol_python_sdk/abi/MockERC20/MockERC20_client.py index d7eab76..a51a7d4 100644 --- a/src/story_protocol_python_sdk/abi/MockERC20/MockERC20_client.py +++ b/src/story_protocol_python_sdk/abi/MockERC20/MockERC20_client.py @@ -1,34 +1,38 @@ - import json import os from web3 import Web3 + class MockERC20Client: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'MockERC20': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "MockERC20": + contract_address = contract["contract_address"] break if not contract_address: raise ValueError(f"Contract address for MockERC20 not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'MockERC20.json') - with open(abi_path, 'r') as abi_file: + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "MockERC20.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def transfer(self, to, value): return self.contract.functions.transfer(to, value).transact() - + def build_transfer_transaction(self, to, value, tx_params): return self.contract.functions.transfer(to, value).build_transaction(tx_params) - + def balanceOf(self, account): return self.contract.functions.balanceOf(account).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/ModuleRegistry/ModuleRegistry_client.py b/src/story_protocol_python_sdk/abi/ModuleRegistry/ModuleRegistry_client.py index f907dcc..bfcc1b6 100644 --- a/src/story_protocol_python_sdk/abi/ModuleRegistry/ModuleRegistry_client.py +++ b/src/story_protocol_python_sdk/abi/ModuleRegistry/ModuleRegistry_client.py @@ -1,28 +1,34 @@ - import json import os from web3 import Web3 + class ModuleRegistryClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'ModuleRegistry': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "ModuleRegistry": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for ModuleRegistry not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'ModuleRegistry.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for ModuleRegistry not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "ModuleRegistry.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def isRegistered(self, moduleAddress): return self.contract.functions.isRegistered(moduleAddress).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/PILicenseTemplate/PILicenseTemplate_client.py b/src/story_protocol_python_sdk/abi/PILicenseTemplate/PILicenseTemplate_client.py index 734cb51..9b5b8e8 100644 --- a/src/story_protocol_python_sdk/abi/PILicenseTemplate/PILicenseTemplate_client.py +++ b/src/story_protocol_python_sdk/abi/PILicenseTemplate/PILicenseTemplate_client.py @@ -1,40 +1,53 @@ - import json import os from web3 import Web3 + class PILicenseTemplateClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'PILicenseTemplate': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "PILicenseTemplate": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for PILicenseTemplate not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'PILicenseTemplate.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for PILicenseTemplate not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "PILicenseTemplate.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def registerLicenseTerms(self, terms): return self.contract.functions.registerLicenseTerms(terms).transact() - + def build_registerLicenseTerms_transaction(self, terms, tx_params): - return self.contract.functions.registerLicenseTerms(terms).build_transaction(tx_params) - + return self.contract.functions.registerLicenseTerms(terms).build_transaction( + tx_params + ) + def exists(self, licenseTermsId): return self.contract.functions.exists(licenseTermsId).call() - + def getLicenseTerms(self, selectedLicenseTermsId): return self.contract.functions.getLicenseTerms(selectedLicenseTermsId).call() - + def getLicenseTermsId(self, terms): return self.contract.functions.getLicenseTermsId(terms).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/RegistrationWorkflows/RegistrationWorkflows_client.py b/src/story_protocol_python_sdk/abi/RegistrationWorkflows/RegistrationWorkflows_client.py index d157cfa..76c0298 100644 --- a/src/story_protocol_python_sdk/abi/RegistrationWorkflows/RegistrationWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/RegistrationWorkflows/RegistrationWorkflows_client.py @@ -1,49 +1,74 @@ - import json import os from web3 import Web3 + class RegistrationWorkflowsClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'RegistrationWorkflows': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "RegistrationWorkflows": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for RegistrationWorkflows not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'RegistrationWorkflows.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for RegistrationWorkflows not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "RegistrationWorkflows.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def createCollection(self, spgNftInitParams): return self.contract.functions.createCollection(spgNftInitParams).transact() - + def build_createCollection_transaction(self, spgNftInitParams, tx_params): - return self.contract.functions.createCollection(spgNftInitParams).build_transaction(tx_params) - + return self.contract.functions.createCollection( + spgNftInitParams + ).build_transaction(tx_params) + def mintAndRegisterIp(self, spgNftContract, recipient, ipMetadata, allowDuplicates): - return self.contract.functions.mintAndRegisterIp(spgNftContract, recipient, ipMetadata, allowDuplicates).transact() - - def build_mintAndRegisterIp_transaction(self, spgNftContract, recipient, ipMetadata, allowDuplicates, tx_params): - return self.contract.functions.mintAndRegisterIp(spgNftContract, recipient, ipMetadata, allowDuplicates).build_transaction(tx_params) - + return self.contract.functions.mintAndRegisterIp( + spgNftContract, recipient, ipMetadata, allowDuplicates + ).transact() + + def build_mintAndRegisterIp_transaction( + self, spgNftContract, recipient, ipMetadata, allowDuplicates, tx_params + ): + return self.contract.functions.mintAndRegisterIp( + spgNftContract, recipient, ipMetadata, allowDuplicates + ).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 registerIp(self, nftContract, tokenId, ipMetadata, sigMetadata): - return self.contract.functions.registerIp(nftContract, tokenId, ipMetadata, sigMetadata).transact() - - def build_registerIp_transaction(self, nftContract, tokenId, ipMetadata, sigMetadata, tx_params): - return self.contract.functions.registerIp(nftContract, tokenId, ipMetadata, sigMetadata).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.registerIp( + nftContract, tokenId, ipMetadata, sigMetadata + ).transact() + + def build_registerIp_transaction( + self, nftContract, tokenId, ipMetadata, sigMetadata, tx_params + ): + return self.contract.functions.registerIp( + nftContract, tokenId, ipMetadata, sigMetadata + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/RoyaltyModule/RoyaltyModule_client.py b/src/story_protocol_python_sdk/abi/RoyaltyModule/RoyaltyModule_client.py index 18c02ed..1907130 100644 --- a/src/story_protocol_python_sdk/abi/RoyaltyModule/RoyaltyModule_client.py +++ b/src/story_protocol_python_sdk/abi/RoyaltyModule/RoyaltyModule_client.py @@ -1,40 +1,52 @@ - import json import os from web3 import Web3 + class RoyaltyModuleClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'RoyaltyModule': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "RoyaltyModule": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for RoyaltyModule not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'RoyaltyModule.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for RoyaltyModule not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "RoyaltyModule.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def payRoyaltyOnBehalf(self, receiverIpId, payerIpId, token, amount): - return self.contract.functions.payRoyaltyOnBehalf(receiverIpId, payerIpId, token, amount).transact() - - def build_payRoyaltyOnBehalf_transaction(self, receiverIpId, payerIpId, token, amount, tx_params): - return self.contract.functions.payRoyaltyOnBehalf(receiverIpId, payerIpId, token, amount).build_transaction(tx_params) - + return self.contract.functions.payRoyaltyOnBehalf( + receiverIpId, payerIpId, token, amount + ).transact() + + def build_payRoyaltyOnBehalf_transaction( + self, receiverIpId, payerIpId, token, amount, tx_params + ): + return self.contract.functions.payRoyaltyOnBehalf( + receiverIpId, payerIpId, token, amount + ).build_transaction(tx_params) + def ipRoyaltyVaults(self, ipId): return self.contract.functions.ipRoyaltyVaults(ipId).call() - + def isWhitelistedRoyaltyPolicy(self, royaltyPolicy): return self.contract.functions.isWhitelistedRoyaltyPolicy(royaltyPolicy).call() - + def isWhitelistedRoyaltyToken(self, token): return self.contract.functions.isWhitelistedRoyaltyToken(token).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/RoyaltyPolicyLAP/RoyaltyPolicyLAP_client.py b/src/story_protocol_python_sdk/abi/RoyaltyPolicyLAP/RoyaltyPolicyLAP_client.py index cf09002..f553857 100644 --- a/src/story_protocol_python_sdk/abi/RoyaltyPolicyLAP/RoyaltyPolicyLAP_client.py +++ b/src/story_protocol_python_sdk/abi/RoyaltyPolicyLAP/RoyaltyPolicyLAP_client.py @@ -1,31 +1,46 @@ - import json import os from web3 import Web3 + class RoyaltyPolicyLAPClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'RoyaltyPolicyLAP': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "RoyaltyPolicyLAP": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for RoyaltyPolicyLAP not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'RoyaltyPolicyLAP.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for RoyaltyPolicyLAP not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "RoyaltyPolicyLAP.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def transferToVault(self, ipId, ancestorIpId, token): - return self.contract.functions.transferToVault(ipId, ancestorIpId, token).transact() - + return self.contract.functions.transferToVault( + ipId, ancestorIpId, token + ).transact() + def build_transferToVault_transaction(self, ipId, ancestorIpId, token, tx_params): - return self.contract.functions.transferToVault(ipId, ancestorIpId, token).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.transferToVault( + ipId, ancestorIpId, token + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/RoyaltyPolicyLRP/RoyaltyPolicyLRP_client.py b/src/story_protocol_python_sdk/abi/RoyaltyPolicyLRP/RoyaltyPolicyLRP_client.py index 64f5c3d..62ad2f7 100644 --- a/src/story_protocol_python_sdk/abi/RoyaltyPolicyLRP/RoyaltyPolicyLRP_client.py +++ b/src/story_protocol_python_sdk/abi/RoyaltyPolicyLRP/RoyaltyPolicyLRP_client.py @@ -1,31 +1,46 @@ - import json import os from web3 import Web3 + class RoyaltyPolicyLRPClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'RoyaltyPolicyLRP': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "RoyaltyPolicyLRP": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for RoyaltyPolicyLRP not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'RoyaltyPolicyLRP.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for RoyaltyPolicyLRP not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "RoyaltyPolicyLRP.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def transferToVault(self, ipId, ancestorIpId, token): - return self.contract.functions.transferToVault(ipId, ancestorIpId, token).transact() - + return self.contract.functions.transferToVault( + ipId, ancestorIpId, token + ).transact() + def build_transferToVault_transaction(self, ipId, ancestorIpId, token, tx_params): - return self.contract.functions.transferToVault(ipId, ancestorIpId, token).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.transferToVault( + ipId, ancestorIpId, token + ).build_transaction(tx_params) 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 98e0fcd..407cb80 100644 --- a/src/story_protocol_python_sdk/abi/RoyaltyTokenDistributionWorkflows/RoyaltyTokenDistributionWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/RoyaltyTokenDistributionWorkflows/RoyaltyTokenDistributionWorkflows_client.py @@ -1,55 +1,179 @@ - import json import os from web3 import Web3 + class RoyaltyTokenDistributionWorkflowsClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'RoyaltyTokenDistributionWorkflows': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "RoyaltyTokenDistributionWorkflows": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for RoyaltyTokenDistributionWorkflows not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'RoyaltyTokenDistributionWorkflows.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for RoyaltyTokenDistributionWorkflows not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "RoyaltyTokenDistributionWorkflows.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def distributeRoyaltyTokens(self, ipId, royaltyShares, sigApproveRoyaltyTokens): - return self.contract.functions.distributeRoyaltyTokens(ipId, royaltyShares, sigApproveRoyaltyTokens).transact() - - def build_distributeRoyaltyTokens_transaction(self, ipId, royaltyShares, sigApproveRoyaltyTokens, tx_params): - return self.contract.functions.distributeRoyaltyTokens(ipId, royaltyShares, sigApproveRoyaltyTokens).build_transaction(tx_params) - - def mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens(self, spgNftContract, recipient, ipMetadata, licenseTermsData, royaltyShares, allowDuplicates): - return self.contract.functions.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens(spgNftContract, recipient, ipMetadata, licenseTermsData, royaltyShares, allowDuplicates).transact() - - def build_mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_transaction(self, spgNftContract, recipient, ipMetadata, licenseTermsData, royaltyShares, allowDuplicates, tx_params): - return self.contract.functions.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens(spgNftContract, recipient, ipMetadata, licenseTermsData, royaltyShares, allowDuplicates).build_transaction(tx_params) - - def mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens(self, spgNftContract, recipient, ipMetadata, derivData, royaltyShares, allowDuplicates): - return self.contract.functions.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens(spgNftContract, recipient, ipMetadata, derivData, royaltyShares, allowDuplicates).transact() - - def build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transaction(self, spgNftContract, recipient, ipMetadata, derivData, royaltyShares, allowDuplicates, tx_params): - return self.contract.functions.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens(spgNftContract, recipient, ipMetadata, derivData, royaltyShares, allowDuplicates).build_transaction(tx_params) - - def registerIpAndAttachPILTermsAndDeployRoyaltyVault(self, nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig): - return self.contract.functions.registerIpAndAttachPILTermsAndDeployRoyaltyVault(nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig).transact() - - def build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction(self, nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig, tx_params): - return self.contract.functions.registerIpAndAttachPILTermsAndDeployRoyaltyVault(nftContract, tokenId, ipMetadata, licenseTermsData, sigMetadataAndAttachAndConfig).build_transaction(tx_params) - - def registerIpAndMakeDerivativeAndDeployRoyaltyVault(self, nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister): - return self.contract.functions.registerIpAndMakeDerivativeAndDeployRoyaltyVault(nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister).transact() - - def build_registerIpAndMakeDerivativeAndDeployRoyaltyVault_transaction(self, nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister, tx_params): - return self.contract.functions.registerIpAndMakeDerivativeAndDeployRoyaltyVault(nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister).build_transaction(tx_params) - - \ No newline at end of file + return self.contract.functions.distributeRoyaltyTokens( + ipId, royaltyShares, sigApproveRoyaltyTokens + ).transact() + + def build_distributeRoyaltyTokens_transaction( + self, ipId, royaltyShares, sigApproveRoyaltyTokens, tx_params + ): + return self.contract.functions.distributeRoyaltyTokens( + ipId, royaltyShares, sigApproveRoyaltyTokens + ).build_transaction(tx_params) + + def mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( + self, + spgNftContract, + recipient, + ipMetadata, + licenseTermsData, + royaltyShares, + allowDuplicates, + ): + return self.contract.functions.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( + spgNftContract, + recipient, + ipMetadata, + licenseTermsData, + royaltyShares, + allowDuplicates, + ).transact() + + def build_mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens_transaction( + self, + spgNftContract, + recipient, + ipMetadata, + licenseTermsData, + royaltyShares, + allowDuplicates, + tx_params, + ): + return self.contract.functions.mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens( + spgNftContract, + recipient, + ipMetadata, + licenseTermsData, + royaltyShares, + allowDuplicates, + ).build_transaction( + tx_params + ) + + def mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( + self, + spgNftContract, + recipient, + ipMetadata, + derivData, + royaltyShares, + allowDuplicates, + ): + return self.contract.functions.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( + spgNftContract, + recipient, + ipMetadata, + derivData, + royaltyShares, + allowDuplicates, + ).transact() + + def build_mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens_transaction( + self, + spgNftContract, + recipient, + ipMetadata, + derivData, + royaltyShares, + allowDuplicates, + tx_params, + ): + return self.contract.functions.mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens( + spgNftContract, + recipient, + ipMetadata, + derivData, + royaltyShares, + allowDuplicates, + ).build_transaction( + tx_params + ) + + def registerIpAndAttachPILTermsAndDeployRoyaltyVault( + self, + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + ): + return self.contract.functions.registerIpAndAttachPILTermsAndDeployRoyaltyVault( + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + ).transact() + + def build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction( + self, + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + tx_params, + ): + return self.contract.functions.registerIpAndAttachPILTermsAndDeployRoyaltyVault( + nftContract, + tokenId, + ipMetadata, + licenseTermsData, + sigMetadataAndAttachAndConfig, + ).build_transaction(tx_params) + + def registerIpAndMakeDerivativeAndDeployRoyaltyVault( + self, nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister + ): + return self.contract.functions.registerIpAndMakeDerivativeAndDeployRoyaltyVault( + nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister + ).transact() + + def build_registerIpAndMakeDerivativeAndDeployRoyaltyVault_transaction( + self, + nftContract, + tokenId, + ipMetadata, + derivData, + sigMetadataAndRegister, + tx_params, + ): + return self.contract.functions.registerIpAndMakeDerivativeAndDeployRoyaltyVault( + nftContract, tokenId, ipMetadata, derivData, sigMetadataAndRegister + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/RoyaltyWorkflows/RoyaltyWorkflows_client.py b/src/story_protocol_python_sdk/abi/RoyaltyWorkflows/RoyaltyWorkflows_client.py index 72850ad..91884c3 100644 --- a/src/story_protocol_python_sdk/abi/RoyaltyWorkflows/RoyaltyWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/RoyaltyWorkflows/RoyaltyWorkflows_client.py @@ -1,31 +1,56 @@ - import json import os from web3 import Web3 + class RoyaltyWorkflowsClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'RoyaltyWorkflows': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "RoyaltyWorkflows": + contract_address = contract["contract_address"] break if not contract_address: - raise ValueError(f"Contract address for RoyaltyWorkflows not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'RoyaltyWorkflows.json') - with open(abi_path, 'r') as abi_file: + raise ValueError( + f"Contract address for RoyaltyWorkflows not found in config.json" + ) + abi_path = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "abi", + "jsons", + "RoyaltyWorkflows.json", + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - - def claimAllRevenue(self, ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens): - return self.contract.functions.claimAllRevenue(ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens).transact() - - def build_claimAllRevenue_transaction(self, ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens, tx_params): - return self.contract.functions.claimAllRevenue(ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens).build_transaction(tx_params) - - \ No newline at end of file + + def claimAllRevenue( + self, ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens + ): + return self.contract.functions.claimAllRevenue( + ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens + ).transact() + + def build_claimAllRevenue_transaction( + self, + ancestorIpId, + claimer, + childIpIds, + royaltyPolicies, + currencyTokens, + tx_params, + ): + return self.contract.functions.claimAllRevenue( + ancestorIpId, claimer, childIpIds, royaltyPolicies, currencyTokens + ).build_transaction(tx_params) 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 e5feeb7..f19a684 100644 --- a/src/story_protocol_python_sdk/abi/SPGNFTImpl/SPGNFTImpl_client.py +++ b/src/story_protocol_python_sdk/abi/SPGNFTImpl/SPGNFTImpl_client.py @@ -1,20 +1,20 @@ - import json import os from web3 import Web3 + class SPGNFTImplClient: def __init__(self, web3: Web3, contract_address=None): self.web3 = web3 - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'SPGNFTImpl.json') - with open(abi_path, 'r') as abi_file: + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "SPGNFTImpl.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def mintFee(self): return self.contract.functions.mintFee().call() - + def mintFeeToken(self): return self.contract.functions.mintFeeToken().call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/abi/WIP/WIP_client.py b/src/story_protocol_python_sdk/abi/WIP/WIP_client.py index 9fbe6c1..0587c65 100644 --- a/src/story_protocol_python_sdk/abi/WIP/WIP_client.py +++ b/src/story_protocol_python_sdk/abi/WIP/WIP_client.py @@ -1,61 +1,69 @@ - import json import os from web3 import Web3 + class WIPClient: def __init__(self, web3: Web3): self.web3 = web3 # Assuming config.json is located at the root of the project - config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'config.json')) - with open(config_path, 'r') as config_file: + config_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), "..", "..", "scripts", "config.json" + ) + ) + with open(config_path, "r") as config_file: config = json.load(config_file) contract_address = None - for contract in config['contracts']: - if contract['contract_name'] == 'WIP': - contract_address = contract['contract_address'] + for contract in config["contracts"]: + if contract["contract_name"] == "WIP": + contract_address = contract["contract_address"] break if not contract_address: raise ValueError(f"Contract address for WIP not found in config.json") - abi_path = os.path.join(os.path.dirname(__file__), '..', '..', 'abi', 'jsons', 'WIP.json') - with open(abi_path, 'r') as abi_file: + abi_path = os.path.join( + os.path.dirname(__file__), "..", "..", "abi", "jsons", "WIP.json" + ) + with open(abi_path, "r") as abi_file: abi = json.load(abi_file) self.contract = self.web3.eth.contract(address=contract_address, abi=abi) - + def approve(self, spender, amount): return self.contract.functions.approve(spender, amount).transact() - + def build_approve_transaction(self, spender, amount, tx_params): - return self.contract.functions.approve(spender, amount).build_transaction(tx_params) - + return self.contract.functions.approve(spender, amount).build_transaction( + tx_params + ) + def deposit(self): return self.contract.functions.deposit().transact() - + def build_deposit_transaction(self, tx_params): return self.contract.functions.deposit().build_transaction(tx_params) - + def transfer(self, to, amount): return self.contract.functions.transfer(to, amount).transact() - + def build_transfer_transaction(self, to, amount, tx_params): return self.contract.functions.transfer(to, amount).build_transaction(tx_params) - + def transferFrom(self, from_address, to, amount): return self.contract.functions.transferFrom(from_address, to, amount).transact() - + def build_transferFrom_transaction(self, from_address, to, amount, tx_params): - return self.contract.functions.transferFrom(from_address, to, amount).build_transaction(tx_params) - + return self.contract.functions.transferFrom( + from_address, to, amount + ).build_transaction(tx_params) + def withdraw(self, value): return self.contract.functions.withdraw(value).transact() - + def build_withdraw_transaction(self, value, tx_params): return self.contract.functions.withdraw(value).build_transaction(tx_params) - + def allowance(self, owner, spender): return self.contract.functions.allowance(owner, spender).call() - + def balanceOf(self, owner): return self.contract.functions.balanceOf(owner).call() - - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/resources/Dispute.py b/src/story_protocol_python_sdk/resources/Dispute.py index b232419..e9fd344 100644 --- a/src/story_protocol_python_sdk/resources/Dispute.py +++ b/src/story_protocol_python_sdk/resources/Dispute.py @@ -2,9 +2,15 @@ from eth_abi.abi import encode from story_protocol_python_sdk.resources.WIP import WIP -from story_protocol_python_sdk.abi.DisputeModule.DisputeModule_client import DisputeModuleClient -from story_protocol_python_sdk.abi.ArbitrationPolicyUMA.ArbitrationPolicyUMA_client import ArbitrationPolicyUMAClient -from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import IPAccountImplClient +from story_protocol_python_sdk.abi.DisputeModule.DisputeModule_client import ( + DisputeModuleClient, +) +from story_protocol_python_sdk.abi.ArbitrationPolicyUMA.ArbitrationPolicyUMA_client import ( + ArbitrationPolicyUMAClient, +) +from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( + IPAccountImplClient, +) from story_protocol_python_sdk.abi.WIP.WIP_client import WIPClient from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -12,6 +18,7 @@ from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS from story_protocol_python_sdk.utils.oov3 import get_assertion_bond + class Dispute: """ A class to manage disputes on Story Protocol. @@ -20,6 +27,7 @@ class Dispute: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -33,7 +41,7 @@ def __init__(self, web3: Web3, account, chain_id: int): def _validate_address(self, address: str) -> str: """ Validates if a string is a valid Ethereum address. - + :param address str: The address to validate :return str: The validated address :raises ValueError: If the address is invalid @@ -42,7 +50,15 @@ def _validate_address(self, address: str) -> str: raise ValueError(f"Invalid address: {address}.") return address - def raise_dispute(self, target_ip_id: str, target_tag: str, cid: str, liveness: int, bond: int, tx_options: dict = None) -> dict: + def raise_dispute( + self, + target_ip_id: str, + target_tag: str, + cid: str, + liveness: int, + bond: int, + tx_options: dict = None, + ) -> dict: """ Raises a dispute on a given IP ID. @@ -59,10 +75,12 @@ def raise_dispute(self, target_ip_id: str, target_tag: str, cid: str, liveness: target_ip_id = self._validate_address(target_ip_id) # Convert tag to bytes32 - tag_bytes = self.web3.to_hex(text=target_tag).ljust(66, '0') + tag_bytes = self.web3.to_hex(text=target_tag).ljust(66, "0") # Check if tag is whitelisted - is_whitelisted = self.dispute_module_client.isWhitelistedDisputeTag(tag_bytes) + is_whitelisted = self.dispute_module_client.isWhitelistedDisputeTag( + tag_bytes + ) if not is_whitelisted: raise ValueError(f"The target tag {target_tag} is not whitelisted.") @@ -70,18 +88,20 @@ def raise_dispute(self, target_ip_id: str, target_tag: str, cid: str, liveness: min_liveness = self.arbitration_policy_uma_client.minLiveness() max_liveness = self.arbitration_policy_uma_client.maxLiveness() if liveness < min_liveness or liveness > max_liveness: - raise ValueError(f"Liveness must be between {min_liveness} and {max_liveness}.") + raise ValueError( + f"Liveness must be between {min_liveness} and {max_liveness}." + ) # Check bond amount max_bonds = self.arbitration_policy_uma_client.maxBonds( - token=self.web3.to_checksum_address("0x1514000000000000000000000000000000000000") + token=self.web3.to_checksum_address( + "0x1514000000000000000000000000000000000000" + ) ) if bond > max_bonds: raise ValueError(f"Bond must be less than {max_bonds}.") - deposit_response = self.wip.deposit( - amount=bond - ) + deposit_response = self.wip.deposit(amount=bond) # Convert CID to IPFS hash dispute_evidence_hash = convert_cid_to_hash_ipfs(cid) @@ -89,13 +109,9 @@ def raise_dispute(self, target_ip_id: str, target_tag: str, cid: str, liveness: # Encode the data for the arbitration policy data = encode( ["uint64", "address", "uint256"], - [ - liveness, - "0x1514000000000000000000000000000000000000", - bond - ] + [liveness, "0x1514000000000000000000000000000000000000", bond], ) - + response = build_and_send_transaction( self.web3, self.account, @@ -104,20 +120,22 @@ def raise_dispute(self, target_ip_id: str, target_tag: str, cid: str, liveness: dispute_evidence_hash, tag_bytes, data, - tx_options=tx_options + tx_options=tx_options, ) - dispute_id = self._parse_tx_dispute_raised_event(response['tx_receipt']) + dispute_id = self._parse_tx_dispute_raised_event(response["tx_receipt"]) return { - 'tx_hash': response['tx_hash'], - 'dispute_id': dispute_id if dispute_id else None + "tx_hash": response["tx_hash"], + "dispute_id": dispute_id if dispute_id else None, } except Exception as e: raise ValueError(f"Failed to raise dispute: {str(e)}") - def cancel_dispute(self, dispute_id: int, data: str = "0x", tx_options: dict = None) -> dict: + def cancel_dispute( + self, dispute_id: int, data: str = "0x", tx_options: dict = None + ) -> dict: """ Cancels an ongoing dispute. @@ -133,17 +151,17 @@ def cancel_dispute(self, dispute_id: int, data: str = "0x", tx_options: dict = N self.dispute_module_client.build_cancelDispute_transaction, dispute_id, data, - tx_options=tx_options + tx_options=tx_options, ) - return { - 'tx_hash': response['tx_hash'] - } + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to cancel dispute: {str(e)}") - def resolve_dispute(self, dispute_id: int, data: str, tx_options: dict = None) -> dict: + def resolve_dispute( + self, dispute_id: int, data: str, tx_options: dict = None + ) -> dict: """ Resolves a dispute after it has been judged. @@ -159,17 +177,17 @@ def resolve_dispute(self, dispute_id: int, data: str, tx_options: dict = None) - self.dispute_module_client.build_resolveDispute_transaction, dispute_id, data, - tx_options=tx_options + tx_options=tx_options, ) - return { - 'tx_hash': response['tx_hash'] - } + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to resolve dispute: {str(e)}") - def tag_if_related_ip_infringed(self, infringement_tags: list, tx_options: dict = None) -> list: + def tag_if_related_ip_infringed( + self, infringement_tags: list, tx_options: dict = None + ) -> list: """ Tags a derivative if a parent has been tagged with an infringement tag. @@ -181,20 +199,20 @@ def tag_if_related_ip_infringed(self, infringement_tags: list, tx_options: dict """ try: tx_hashes = [] - + for tag in infringement_tags: - ip_id = self._validate_address(tag['ip_id']) - + ip_id = self._validate_address(tag["ip_id"]) + response = build_and_send_transaction( self.web3, self.account, self.dispute_module_client.build_tagIfRelatedIpInfringed_transaction, ip_id, - tag['dispute_id'], - tx_options=tx_options + tag["dispute_id"], + tx_options=tx_options, ) - - tx_hashes.append(response['tx_hash']) + + tx_hashes.append(response["tx_hash"]) return tx_hashes @@ -212,14 +230,20 @@ def _parse_tx_dispute_raised_event(self, tx_receipt: dict) -> int: text="DisputeRaised(uint256,address,address,uint256,address,bytes32,bytes32,bytes)" ).hex() - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - data = log['data'] - dispute_id = int.from_bytes(data[:32], byteorder='big') + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + data = log["data"] + dispute_id = int.from_bytes(data[:32], byteorder="big") return dispute_id return None - def dispute_assertion(self, assertion_id: str, counter_evidence_cid: str, ip_id: str, tx_options: dict = None) -> dict: + def dispute_assertion( + self, + assertion_id: str, + counter_evidence_cid: str, + ip_id: str, + tx_options: dict = None, + ) -> dict: """ Counters a dispute that was raised by another party on an IP using counter evidence. The counter evidence (e.g., documents, images) should be uploaded to IPFS, @@ -240,84 +264,85 @@ def dispute_assertion(self, assertion_id: str, counter_evidence_cid: str, ip_id: try: # Validate IP ID ip_id = self._validate_address(ip_id) - + # Create IP Account client ip_account = IPAccountImplClient(self.web3, contract_address=ip_id) - + # Get assertion details to determine bond amount - bond = get_assertion_bond(self.web3, self.arbitration_policy_uma_client, assertion_id) + bond = get_assertion_bond( + self.web3, self.arbitration_policy_uma_client, assertion_id + ) # Check if user has enough WIP tokens user_balance = self.wip.balance_of(address=self.account.address) - + if user_balance < bond: - raise ValueError(f"Insufficient WIP balance. Required: {bond}, Available: {user_balance}") - + raise ValueError( + f"Insufficient WIP balance. Required: {bond}, Available: {user_balance}" + ) + # Convert CID to IPFS hash counter_evidence_hash = convert_cid_to_hash_ipfs(counter_evidence_cid) - + # Get encoded data for dispute assertion encoded_data = self.arbitration_policy_uma_client.contract.encode_abi( abi_element_identifier="disputeAssertion", - args= [ - assertion_id, - counter_evidence_hash - ] + args=[assertion_id, counter_evidence_hash], ) # Check allowance allowance = self.wip.allowance( - owner=self.account.address, - spender=ip_account.contract.address + owner=self.account.address, spender=ip_account.contract.address ) - + # Approve IP Account to transfer WrappedIP tokens if needed if allowance < bond: approve_tx = self.wip.approve( - spender=ip_account.contract.address, - amount=2**256 - 1 # maxUint256 + spender=ip_account.contract.address, amount=2**256 - 1 # maxUint256 ) - + # Prepare calls for executeBatch calls = [] - + if bond > 0: # Transfer tokens from wallet to IP Account transfer_data = self.wip_client.contract.encode_abi( abi_element_identifier="transferFrom", - args=[ - self.account.address, - ip_account.contract.address, - bond - ] + args=[self.account.address, ip_account.contract.address, bond], + ) + calls.append( + { + "target": self.wip_client.contract.address, + "value": 0, + "data": transfer_data, + } ) - calls.append({ - 'target': self.wip_client.contract.address, - 'value': 0, - 'data': transfer_data - }) - + # Approve arbitration policy to spend tokens approve_data = self.wip_client.contract.encode_abi( abi_element_identifier="approve", args=[ self.arbitration_policy_uma_client.contract.address, - 2**256 - 1 # maxUint256 - ] + 2**256 - 1, # maxUint256 + ], + ) + calls.append( + { + "target": self.wip_client.contract.address, + "value": 0, + "data": approve_data, + } ) - calls.append({ - 'target': self.wip_client.contract.address, - 'value': 0, - 'data': approve_data - }) - + # Add dispute assertion call - calls.append({ - 'target': self.arbitration_policy_uma_client.contract.address, - 'value': 0, - 'data': encoded_data - }) - + calls.append( + { + "target": self.arbitration_policy_uma_client.contract.address, + "value": 0, + "data": encoded_data, + } + ) + # Execute batch transaction response = build_and_send_transaction( self.web3, @@ -325,14 +350,14 @@ def dispute_assertion(self, assertion_id: str, counter_evidence_cid: str, ip_id: ip_account.build_executeBatch_transaction, calls, 0, - tx_options=tx_options + tx_options=tx_options, ) return { - 'tx_hash': response['tx_hash'], - 'receipt': response.get('tx_receipt') + "tx_hash": response["tx_hash"], + "receipt": response.get("tx_receipt"), } - + except Exception as e: raise ValueError(f"Failed to dispute assertion: {str(e)}") @@ -345,13 +370,17 @@ def dispute_id_to_assertion_id(self, dispute_id: int) -> str: :raises ValueError: If there is an error during the conversion. """ try: - assertion_id = self.arbitration_policy_uma_client.disputeIdToAssertionId(dispute_id) + assertion_id = self.arbitration_policy_uma_client.disputeIdToAssertionId( + dispute_id + ) return assertion_id except Exception as e: raise ValueError(f"Failed to convert dispute ID to assertion ID: {str(e)}") - + def get_assertion_bond(self, assertion_id: str) -> int: """ Get the bond amount for a given assertion ID. """ - return get_assertion_bond(self.web3, self.arbitration_policy_uma_client, assertion_id) \ No newline at end of file + return get_assertion_bond( + self.web3, self.arbitration_policy_uma_client, assertion_id + ) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 77aef26..079c7ad 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -2,14 +2,30 @@ from web3 import Web3 -from story_protocol_python_sdk.abi.GroupingModule.GroupingModule_client import GroupingModuleClient -from story_protocol_python_sdk.abi.GroupingWorkflows.GroupingWorkflows_client import GroupingWorkflowsClient -from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import IPAssetRegistryClient -from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import CoreMetadataModuleClient -from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import LicensingModuleClient -from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import LicenseRegistryClient -from story_protocol_python_sdk.abi.PILicenseTemplate.PILicenseTemplate_client import PILicenseTemplateClient -from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import IPAccountImplClient +from story_protocol_python_sdk.abi.GroupingModule.GroupingModule_client import ( + GroupingModuleClient, +) +from story_protocol_python_sdk.abi.GroupingWorkflows.GroupingWorkflows_client import ( + GroupingWorkflowsClient, +) +from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( + IPAssetRegistryClient, +) +from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import ( + CoreMetadataModuleClient, +) +from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import ( + LicensingModuleClient, +) +from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import ( + LicenseRegistryClient, +) +from story_protocol_python_sdk.abi.PILicenseTemplate.PILicenseTemplate_client import ( + PILicenseTemplateClient, +) +from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( + IPAccountImplClient, +) from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -25,6 +41,7 @@ class Group: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -58,24 +75,18 @@ def register_group(self, group_pool: str, tx_options: dict = None) -> dict: self.account, self.grouping_module_client.build_registerGroup_transaction, group_pool, - tx_options=tx_options + tx_options=tx_options, ) - group_id = self._parse_tx_ip_group_registered_event(response['tx_receipt']) - - return { - 'tx_hash': response['tx_hash'], - 'group_id': group_id - } - + group_id = self._parse_tx_ip_group_registered_event(response["tx_receipt"]) + + return {"tx_hash": response["tx_hash"], "group_id": group_id} + except Exception as e: raise ValueError(f"Failed to register group: {str(e)}") def register_group_and_attach_license( - self, - group_pool: str, - license_data: dict, - tx_options: dict = None + self, group_pool: str, license_data: dict, tx_options: dict = None ) -> dict: """ Register a group IP with a group reward pool and attach license terms to the group IP. @@ -91,22 +102,19 @@ def register_group_and_attach_license( # Process license data license_data_processed = self._get_license_data([license_data])[0] - + response = build_and_send_transaction( self.web3, self.account, self.grouping_workflows_client.build_registerGroupAndAttachLicense_transaction, group_pool, license_data_processed, - tx_options=tx_options + tx_options=tx_options, ) - group_id = self._parse_tx_ip_group_registered_event(response['tx_receipt']) + group_id = self._parse_tx_ip_group_registered_event(response["tx_receipt"]) - return { - 'tx_hash': response['tx_hash'], - 'group_id': group_id - } + return {"tx_hash": response["tx_hash"], "group_id": group_id} except Exception as e: raise ValueError(f"Failed to register group and attach license: {str(e)}") @@ -121,10 +129,10 @@ def mint_and_register_ip_and_attach_license_and_add_to_group( recipient: str = None, allow_duplicates: bool = True, deadline: int = None, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ - Mint an NFT from a SPGNFT collection, register it with metadata as an IP, + Mint an NFT from a SPGNFT collection, register it with metadata as an IP, attach license terms to the registered IP, and add it to a group IP. :param group_id str: The ID of the group to add the IP to. @@ -141,40 +149,46 @@ def mint_and_register_ip_and_attach_license_and_add_to_group( try: if not self.web3.is_address(group_id): raise ValueError(f'Group ID "{group_id}" is invalid.') - + if not self.web3.is_address(spg_nft_contract): - raise ValueError(f'SPG NFT contract address "{spg_nft_contract}" is invalid.') - + raise ValueError( + f'SPG NFT contract address "{spg_nft_contract}" is invalid.' + ) + is_registered = self.ip_asset_registry_client.isRegistered(group_id) if not is_registered: raise ValueError(f"Group IP {group_id} is not registered.") - + # Get IP account state ip_account = IPAccountImplClient(self.web3, contract_address=group_id) state = ip_account.state() - + # Calculate deadline calculated_deadline = self.sign_util.get_deadline(deadline=deadline) - + # Get permission signature for adding to group sig_add_to_group = self.sign_util.get_permission_signature( ip_id=group_id, deadline=calculated_deadline, state=state, - permissions=[{ - 'ipId': group_id, - 'signer': self.grouping_workflows_client.contract.address, - 'to': self.grouping_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "addIp(address,address[],uint256)", - }] + permissions=[ + { + "ipId": group_id, + "signer": self.grouping_workflows_client.contract.address, + "to": self.grouping_module_client.contract.address, + "permission": 1, # ALLOW + "func": "addIp(address,address[],uint256)", + } + ], ) licenses_data = self._get_license_data(license_data) - + metadata = self._get_ip_metadata(ip_metadata) - - max_allowed_reward_share = self.license_terms_util.get_revenue_share(max_allowed_reward_share) + + max_allowed_reward_share = self.license_terms_util.get_revenue_share( + max_allowed_reward_share + ) # Set recipient to caller if not provided if not recipient: @@ -191,25 +205,31 @@ def mint_and_register_ip_and_attach_license_and_add_to_group( licenses_data, metadata, { - 'signer': self.account.address, - 'deadline': calculated_deadline, - 'signature': self.web3.to_bytes(hexstr=sig_add_to_group["signature"]) + "signer": self.account.address, + "deadline": calculated_deadline, + "signature": self.web3.to_bytes( + hexstr=sig_add_to_group["signature"] + ), }, allow_duplicates, - tx_options=tx_options + tx_options=tx_options, ) - + # Parse events to get IP ID and token ID - registration_data = self._parse_tx_ip_registered_event(response['tx_receipt']) - + registration_data = self._parse_tx_ip_registered_event( + response["tx_receipt"] + ) + return { - 'tx_hash': response['tx_hash'], - 'ip_id': registration_data['ip_id'], - 'token_id': registration_data['token_id'] + "tx_hash": response["tx_hash"], + "ip_id": registration_data["ip_id"], + "token_id": registration_data["token_id"], } - + except Exception as e: - raise ValueError(f"Failed to mint and register IP and attach license and add to group: {str(e)}") + raise ValueError( + f"Failed to mint and register IP and attach license and add to group: {str(e)}" + ) def register_ip_and_attach_license_and_add_to_group( self, @@ -220,10 +240,10 @@ def register_ip_and_attach_license_and_add_to_group( max_allowed_reward_share: int, ip_metadata: dict = None, deadline: int = None, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ - Register an NFT as IP with metadata, attach license terms to the registered IP, + Register an NFT as IP with metadata, attach license terms to the registered IP, and add it to a group IP. :param group_id str: The ID of the group to add the IP to. @@ -239,43 +259,43 @@ def register_ip_and_attach_license_and_add_to_group( try: if not self.web3.is_address(group_id): raise ValueError(f'Group ID "{group_id}" is invalid.') - + if not self.web3.is_address(nft_contract): raise ValueError(f'NFT contract address "{nft_contract}" is invalid.') - + # Check if group is registered is_registered = self.ip_asset_registry_client.isRegistered(group_id) if not is_registered: raise ValueError(f"Group IP {group_id} is not registered.") - + # Get IP ID for the NFT ip_id = self.ip_asset_registry_client.ipId( - self.chain_id, - nft_contract, - token_id + self.chain_id, nft_contract, token_id ) - + # Get IP account state ip_account = IPAccountImplClient(self.web3, group_id) state = ip_account.state() - + # Calculate deadline calculated_deadline = self.sign_util.get_deadline(deadline=deadline) - + # Get permission signature for adding to group sig_add_to_group = self.sign_util.get_permission_signature( ip_id=group_id, deadline=calculated_deadline, state=state, - permissions=[{ - 'ipId': group_id, - 'signer': self.grouping_workflows_client.contract.address, - 'to': self.grouping_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "addIp(address,address[],uint256)", - }] + permissions=[ + { + "ipId": group_id, + "signer": self.grouping_workflows_client.contract.address, + "to": self.grouping_module_client.contract.address, + "permission": 1, # ALLOW + "func": "addIp(address,address[],uint256)", + } + ], ) - + # Get permission signature for metadata and license sig_metadata_and_attach = self.sign_util.get_permission_signature( ip_id=ip_id, @@ -283,32 +303,32 @@ def register_ip_and_attach_license_and_add_to_group( state=self.web3.to_bytes(hexstr=ZERO_HASH), permissions=[ { - 'ipId': ip_id, - 'signer': self.grouping_workflows_client.contract.address, - 'to': self.core_metadata_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "setAll(address,string,bytes32,bytes32)" + "ipId": ip_id, + "signer": self.grouping_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": 1, # ALLOW + "func": "setAll(address,string,bytes32,bytes32)", }, { - 'ipId': ip_id, - 'signer': self.grouping_workflows_client.contract.address, - 'to': self.licensing_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "attachLicenseTerms(address,address,uint256)" + "ipId": ip_id, + "signer": self.grouping_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": 1, # ALLOW + "func": "attachLicenseTerms(address,address,uint256)", }, { - 'ipId': ip_id, - 'signer': self.grouping_workflows_client.contract.address, - 'to': self.licensing_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))" - } - ] + "ipId": ip_id, + "signer": self.grouping_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": 1, # ALLOW + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ], ) - + # Process license data licenses_data = self._get_license_data(license_data) - + # Process IP metadata metadata = self._get_ip_metadata(ip_metadata) @@ -323,29 +343,37 @@ def register_ip_and_attach_license_and_add_to_group( licenses_data, metadata, { - 'signer': self.account.address, - 'deadline': calculated_deadline, - 'signature': self.web3.to_bytes(hexstr=sig_metadata_and_attach['signature']) + "signer": self.account.address, + "deadline": calculated_deadline, + "signature": self.web3.to_bytes( + hexstr=sig_metadata_and_attach["signature"] + ), }, { - 'signer': self.account.address, - 'deadline': calculated_deadline, - 'signature': self.web3.to_bytes(hexstr=sig_add_to_group['signature']) + "signer": self.account.address, + "deadline": calculated_deadline, + "signature": self.web3.to_bytes( + hexstr=sig_add_to_group["signature"] + ), }, - tx_options=tx_options + tx_options=tx_options, ) - + # Parse events to get IP ID and token ID - registration_data = self._parse_tx_ip_registered_event(response['tx_receipt']) - + registration_data = self._parse_tx_ip_registered_event( + response["tx_receipt"] + ) + return { - 'tx_hash': response['tx_hash'], - 'ip_id': registration_data['ip_id'], - 'token_id': registration_data['token_id'] + "tx_hash": response["tx_hash"], + "ip_id": registration_data["ip_id"], + "token_id": registration_data["token_id"], } - + except Exception as e: - raise ValueError(f"Failed to register IP and attach license and add to group: {str(e)}") + raise ValueError( + f"Failed to register IP and attach license and add to group: {str(e)}" + ) def register_group_and_attach_license_and_add_ips( self, @@ -353,10 +381,10 @@ def register_group_and_attach_license_and_add_ips( ip_ids: list, license_data: dict, max_allowed_reward_share: int, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ - Register a group IP with a group reward pool, attach license terms to the group IP, + Register a group IP with a group reward pool, attach license terms to the group IP, and add individual IPs to the group IP. :param group_pool str: The address of the group pool. @@ -369,31 +397,31 @@ def register_group_and_attach_license_and_add_ips( try: if not self.web3.is_address(group_pool): raise ValueError(f'Group pool address "{group_pool}" is invalid.') - + # Validate IP IDs for ip_id in ip_ids: if not self.web3.is_address(ip_id): raise ValueError(f'IP ID "{ip_id}" is invalid.') - + is_registered = self.ip_asset_registry_client.isRegistered(ip_id) if not is_registered: raise ValueError(f"IP {ip_id} is not registered.") - + # Process license data license_data_processed = self._get_license_data([license_data])[0] - + # Check if license terms are attached to all IPs for ip_id in ip_ids: is_attached = self.license_registry_client.hasIpAttachedLicenseTerms( ip_id, - license_data_processed['licenseTemplate'], - license_data_processed['licenseTermsId'] + license_data_processed["licenseTemplate"], + license_data_processed["licenseTermsId"], ) if not is_attached: raise ValueError( f"License terms must be attached to IP {ip_id} before adding to group." ) - + response = build_and_send_transaction( self.web3, self.account, @@ -402,25 +430,24 @@ def register_group_and_attach_license_and_add_ips( ip_ids, self.license_terms_util.get_revenue_share(max_allowed_reward_share), license_data_processed, - tx_options=tx_options + tx_options=tx_options, ) - - group_id = self._parse_tx_ip_group_registered_event(response['tx_receipt']) - - return { - 'tx_hash': response['tx_hash'], - 'group_id': group_id - } - + + group_id = self._parse_tx_ip_group_registered_event(response["tx_receipt"]) + + return {"tx_hash": response["tx_hash"], "group_id": group_id} + except Exception as e: - raise ValueError(f"Failed to register group and attach license and add IPs: {str(e)}") + raise ValueError( + f"Failed to register group and attach license and add IPs: {str(e)}" + ) def collect_and_distribute_group_royalties( self, group_ip_id: str, currency_tokens: list, member_ip_ids: list, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Collect royalties for the entire group and distribute the rewards to each member IP's royalty vault. @@ -434,35 +461,39 @@ def collect_and_distribute_group_royalties( try: if not self.web3.is_address(group_ip_id): raise ValueError(f'Group IP ID "{group_ip_id}" is invalid.') - + if not currency_tokens: raise ValueError("At least one currency token is required.") - + if not member_ip_ids: raise ValueError("At least one member IP ID is required.") - + # Validate currency tokens for token in currency_tokens: if not self.web3.is_address(token): raise ValueError(f'Currency token address "{token}" is invalid.') - + if token == ZERO_ADDRESS: raise ValueError("Currency token cannot be the zero address.") - + # Validate group IP - is_group_registered = self.ip_asset_registry_client.isRegistered(group_ip_id) + is_group_registered = self.ip_asset_registry_client.isRegistered( + group_ip_id + ) if not is_group_registered: - raise ValueError(f"The group IP with ID {group_ip_id} is not registered.") - + raise ValueError( + f"The group IP with ID {group_ip_id} is not registered." + ) + # Validate member IPs for ip_id in member_ip_ids: if not self.web3.is_address(ip_id): raise ValueError(f'Member IP ID "{ip_id}" is invalid.') - + is_member_registered = self.ip_asset_registry_client.isRegistered(ip_id) if not is_member_registered: raise ValueError(f"Member IP with ID {ip_id} is not registered.") - + response = build_and_send_transaction( self.web3, self.account, @@ -470,21 +501,29 @@ def collect_and_distribute_group_royalties( group_ip_id, currency_tokens, member_ip_ids, - tx_options=tx_options + tx_options=tx_options, ) # Parse events to get collected royalties - collected_royalties = self._parse_tx_collected_royalties_to_group_pool_event(response['tx_receipt']) - royalties_distributed = self._parse_tx_royalty_paid_event(response['tx_receipt']) - + collected_royalties = ( + self._parse_tx_collected_royalties_to_group_pool_event( + response["tx_receipt"] + ) + ) + royalties_distributed = self._parse_tx_royalty_paid_event( + response["tx_receipt"] + ) + return { - 'tx_hash': response['tx_hash'], - 'collected_royalties': collected_royalties, - 'royalties_distributed': royalties_distributed + "tx_hash": response["tx_hash"], + "collected_royalties": collected_royalties, + "royalties_distributed": royalties_distributed, } - + except Exception as e: - raise ValueError(f"Failed to collect and distribute group royalties: {str(e)}") + raise ValueError( + f"Failed to collect and distribute group royalties: {str(e)}" + ) def _get_license_data(self, license_data: list) -> list: """ @@ -495,46 +534,52 @@ def _get_license_data(self, license_data: list) -> list: """ if not license_data: raise ValueError("License data is required.") - + result = [] for item in license_data: # Check if license_template is provided - if 'license_template' in item: - license_template = item['license_template'] + if "license_template" in item: + license_template = item["license_template"] else: license_template = self.pi_license_template_client.contract.address - + if not self.web3.is_address(license_template): - raise ValueError(f'License template address "{license_template}" is invalid.') - + raise ValueError( + f'License template address "{license_template}" is invalid.' + ) + # Validate licensing config - licensing_config = item.get('licensing_config', {}) - + licensing_config = item.get("licensing_config", {}) + try: self.license_terms_util.validate_licensing_config(licensing_config) except Exception as e: raise ValueError(f"Licensing config validation failed: {str(e)}") - + # Convert to camelCase for contract interaction camelcase_config = { - 'isSet': licensing_config.get('is_set', True), - 'mintingFee': licensing_config.get('minting_fee', 0), - 'hookData': licensing_config.get('hook_data', ZERO_ADDRESS), - 'licensingHook': licensing_config.get('licensing_hook', ZERO_ADDRESS), - 'commercialRevShare': licensing_config.get('commercial_rev_share', 0), - 'disabled': licensing_config.get('disabled', False), - 'expectMinimumGroupRewardShare': licensing_config.get('expect_minimum_group_reward_share', 0), - 'expectGroupRewardPool': licensing_config.get('expect_group_reward_pool', ZERO_ADDRESS) + "isSet": licensing_config.get("is_set", True), + "mintingFee": licensing_config.get("minting_fee", 0), + "hookData": licensing_config.get("hook_data", ZERO_ADDRESS), + "licensingHook": licensing_config.get("licensing_hook", ZERO_ADDRESS), + "commercialRevShare": licensing_config.get("commercial_rev_share", 0), + "disabled": licensing_config.get("disabled", False), + "expectMinimumGroupRewardShare": licensing_config.get( + "expect_minimum_group_reward_share", 0 + ), + "expectGroupRewardPool": licensing_config.get( + "expect_group_reward_pool", ZERO_ADDRESS + ), } - + processed_item = { - 'licenseTemplate': license_template, - 'licenseTermsId': item['license_terms_id'], - 'licensingConfig': camelcase_config + "licenseTemplate": license_template, + "licenseTermsId": item["license_terms_id"], + "licensingConfig": camelcase_config, } - + result.append(processed_item) - + return result def _get_ip_metadata(self, ip_metadata: dict = None) -> dict: @@ -545,20 +590,22 @@ def _get_ip_metadata(self, ip_metadata: dict = None) -> dict: :return dict: Processed IP metadata. """ metadata = { - 'ipMetadataURI': "", - 'ipMetadataHash': ZERO_HASH, - 'nftMetadataURI': "", - 'nftMetadataHash': ZERO_HASH, + "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), - }) - + 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), + } + ) + return metadata def _parse_tx_ip_group_registered_event(self, tx_receipt: dict) -> str: @@ -569,13 +616,15 @@ def _parse_tx_ip_group_registered_event(self, tx_receipt: dict) -> str: :return str: The group ID. :raises ValueError: If the event is not found in the transaction receipt. """ - event_signature = self.web3.keccak(text="IPGroupRegistered(address,address)").hex() - - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - group_id = '0x' + log['topics'][1].hex()[24:] + event_signature = self.web3.keccak( + text="IPGroupRegistered(address,address)" + ).hex() + + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + group_id = "0x" + log["topics"][1].hex()[24:] return self.web3.to_checksum_address(group_id) - + raise ValueError("IPGroupRegistered event not found in transaction receipt") def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> dict: @@ -589,41 +638,47 @@ def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> dict: event_signature = self.web3.keccak( text="IPRegistered(address,uint256,address,uint256,string,string,uint256)" ).hex() - - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - ip_id = '0x' + log['data'].hex()[24:64] - token_id = int(log['topics'][3].hex(), 16) - + + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + ip_id = "0x" + log["data"].hex()[24:64] + token_id = int(log["topics"][3].hex(), 16) + return { - 'ip_id': self.web3.to_checksum_address(ip_id), - 'token_id': token_id + "ip_id": self.web3.to_checksum_address(ip_id), + "token_id": token_id, } - + raise ValueError("IPRegistered event not found in transaction receipt") - def _parse_tx_collected_royalties_to_group_pool_event(self, tx_receipt: dict) -> list: + def _parse_tx_collected_royalties_to_group_pool_event( + self, tx_receipt: dict + ) -> list: """ Parse the CollectedRoyaltiesToGroupPool event from a transaction receipt. :param tx_receipt dict: The transaction receipt. :return list: List of collected royalties. """ - event_signature = self.web3.keccak(text="CollectedRoyaltiesToGroupPool(address,address,address,uint256)").hex() + event_signature = self.web3.keccak( + text="CollectedRoyaltiesToGroupPool(address,address,address,uint256)" + ).hex() collected_royalties = [] - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - group_id = '0x' + log['topics'][1].hex()[24:] - amount = int(log['data'][:66].hex(), 16) - token = '0x' + log['topics'][2].hex()[24:] - - collected_royalties.append({ - 'group_id': self.web3.to_checksum_address(group_id), - 'amount': amount, - 'token': self.web3.to_checksum_address(token) - }) - + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + group_id = "0x" + log["topics"][1].hex()[24:] + amount = int(log["data"][:66].hex(), 16) + token = "0x" + log["topics"][2].hex()[24:] + + collected_royalties.append( + { + "group_id": self.web3.to_checksum_address(group_id), + "amount": amount, + "token": self.web3.to_checksum_address(token), + } + ) + return collected_royalties def _parse_tx_royalty_paid_event(self, tx_receipt: dict) -> list: @@ -633,22 +688,26 @@ def _parse_tx_royalty_paid_event(self, tx_receipt: dict) -> list: :param tx_receipt dict: The transaction receipt. :return list: List of royalties distributed. """ - event_signature = self.web3.keccak(text="RoyaltyPaid(address,address,address,address,uint256,uint256)").hex() + event_signature = self.web3.keccak( + text="RoyaltyPaid(address,address,address,address,uint256,uint256)" + ).hex() royalties_distributed = [] - - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - receiver_ip_id = '0x' + log['topics'][0].hex()[24:] - data = log['data'] + + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + receiver_ip_id = "0x" + log["topics"][0].hex()[24:] + data = log["data"] amount = int(data[128:160].hex(), 16) - token = '0x' + data[108:128].hex() + token = "0x" + data[108:128].hex() amount_after_fee = int(data[160:].hex(), 16) - - royalties_distributed.append({ - 'ip_id': self.web3.to_checksum_address(receiver_ip_id), - 'amount': amount, - 'token': self.web3.to_checksum_address(token), - 'amount_after_fee': amount_after_fee - }) - + + royalties_distributed.append( + { + "ip_id": self.web3.to_checksum_address(receiver_ip_id), + "amount": amount, + "token": self.web3.to_checksum_address(token), + "amount_after_fee": amount_after_fee, + } + ) + return royalties_distributed diff --git a/src/story_protocol_python_sdk/resources/IPAccount.py b/src/story_protocol_python_sdk/resources/IPAccount.py index 2b4e05b..32fada8 100644 --- a/src/story_protocol_python_sdk/resources/IPAccount.py +++ b/src/story_protocol_python_sdk/resources/IPAccount.py @@ -2,14 +2,23 @@ from web3 import Web3 -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.AccessController.AccessController_client import AccessControllerClient -from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import CoreMetadataModuleClient +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.AccessController.AccessController_client import ( + AccessControllerClient, +) +from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import ( + CoreMetadataModuleClient, +) from story_protocol_python_sdk.abi.MockERC20.MockERC20_client import MockERC20Client from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction + class IPAccount: """A class to execute a transaction from the IP Account. @@ -38,12 +47,14 @@ def get_token(self, ip_id: str) -> dict: """ try: checksum_address = Web3.to_checksum_address(ip_id) - ip_account_client = IPAccountImplClient(self.web3, contract_address=checksum_address) + ip_account_client = IPAccountImplClient( + self.web3, contract_address=checksum_address + ) chain_id, token_contract, token_id = ip_account_client.token() return { - 'chain_id': chain_id, - 'token_contract': token_contract, - 'token_id': token_id + "chain_id": chain_id, + "token_contract": token_contract, + "token_id": token_id, } except ValueError: # Catch ValueError from to_checksum_address raise ValueError(f"Invalid IP id address: {ip_id}") @@ -56,12 +67,16 @@ def _validate_transaction_params(self, ip_id: str, to: str): :raises ValueError: If recipient address is invalid or IP is not registered. """ if not self.web3.is_address(to): - raise ValueError(f"The recipient of the transaction {to} is not a valid address.") - + raise ValueError( + f"The recipient of the transaction {to} is not a valid address." + ) + if not self._is_registered(ip_id): raise ValueError(f"The IP id {ip_id} is not registered.") - def execute(self, to: str, value: int, ip_id: str, data: str, tx_options: dict = None) -> dict: + def execute( + self, to: str, value: int, ip_id: str, data: str, tx_options: dict = None + ) -> dict: """Execute a transaction from the IP Account. :param to str: The recipient of the transaction. @@ -72,23 +87,33 @@ def execute(self, to: str, value: int, ip_id: str, data: str, tx_options: dict = :returns dict: Dictionary containing the transaction hash. """ self._validate_transaction_params(ip_id, to) - + ip_account_client = IPAccountImplClient(self.web3, contract_address=ip_id) - + response = build_and_send_transaction( self.web3, self.account, ip_account_client.build_execute_transaction, to, - value, + value, data, 0, - tx_options=tx_options + tx_options=tx_options, ) return response - def execute_with_sig(self, ip_id: str, to: str, data: str, signer: str, deadline: int, signature: str, value: int = 0, tx_options: dict = None) -> dict: + def execute_with_sig( + self, + ip_id: str, + to: str, + data: str, + signer: str, + deadline: int, + signature: str, + value: int = 0, + tx_options: dict = None, + ) -> dict: """Execute a signed transaction from the IP Account. :param ip_id str: The IP ID to get IP account. @@ -104,7 +129,7 @@ def execute_with_sig(self, ip_id: str, to: str, data: str, signer: str, deadline self._validate_transaction_params(ip_id, to) ip_account_client = IPAccountImplClient(self.web3, contract_address=ip_id) - + response = build_and_send_transaction( self.web3, self.account, @@ -115,11 +140,11 @@ def execute_with_sig(self, ip_id: str, to: str, data: str, signer: str, deadline signer, deadline, signature, - tx_options=tx_options + tx_options=tx_options, ) return response - + def get_ip_account_nonce(self, ip_id: str) -> bytes: """Get the IP Account's internal nonce for transaction ordering. @@ -129,7 +154,9 @@ def get_ip_account_nonce(self, ip_id: str) -> bytes: """ try: checksum_address = Web3.to_checksum_address(ip_id) - ip_account_client = IPAccountImplClient(self.web3, contract_address=checksum_address) + ip_account_client = IPAccountImplClient( + self.web3, contract_address=checksum_address + ) return ip_account_client.state() except ValueError: # Catch ValueError from to_checksum_address raise ValueError(f"Invalid IP id address: {ip_id}") @@ -139,9 +166,9 @@ def _is_registered(self, ip_id: str) -> bool: :param ip_id str: The IP ID to check. :returns bool: True if registered, False otherwise. - """ + """ return self.ip_asset_registry_client.isRegistered(ip_id) - + def owner(self, ip_id: str) -> str: """Get the owner of the IP Account. @@ -151,14 +178,18 @@ def owner(self, ip_id: str) -> str: """ try: checksum_address = Web3.to_checksum_address(ip_id) - ip_account_client = IPAccountImplClient(self.web3, contract_address=checksum_address) + ip_account_client = IPAccountImplClient( + self.web3, contract_address=checksum_address + ) return ip_account_client.owner() except ValueError: # Catch ValueError from to_checksum_address raise ValueError(f"Invalid IP id address: {ip_id}") except Exception as e: raise e - def set_ip_metadata(self, ip_id: str, metadata_uri: str, metadata_hash: str, tx_options: dict = None) -> dict: + def set_ip_metadata( + self, ip_id: str, metadata_uri: str, metadata_hash: str, tx_options: dict = None + ) -> dict: """Sets the metadataURI for an IP asset. :param ip_id str: The IP ID to set metadata for. @@ -174,7 +205,7 @@ def set_ip_metadata(self, ip_id: str, metadata_uri: str, metadata_hash: str, tx_ data = self.core_metadata_module_client.contract.encode_abi( abi_element_identifier="setMetadataURI", - args=[Web3.to_checksum_address(ip_id), metadata_uri, metadata_hash] + args=[Web3.to_checksum_address(ip_id), metadata_uri, metadata_hash], ) response = self.execute( @@ -182,7 +213,7 @@ def set_ip_metadata(self, ip_id: str, metadata_uri: str, metadata_hash: str, tx_ value=0, ip_id=ip_id, data=data, - tx_options=tx_options + tx_options=tx_options, ) return response @@ -207,35 +238,32 @@ def transfer_erc20(self, ip_id: str, tokens: list, tx_options: dict = None) -> d ip_account = IPAccountImplClient(self.web3, contract_address=ip_id) for token in tokens: - if not all(key in token for key in ['address', 'target', 'amount']): - raise ValueError("Each token transfer must include 'address', 'target', and 'amount'") - + if not all(key in token for key in ["address", "target", "amount"]): + raise ValueError( + "Each token transfer must include 'address', 'target', and 'amount'" + ) + calls = [] for token in tokens: - token_address = self.web3.to_checksum_address(token['address']) - target_address = self.web3.to_checksum_address(token['target']) - amount = int(token['amount']) - + token_address = self.web3.to_checksum_address(token["address"]) + target_address = self.web3.to_checksum_address(token["target"]) + amount = int(token["amount"]) + data = self.mock_erc20_client.contract.encode_abi( - abi_element_identifier="transfer", - args=[target_address, amount] + abi_element_identifier="transfer", args=[target_address, amount] ) - - calls.append({ - 'target': token_address, - 'data': data, - 'value': 0 - }) - + + calls.append({"target": token_address, "data": data, "value": 0}) + response = build_and_send_transaction( self.web3, self.account, ip_account.build_executeBatch_transaction, calls, 0, - tx_options=tx_options + tx_options=tx_options, ) - + return response except Exception as e: raise ValueError(f"Failed to transfer ERC20: {str(e)}") diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index d095f90..0278649 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -2,16 +2,36 @@ from web3 import Web3 -from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import IPAssetRegistryClient -from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import LicensingModuleClient -from story_protocol_python_sdk.abi.LicenseToken.LicenseToken_client import LicenseTokenClient -from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import LicenseRegistryClient -from story_protocol_python_sdk.abi.RegistrationWorkflows.RegistrationWorkflows_client import RegistrationWorkflowsClient -from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import LicenseAttachmentWorkflowsClient -from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import DerivativeWorkflowsClient -from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import CoreMetadataModuleClient -from story_protocol_python_sdk.abi.AccessController.AccessController_client import AccessControllerClient -from story_protocol_python_sdk.abi.PILicenseTemplate.PILicenseTemplate_client import PILicenseTemplateClient +from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( + IPAssetRegistryClient, +) +from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import ( + LicensingModuleClient, +) +from story_protocol_python_sdk.abi.LicenseToken.LicenseToken_client import ( + LicenseTokenClient, +) +from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import ( + LicenseRegistryClient, +) +from story_protocol_python_sdk.abi.RegistrationWorkflows.RegistrationWorkflows_client import ( + RegistrationWorkflowsClient, +) +from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import ( + LicenseAttachmentWorkflowsClient, +) +from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import ( + DerivativeWorkflowsClient, +) +from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import ( + CoreMetadataModuleClient, +) +from story_protocol_python_sdk.abi.AccessController.AccessController_client import ( + AccessControllerClient, +) +from story_protocol_python_sdk.abi.PILicenseTemplate.PILicenseTemplate_client import ( + PILicenseTemplateClient, +) from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -20,6 +40,7 @@ from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient + class IPAsset: """ IPAssetClient allows you to create, get, and list IP Assets with Story @@ -29,6 +50,7 @@ class IPAsset: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -39,7 +61,9 @@ def __init__(self, web3: Web3, account, chain_id: int): self.license_token_client = LicenseTokenClient(web3) self.license_registry_client = LicenseRegistryClient(web3) self.registration_workflows_client = RegistrationWorkflowsClient(web3) - self.license_attachment_workflows_client = LicenseAttachmentWorkflowsClient(web3) + self.license_attachment_workflows_client = LicenseAttachmentWorkflowsClient( + web3 + ) self.derivative_workflows_client = DerivativeWorkflowsClient(web3) self.core_metadata_module_client = CoreMetadataModuleClient(web3) self.access_controller_client = AccessControllerClient(web3) @@ -55,18 +79,17 @@ def mint( metadata_uri: str, metadata_hash: bytes, allow_duplicates: bool = False, - tx_options: dict = None + tx_options: dict = None, ): spg_nft_client = SPGNFTImplClient(self.web3, contract_address=nft_contract) - - def build_mint_transaction(to, metadata_uri, metadata_hash, allow_duplicates, transaction_options): + + def build_mint_transaction( + to, metadata_uri, metadata_hash, allow_duplicates, transaction_options + ): return spg_nft_client.contract.functions.mint( - to, - metadata_uri, - metadata_hash, - allow_duplicates + to, metadata_uri, metadata_hash, allow_duplicates ).build_transaction(transaction_options) - + response = build_and_send_transaction( self.web3, self.account, @@ -75,23 +98,23 @@ def build_mint_transaction(to, metadata_uri, metadata_hash, allow_duplicates, tr metadata_uri, metadata_hash, allow_duplicates, - tx_options=tx_options + tx_options=tx_options, ) - - tx_hash = response['tx_hash'] + + tx_hash = response["tx_hash"] # Ensure the transaction hash starts with "0x" - if isinstance(tx_hash, str) and not tx_hash.startswith('0x'): - tx_hash = '0x' + tx_hash - + if isinstance(tx_hash, str) and not tx_hash.startswith("0x"): + tx_hash = "0x" + tx_hash + return tx_hash - + def register( self, nft_contract: str, token_id: int, ip_metadata: dict = None, deadline: int = None, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Register an NFT as IP, creating a corresponding IP record. @@ -109,66 +132,71 @@ def register( try: ip_id = self._get_ip_id(nft_contract, token_id) if self._is_registered(ip_id): - return { - 'tx_hash': None, - 'ip_id': ip_id - } + return {"tx_hash": None, "ip_id": ip_id} req_object = { - 'tokenId': token_id, - 'nftContract': self.web3.to_checksum_address(nft_contract), - 'ipMetadata': { - 'ipMetadataURI': "", - 'ipMetadataHash': ZERO_HASH, - 'nftMetadataURI': "", - 'nftMetadataHash': ZERO_HASH, + "tokenId": token_id, + "nftContract": self.web3.to_checksum_address(nft_contract), + "ipMetadata": { + "ipMetadataURI": "", + "ipMetadataHash": ZERO_HASH, + "nftMetadataURI": "", + "nftMetadataHash": ZERO_HASH, }, - 'sigMetadata': { - 'signer': ZERO_ADDRESS, - 'deadline': 0, - 'signature': ZERO_HASH, + "sigMetadata": { + "signer": ZERO_ADDRESS, + "deadline": 0, + "signature": ZERO_HASH, }, } if ip_metadata: - req_object['ipMetadata'].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), - }) + req_object["ipMetadata"].update( + { + "ipMetadataURI": ip_metadata.get("ip_metadata_uri", ""), + "ipMetadataHash": ip_metadata.get( + "ip_metadata_hash", ZERO_HASH + ), + "nftMetadataURI": ip_metadata.get("nft_metadata_uri", ""), + "nftMetadataHash": ip_metadata.get( + "nft_metadata_hash", ZERO_HASH + ), + } + ) calculated_deadline = self.sign_util.get_deadline(deadline=deadline) signature_response = self.sign_util.get_permission_signature( ip_id=ip_id, deadline=calculated_deadline, state=self.web3.to_bytes(hexstr=ZERO_HASH), - permissions=[{ - 'ipId': ip_id, - 'signer': self.registration_workflows_client.contract.address, - 'to': self.core_metadata_module_client.contract.address, - 'func': "setAll(address,string,bytes32,bytes32)", - 'permission': 1 - }] + permissions=[ + { + "ipId": ip_id, + "signer": self.registration_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "func": "setAll(address,string,bytes32,bytes32)", + "permission": 1, + } + ], ) signature = self.web3.to_bytes(hexstr=signature_response["signature"]) - req_object['sigMetadata'] = { - 'signer': self.web3.to_checksum_address(self.account.address), - 'deadline': calculated_deadline, - 'signature': signature, + req_object["sigMetadata"] = { + "signer": self.web3.to_checksum_address(self.account.address), + "deadline": calculated_deadline, + "signature": signature, } response = build_and_send_transaction( self.web3, self.account, self.registration_workflows_client.build_registerIp_transaction, - req_object['nftContract'], - req_object['tokenId'], - req_object['ipMetadata'], - req_object['sigMetadata'], - tx_options=tx_options + req_object["nftContract"], + req_object["tokenId"], + req_object["ipMetadata"], + req_object["sigMetadata"], + tx_options=tx_options, ) else: response = build_and_send_transaction( @@ -178,19 +206,16 @@ def register( self.chain_id, nft_contract, token_id, - tx_options=tx_options + tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) + ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"]) - return { - 'tx_hash': response['tx_hash'], - 'ip_id': ip_registered['ip_id'] - } + return {"tx_hash": response["tx_hash"], "ip_id": ip_registered["ip_id"]} except Exception as e: raise e - + def register_derivative( self, child_ip_id: str, @@ -200,7 +225,7 @@ def register_derivative( max_rts: int = 0, max_revenue_share: int = 0, license_template: str = None, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Registers a derivative directly with parent IP's license terms, without needing license tokens, @@ -223,36 +248,38 @@ def register_derivative( """ try: if not self._is_registered(child_ip_id): - raise ValueError(f"The child IP with id {child_ip_id} is not registered.") - - derivative_data = self._validate_derivative_data({ - 'childIpId': child_ip_id, - 'parentIpIds': parent_ip_ids, - 'licenseTermsIds': license_terms_ids, - 'maxMintingFee': max_minting_fee, - 'maxRts': max_rts, - 'maxRevenueShare': max_revenue_share, - 'licenseTemplate': license_template - }) + raise ValueError( + f"The child IP with id {child_ip_id} is not registered." + ) + + derivative_data = self._validate_derivative_data( + { + "childIpId": child_ip_id, + "parentIpIds": parent_ip_ids, + "licenseTermsIds": license_terms_ids, + "maxMintingFee": max_minting_fee, + "maxRts": max_rts, + "maxRevenueShare": max_revenue_share, + "licenseTemplate": license_template, + } + ) response = build_and_send_transaction( self.web3, self.account, self.licensing_module_client.build_registerDerivative_transaction, - derivative_data['childIpId'], - derivative_data['parentIpIds'], - derivative_data['licenseTermsIds'], - derivative_data['licenseTemplate'], - derivative_data['royaltyContext'], - derivative_data['maxMintingFee'], - derivative_data['maxRts'], - derivative_data['maxRevenueShare'], - tx_options=tx_options + derivative_data["childIpId"], + derivative_data["parentIpIds"], + derivative_data["licenseTermsIds"], + derivative_data["licenseTemplate"], + derivative_data["royaltyContext"], + derivative_data["maxMintingFee"], + derivative_data["maxRts"], + derivative_data["maxRevenueShare"], + tx_options=tx_options, ) - return { - 'tx_hash': response['tx_hash'] - } + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError("Failed to register derivative") from e @@ -262,7 +289,7 @@ def register_derivative_with_license_tokens( child_ip_id: str, license_token_ids: list, max_rts: int = 0, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Registers a derivative with license tokens. The derivative IP is registered with license tokens @@ -280,10 +307,12 @@ def register_derivative_with_license_tokens( try: # Validate max_rts self._validate_max_rts(max_rts) - + # Validate child IP registration if not self._is_registered(child_ip_id): - raise ValueError(f"The child IP with id {child_ip_id} is not registered.") + raise ValueError( + f"The child IP with id {child_ip_id} is not registered." + ) # Validate license token IDs and ownership validated_token_ids = self._validate_license_token_ids(license_token_ids) @@ -297,16 +326,16 @@ def register_derivative_with_license_tokens( validated_token_ids, ZERO_ADDRESS, max_rts, - tx_options=tx_options + tx_options=tx_options, ) - return { - 'tx_hash': response['tx_hash'] - } + return {"tx_hash": response["tx_hash"]} except Exception as e: - raise ValueError(f"Failed to register derivative with license tokens: {str(e)}") - + raise ValueError( + f"Failed to register derivative with license tokens: {str(e)}" + ) + def mint_and_register_ip_asset_with_pil_terms( self, spg_nft_contract: str, @@ -314,7 +343,7 @@ def mint_and_register_ip_asset_with_pil_terms( ip_metadata: dict = None, recipient: str = None, allow_duplicates: bool = False, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Mint an NFT from a collection and register it as an IP. @@ -375,64 +404,77 @@ def mint_and_register_ip_asset_with_pil_terms( license_terms = [] for term in terms: validated_term = self.license_terms_util.validate_license_terms( - term['terms'] + term["terms"] ) validated_licensing_config = ( self.license_terms_util.validate_licensing_config( - term['licensing_config'] + term["licensing_config"] ) ) camelcase_term = { - 'transferable': term['terms']['transferable'], - 'royaltyPolicy': term['terms']['royalty_policy'], - 'defaultMintingFee': term['terms']['default_minting_fee'], - 'expiration': term['terms']['expiration'], - 'commercialUse': term['terms']['commercial_use'], - 'commercialAttribution': term['terms']['commercial_attribution'], - 'commercializerChecker': term['terms']['commercializer_checker'], - 'commercializerCheckerData': term['terms']['commercializer_checker_data'], - 'commercialRevShare': term['terms']['commercial_rev_share'], - 'commercialRevCeiling': term['terms']['commercial_rev_ceiling'], - 'derivativesAllowed': term['terms']['derivatives_allowed'], - 'derivativesAttribution': term['terms']['derivatives_attribution'], - 'derivativesApproval': term['terms']['derivatives_approval'], - 'derivativesReciprocal': term['terms']['derivatives_reciprocal'], - 'derivativeRevCeiling': term['terms']['derivative_rev_ceiling'], - 'currency': term['terms']['currency'], - 'uri': term['terms']['uri'] + "transferable": term["terms"]["transferable"], + "royaltyPolicy": term["terms"]["royalty_policy"], + "defaultMintingFee": term["terms"]["default_minting_fee"], + "expiration": term["terms"]["expiration"], + "commercialUse": term["terms"]["commercial_use"], + "commercialAttribution": term["terms"]["commercial_attribution"], + "commercializerChecker": term["terms"]["commercializer_checker"], + "commercializerCheckerData": term["terms"][ + "commercializer_checker_data" + ], + "commercialRevShare": term["terms"]["commercial_rev_share"], + "commercialRevCeiling": term["terms"]["commercial_rev_ceiling"], + "derivativesAllowed": term["terms"]["derivatives_allowed"], + "derivativesAttribution": term["terms"]["derivatives_attribution"], + "derivativesApproval": term["terms"]["derivatives_approval"], + "derivativesReciprocal": term["terms"]["derivatives_reciprocal"], + "derivativeRevCeiling": term["terms"]["derivative_rev_ceiling"], + "currency": term["terms"]["currency"], + "uri": term["terms"]["uri"], } camelcase_config = { - 'isSet': validated_licensing_config['is_set'], - 'mintingFee': validated_licensing_config['minting_fee'], - 'hookData': validated_licensing_config['hook_data'], - 'licensingHook': validated_licensing_config['licensing_hook'], - 'commercialRevShare': validated_licensing_config['commercial_rev_share'], - 'disabled': validated_licensing_config['disabled'], - 'expectMinimumGroupRewardShare': validated_licensing_config['expect_minimum_group_reward_share'], - 'expectGroupRewardPool': validated_licensing_config['expect_group_reward_pool'] + "isSet": validated_licensing_config["is_set"], + "mintingFee": validated_licensing_config["minting_fee"], + "hookData": validated_licensing_config["hook_data"], + "licensingHook": validated_licensing_config["licensing_hook"], + "commercialRevShare": validated_licensing_config[ + "commercial_rev_share" + ], + "disabled": validated_licensing_config["disabled"], + "expectMinimumGroupRewardShare": validated_licensing_config[ + "expect_minimum_group_reward_share" + ], + "expectGroupRewardPool": validated_licensing_config[ + "expect_group_reward_pool" + ], } - license_terms.append({ - 'terms': camelcase_term, - 'licensingConfig': camelcase_config - }) + license_terms.append( + {"terms": camelcase_term, "licensingConfig": camelcase_config} + ) metadata = { - 'ipMetadataURI': "", - 'ipMetadataHash': ZERO_HASH, - 'nftMetadataURI': "", - 'nftMetadataHash': ZERO_HASH, + "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), - }) + 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, @@ -443,19 +485,19 @@ def mint_and_register_ip_asset_with_pil_terms( metadata, license_terms, allow_duplicates, - tx_options=tx_options + tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) + ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"]) license_terms_ids = self._parse_tx_license_terms_attached_event( - response['tx_receipt'] + response["tx_receipt"] ) return { - 'tx_hash': response['tx_hash'], - 'ip_id': ip_registered['ip_id'], - 'license_terms_ids': license_terms_ids, - 'token_id': ip_registered['token_id'] + "tx_hash": response["tx_hash"], + "ip_id": ip_registered["ip_id"], + "license_terms_ids": license_terms_ids, + "token_id": ip_registered["token_id"], } except Exception as e: @@ -467,7 +509,7 @@ def mint_and_register_ip( recipient: str = None, ip_metadata: dict = None, allow_duplicates: bool = True, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Mint an NFT from a SPGNFT collection and register it with metadata as an IP. @@ -488,19 +530,25 @@ def mint_and_register_ip( """ try: metadata = { - 'ipMetadataURI': "", - 'ipMetadataHash': ZERO_HASH, - 'nftMetadataURI': "", - 'nftMetadataHash': ZERO_HASH, + "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), - }) + 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, @@ -510,24 +558,32 @@ def mint_and_register_ip( recipient if recipient else self.account.address, metadata, allow_duplicates, - tx_options=tx_options + tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) + ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"]) return { - 'tx_hash': response['tx_hash'], - 'ip_id': ip_registered['ip_id'], - 'token_id': ip_registered['token_id'] + "tx_hash": response["tx_hash"], + "ip_id": ip_registered["ip_id"], + "token_id": ip_registered["token_id"], } except Exception as e: raise ValueError(f"Failed to mint and register IP: {str(e)}") - def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, license_terms_data: dict, ip_metadata: dict = None, deadline: int = None, tx_options: dict = None) -> dict: + def register_ip_and_attach_pil_terms( + self, + nft_contract: str, + token_id: int, + license_terms_data: dict, + ip_metadata: dict = None, + deadline: int = None, + tx_options: dict = None, + ) -> dict: """ Register a given NFT as an IP and attach Programmable IP License Terms. - + :param nft_contract str: The address of the NFT collection. :param token_id int: The ID of the NFT. :param license_terms_data dict: The PIL terms and licensing configuration data to be attached to the IP. @@ -570,54 +626,63 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic 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.") + raise ValueError( + f"The NFT with id {token_id} is already registered as IP." + ) license_terms = [] for term in license_terms_data: validated_term = self.license_terms_util.validate_license_terms( - term['terms'] + term["terms"] ) validated_licensing_config = ( self.license_terms_util.validate_licensing_config( - term['licensing_config'] + term["licensing_config"] ) ) camelcase_term = { - 'transferable': term['terms']['transferable'], - 'royaltyPolicy': term['terms']['royalty_policy'], - 'defaultMintingFee': term['terms']['default_minting_fee'], - 'expiration': term['terms']['expiration'], - 'commercialUse': term['terms']['commercial_use'], - 'commercialAttribution': term['terms']['commercial_attribution'], - 'commercializerChecker': term['terms']['commercializer_checker'], - 'commercializerCheckerData': term['terms']['commercializer_checker_data'], - 'commercialRevShare': term['terms']['commercial_rev_share'], - 'commercialRevCeiling': term['terms']['commercial_rev_ceiling'], - 'derivativesAllowed': term['terms']['derivatives_allowed'], - 'derivativesAttribution': term['terms']['derivatives_attribution'], - 'derivativesApproval': term['terms']['derivatives_approval'], - 'derivativesReciprocal': term['terms']['derivatives_reciprocal'], - 'derivativeRevCeiling': term['terms']['derivative_rev_ceiling'], - 'currency': term['terms']['currency'], - 'uri': term['terms']['uri'] + "transferable": term["terms"]["transferable"], + "royaltyPolicy": term["terms"]["royalty_policy"], + "defaultMintingFee": term["terms"]["default_minting_fee"], + "expiration": term["terms"]["expiration"], + "commercialUse": term["terms"]["commercial_use"], + "commercialAttribution": term["terms"]["commercial_attribution"], + "commercializerChecker": term["terms"]["commercializer_checker"], + "commercializerCheckerData": term["terms"][ + "commercializer_checker_data" + ], + "commercialRevShare": term["terms"]["commercial_rev_share"], + "commercialRevCeiling": term["terms"]["commercial_rev_ceiling"], + "derivativesAllowed": term["terms"]["derivatives_allowed"], + "derivativesAttribution": term["terms"]["derivatives_attribution"], + "derivativesApproval": term["terms"]["derivatives_approval"], + "derivativesReciprocal": term["terms"]["derivatives_reciprocal"], + "derivativeRevCeiling": term["terms"]["derivative_rev_ceiling"], + "currency": term["terms"]["currency"], + "uri": term["terms"]["uri"], } camelcase_config = { - 'isSet': validated_licensing_config['is_set'], - 'mintingFee': validated_licensing_config['minting_fee'], - 'hookData': validated_licensing_config['hook_data'], - 'licensingHook': validated_licensing_config['licensing_hook'], - 'commercialRevShare': validated_licensing_config['commercial_rev_share'], - 'disabled': validated_licensing_config['disabled'], - 'expectMinimumGroupRewardShare': validated_licensing_config['expect_minimum_group_reward_share'], - 'expectGroupRewardPool': validated_licensing_config['expect_group_reward_pool'] + "isSet": validated_licensing_config["is_set"], + "mintingFee": validated_licensing_config["minting_fee"], + "hookData": validated_licensing_config["hook_data"], + "licensingHook": validated_licensing_config["licensing_hook"], + "commercialRevShare": validated_licensing_config[ + "commercial_rev_share" + ], + "disabled": validated_licensing_config["disabled"], + "expectMinimumGroupRewardShare": validated_licensing_config[ + "expect_minimum_group_reward_share" + ], + "expectGroupRewardPool": validated_licensing_config[ + "expect_group_reward_pool" + ], } - license_terms.append({ - 'terms': camelcase_term, - 'licensingConfig': camelcase_config - }) + license_terms.append( + {"terms": camelcase_term, "licensingConfig": camelcase_config} + ) calculated_deadline = self.sign_util.get_deadline(deadline=deadline) @@ -628,43 +693,49 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic state=self.web3.to_bytes(hexstr=ZERO_HASH), permissions=[ { - 'ipId': ip_id, - 'signer': self.license_attachment_workflows_client.contract.address, - 'to': self.core_metadata_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "setAll(address,string,bytes32,bytes32)" + "ipId": ip_id, + "signer": self.license_attachment_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": 1, # ALLOW + "func": "setAll(address,string,bytes32,bytes32)", }, { - 'ipId': ip_id, - 'signer': self.license_attachment_workflows_client.contract.address, - 'to': self.licensing_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "attachLicenseTerms(address,address,uint256)" + "ipId": ip_id, + "signer": self.license_attachment_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": 1, # ALLOW + "func": "attachLicenseTerms(address,address,uint256)", }, { - 'ipId': ip_id, - 'signer': self.license_attachment_workflows_client.contract.address, - 'to': self.licensing_module_client.contract.address, - 'permission': 1, # ALLOW - 'func': "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))" - } - ] + "ipId": ip_id, + "signer": self.license_attachment_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": 1, # ALLOW + "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", + }, + ], ) metadata = { - 'ipMetadataURI': "", - 'ipMetadataHash': ZERO_HASH, - 'nftMetadataURI': "", - 'nftMetadataHash': ZERO_HASH, + "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), - }) + 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, @@ -675,23 +746,27 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic metadata, license_terms, { - 'signer': self.web3.to_checksum_address(self.account.address), - 'deadline': calculated_deadline, - 'signature': self.web3.to_bytes(hexstr=signature_response['signature']) + "signer": self.web3.to_checksum_address(self.account.address), + "deadline": calculated_deadline, + "signature": self.web3.to_bytes( + hexstr=signature_response["signature"] + ), }, - tx_options=tx_options + tx_options=tx_options, ) - ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) - license_terms_ids = self._parse_tx_license_terms_attached_event(response['tx_receipt']) + ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"]) + license_terms_ids = self._parse_tx_license_terms_attached_event( + response["tx_receipt"] + ) return { - 'tx_hash': response['tx_hash'], - 'ip_id': ip_registered['ip_id'], - 'license_terms_ids': license_terms_ids, - 'token_id': ip_registered['token_id'] + "tx_hash": response["tx_hash"], + "ip_id": ip_registered["ip_id"], + "license_terms_ids": license_terms_ids, + "token_id": ip_registered["token_id"], } - + except Exception as e: raise e @@ -707,7 +782,7 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # """ # Register the given NFT as a derivative IP with metadata without using # license tokens. - + # :param nft_contract str: The address of the NFT collection. # :param token_id int: The ID of the NFT. # :param deriv_data dict: The derivative data for registerDerivative. @@ -728,14 +803,14 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # raise ValueError( # f"The NFT with id {token_id} is already registered as IP." # ) - + # if len(deriv_data['parentIpIds']) != len(deriv_data['licenseTermsIds']): # raise ValueError( # "Parent IP IDs and license terms IDs must match in quantity." # ) # if len(deriv_data['parentIpIds']) not in [1, 2]: # raise ValueError("There can only be 1 or 2 parent IP IDs.") - + # for parent_ip_id, license_terms_id in zip( # deriv_data['parentIpIds'], # deriv_data['licenseTermsIds'] @@ -750,7 +825,7 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # f"the parent ipId {parent_ip_id} before registering " # f"derivative." # ) - + # calculated_deadline = self._get_deadline(deadline=deadline) # sig_register_signature = self._get_signature( # ip_id, @@ -759,7 +834,7 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # "registerDerivative(address,address[],uint256[],address,bytes)", # 2 # ) - + # req_object = { # 'nftContract': nft_contract, # 'tokenId': token_id, @@ -788,14 +863,14 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # 'signature': ZERO_HASH, # }, # } - + # if metadata: # req_object['metadata'].update({ # 'metadataURI': metadata.get('metadataURI', ""), # 'metadataHash': metadata.get('metadataHash', ZERO_HASH), # 'nftMetadataHash': metadata.get('nftMetadataHash', ZERO_HASH), # }) - + # signature = self._get_signature( # ip_id, # self.core_metadata_module_client.contract.address, @@ -803,13 +878,13 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # "setAll(address,string,bytes32,bytes32)", # 1 # ) - + # req_object['sigMetadata'] = { # 'signer': self.web3.to_checksum_address(self.account.address), # 'deadline': calculated_deadline, # 'signature': signature, # } - + # response = build_and_send_transaction( # self.web3, # self.account, @@ -822,107 +897,122 @@ def register_ip_and_attach_pil_terms(self, nft_contract: str, token_id: int, lic # req_object['sigRegister'], # tx_options=tx_options # ) - + # ip_registered = self._parse_tx_ip_registered_event(response['tx_receipt']) - + # return { # 'tx_hash': response['tx_hash'], # 'ip_id': ip_registered['ip_id'] # } - + # except Exception as e: # raise e def _validate_max_rts(self, max_rts: int): """ Validates the maximum number of royalty tokens. - + :param max_rts int: The maximum number of royalty tokens :raises ValueError: If max_rts is invalid """ if not isinstance(max_rts, int): raise ValueError("The maxRts must be a number.") if max_rts < 0 or max_rts > 100_000_000: - raise ValueError("The maxRts must be greater than 0 and less than 100,000,000.") + raise ValueError( + "The maxRts must be greater than 0 and less than 100,000,000." + ) def _validate_derivative_data(self, derivative_data: dict) -> dict: """ Validates the derivative data and returns processed internal data. - + :param derivative_data dict: The derivative data to validate :return dict: The processed internal derivative data :raises ValueError: If validation fails """ internal_data = { - 'childIpId': derivative_data['childIpId'], - 'parentIpIds': derivative_data['parentIpIds'], - 'licenseTermsIds': [int(id) for id in derivative_data['licenseTermsIds']], - 'licenseTemplate': (derivative_data.get('licenseTemplate') - if derivative_data.get('licenseTemplate') is not None - else self.pi_license_template_client.contract.address), - 'royaltyContext': ZERO_ADDRESS, - 'maxMintingFee': int(derivative_data.get('maxMintingFee', 0)), - 'maxRts': int(derivative_data.get('maxRts', 0)), - 'maxRevenueShare': int(derivative_data.get('maxRevenueShare', 0)) + "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']: + if not internal_data["parentIpIds"]: raise ValueError("The parent IP IDs must be provided.") - - if not internal_data['licenseTermsIds']: + + 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: + + if len(internal_data["parentIpIds"]) != len(internal_data["licenseTermsIds"]): + raise ValueError( + "The number of parent IP IDs must match the number of license terms IDs." + ) + + if internal_data["maxMintingFee"] < 0: raise ValueError("The maxMintingFee must be greater than 0.") - - self._validate_max_rts(internal_data['maxRts']) - for parent_id, terms_id in zip(internal_data['parentIpIds'], internal_data['licenseTermsIds']): + self._validate_max_rts(internal_data["maxRts"]) + + for parent_id, terms_id in zip( + internal_data["parentIpIds"], internal_data["licenseTermsIds"] + ): if not self._is_registered(parent_id): - raise ValueError(f"The parent IP with id {parent_id} is not registered.") - + 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 + 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 + parent_id, internal_data["licenseTemplate"], terms_id ) - if internal_data['maxRevenueShare'] != 0 and royalty_percent > internal_data['maxRevenueShare']: + if ( + internal_data["maxRevenueShare"] != 0 + and royalty_percent > internal_data["maxRevenueShare"] + ): raise ValueError( f"The royalty percent for the parent IP with id {parent_id} is greater " f"than the maximum revenue share {internal_data['maxRevenueShare']}." ) - + return internal_data def _validate_license_token_ids(self, license_token_ids: list) -> list: """ Validates the license token IDs and checks ownership. - + :param license_token_ids list: The IDs of the license tokens to validate :return list: The validated and converted license token IDs :raises ValueError: If validation fails """ if not license_token_ids: raise ValueError("License token IDs must be provided.") - + # Convert all IDs to integers license_token_ids = [int(id) for id in license_token_ids] - + # Validate ownership of each token for token_id in license_token_ids: token_owner = self.license_token_client.ownerOf(token_id) if not token_owner or token_owner.lower() != self.account.address.lower(): - raise ValueError(f"License token id {token_id} must be owned by the caller.") - + raise ValueError( + f"License token id {token_id} must be owned by the caller." + ) + return license_token_ids def _get_ip_id(self, token_contract: str, token_id: int) -> str: @@ -934,9 +1024,7 @@ def _get_ip_id(self, token_contract: str, token_id: int) -> str: :return str: The IP ID. """ return self.ip_asset_registry_client.ipId( - self.chain_id, - token_contract, - token_id + self.chain_id, token_contract, token_id ) def _is_registered(self, ip_id: str) -> bool: @@ -958,14 +1046,14 @@ def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> int: event_signature = self.web3.keccak( text="IPRegistered(address,uint256,address,uint256,string,string,uint256)" ).hex() - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - ip_id = '0x' + log['data'].hex()[24:64] - token_id = int(log['topics'][3].hex(), 16) + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + ip_id = "0x" + log["data"].hex()[24:64] + token_id = int(log["topics"][3].hex(), 16) return { - 'ip_id': self.web3.to_checksum_address(ip_id), - 'token_id': token_id + "ip_id": self.web3.to_checksum_address(ip_id), + "token_id": token_id, } return None @@ -980,10 +1068,10 @@ def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int: text="LicenseTermsAttached(address,address,address,uint256)" ).hex() - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - data = log['data'] - license_terms_id = int.from_bytes(data[-32:], byteorder='big') + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + data = log["data"] + license_terms_id = int.from_bytes(data[-32:], byteorder="big") return license_terms_id return None @@ -999,10 +1087,10 @@ def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list: ).hex() license_terms_ids = [] - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - data = log['data'] - license_terms_id = int.from_bytes(data[-32:], byteorder='big') + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + data = log["data"] + license_terms_id = int.from_bytes(data[-32:], byteorder="big") license_terms_ids.append(license_terms_id) return license_terms_ids if license_terms_ids else None diff --git a/src/story_protocol_python_sdk/resources/License.py b/src/story_protocol_python_sdk/resources/License.py index 1090ed7..65869b9 100644 --- a/src/story_protocol_python_sdk/resources/License.py +++ b/src/story_protocol_python_sdk/resources/License.py @@ -2,16 +2,27 @@ from web3 import Web3 -from story_protocol_python_sdk.abi.PILicenseTemplate.PILicenseTemplate_client import PILicenseTemplateClient -from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import LicenseRegistryClient -from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import LicensingModuleClient -from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import IPAssetRegistryClient -from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ModuleRegistryClient +from story_protocol_python_sdk.abi.PILicenseTemplate.PILicenseTemplate_client import ( + PILicenseTemplateClient, +) +from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import ( + LicenseRegistryClient, +) +from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import ( + LicensingModuleClient, +) +from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( + IPAssetRegistryClient, +) +from story_protocol_python_sdk.abi.ModuleRegistry.ModuleRegistry_client import ( + ModuleRegistryClient, +) from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS + class License: """ A class to manage licenses on Story Protocol. @@ -20,6 +31,7 @@ class License: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -61,7 +73,7 @@ def register_pil_terms( derivative_rev_ceiling: int, currency: str, uri: str, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Registers new license terms and returns the ID of the newly registered license terms. @@ -88,43 +100,43 @@ def register_pil_terms( """ try: license_terms = { - 'transferable': transferable, - 'royaltyPolicy': royalty_policy, - 'defaultMintingFee': default_minting_fee, - 'expiration': expiration, - 'commercialUse': commercial_use, - 'commercialAttribution': commercial_attribution, - 'commercializerChecker': commercializer_checker, - 'commercializerCheckerData': commercializer_checker_data, - 'commercialRevShare': commercial_rev_share, - 'commercialRevCeiling': commercial_rev_ceiling, - 'derivativesAllowed': derivatives_allowed, - 'derivativesAttribution': derivatives_attribution, - 'derivativesApproval': derivatives_approval, - 'derivativesReciprocal': derivatives_reciprocal, - 'derivativeRevCeiling': derivative_rev_ceiling, - 'currency': currency, - 'uri': uri + "transferable": transferable, + "royaltyPolicy": royalty_policy, + "defaultMintingFee": default_minting_fee, + "expiration": expiration, + "commercialUse": commercial_use, + "commercialAttribution": commercial_attribution, + "commercializerChecker": commercializer_checker, + "commercializerCheckerData": commercializer_checker_data, + "commercialRevShare": commercial_rev_share, + "commercialRevCeiling": commercial_rev_ceiling, + "derivativesAllowed": derivatives_allowed, + "derivativesAttribution": derivatives_attribution, + "derivativesApproval": derivatives_approval, + "derivativesReciprocal": derivatives_reciprocal, + "derivativeRevCeiling": derivative_rev_ceiling, + "currency": currency, + "uri": uri, } license_terms_snake = { - 'transferable': transferable, - 'royalty_policy': royalty_policy, - 'default_minting_fee': default_minting_fee, - 'expiration': expiration, - 'commercial_use': commercial_use, - 'commercial_attribution': commercial_attribution, - 'commercializer_checker': commercializer_checker, - 'commercializer_checker_data': commercializer_checker_data, - 'commercial_rev_share': commercial_rev_share, - 'commercial_rev_ceiling': commercial_rev_ceiling, - 'derivatives_allowed': derivatives_allowed, - 'derivatives_attribution': derivatives_attribution, - 'derivatives_approval': derivatives_approval, - 'derivatives_reciprocal': derivatives_reciprocal, - 'derivative_rev_ceiling': derivative_rev_ceiling, - 'currency': currency, - 'uri': uri + "transferable": transferable, + "royalty_policy": royalty_policy, + "default_minting_fee": default_minting_fee, + "expiration": expiration, + "commercial_use": commercial_use, + "commercial_attribution": commercial_attribution, + "commercializer_checker": commercializer_checker, + "commercializer_checker_data": commercializer_checker_data, + "commercial_rev_share": commercial_rev_share, + "commercial_rev_ceiling": commercial_rev_ceiling, + "derivatives_allowed": derivatives_allowed, + "derivatives_attribution": derivatives_attribution, + "derivatives_approval": derivatives_approval, + "derivatives_reciprocal": derivatives_reciprocal, + "derivative_rev_ceiling": derivative_rev_ceiling, + "currency": currency, + "uri": uri, } # Validate the license terms @@ -132,21 +144,20 @@ def register_pil_terms( license_terms_id = self._get_license_terms_id(license_terms) if (license_terms_id is not None) and (license_terms_id != 0): - return {'license_terms_id': license_terms_id} + return {"license_terms_id": license_terms_id} response = build_and_send_transaction( self.web3, self.account, self.license_template_client.build_registerLicenseTerms_transaction, license_terms, - tx_options=tx_options + tx_options=tx_options, ) - target_logs = self._parse_tx_license_terms_registered_event(response['tx_receipt']) - return { - 'tx_hash': response['tx_hash'], - 'license_terms_id': target_logs - } + target_logs = self._parse_tx_license_terms_registered_event( + response["tx_receipt"] + ) + return {"tx_hash": response["tx_hash"], "license_terms_id": target_logs} except Exception as e: raise e @@ -160,35 +171,35 @@ def register_non_com_social_remixing_pil(self, tx_options: dict = None) -> dict: """ try: license_terms = self.license_terms_util.get_license_term_by_type( - self.license_terms_util.PIL_TYPE['NON_COMMERCIAL_REMIX']) + self.license_terms_util.PIL_TYPE["NON_COMMERCIAL_REMIX"] + ) license_terms_id = self._get_license_terms_id(license_terms) if (license_terms_id is not None) and (license_terms_id != 0): - return {'license_terms_id': license_terms_id} + return {"license_terms_id": license_terms_id} response = build_and_send_transaction( self.web3, self.account, self.license_template_client.build_registerLicenseTerms_transaction, license_terms, - tx_options=tx_options + tx_options=tx_options, ) - target_logs = self._parse_tx_license_terms_registered_event(response['tx_receipt']) - return { - 'tx_hash': response['tx_hash'], - 'license_terms_id': target_logs - } + target_logs = self._parse_tx_license_terms_registered_event( + response["tx_receipt"] + ) + return {"tx_hash": response["tx_hash"], "license_terms_id": target_logs} except Exception as e: raise e - + def register_commercial_use_pil( - self, - default_minting_fee: int, + self, + default_minting_fee: int, currency: str, - royalty_policy: str = None, - tx_options: dict = None + royalty_policy: str = None, + tx_options: dict = None, ) -> dict: """ Convenient function to register a PIL commercial use license to the registry. @@ -201,32 +212,33 @@ def register_commercial_use_pil( """ try: complete_license_terms = self.license_terms_util.get_license_term_by_type( - self.license_terms_util.PIL_TYPE['COMMERCIAL_USE'], { - 'defaultMintingFee': default_minting_fee, - 'currency': currency, - 'royaltyPolicyAddress': royalty_policy, - }) + self.license_terms_util.PIL_TYPE["COMMERCIAL_USE"], + { + "defaultMintingFee": default_minting_fee, + "currency": currency, + "royaltyPolicyAddress": royalty_policy, + }, + ) license_terms_id = self._get_license_terms_id(complete_license_terms) if (license_terms_id is not None) and (license_terms_id != 0): - return {'license_terms_id': license_terms_id} + return {"license_terms_id": license_terms_id} response = build_and_send_transaction( self.web3, self.account, self.license_template_client.build_registerLicenseTerms_transaction, complete_license_terms, - tx_options=tx_options + tx_options=tx_options, ) - if not response['tx_receipt'].logs: + if not response["tx_receipt"].logs: return None - target_logs = self._parse_tx_license_terms_registered_event(response['tx_receipt']) - return { - 'tx_hash': response['tx_hash'], - 'license_terms_id': target_logs - } + target_logs = self._parse_tx_license_terms_registered_event( + response["tx_receipt"] + ) + return {"tx_hash": response["tx_hash"], "license_terms_id": target_logs} except Exception as e: raise e @@ -237,7 +249,7 @@ def register_commercial_remix_pil( currency: str, commercial_rev_share: int, royalty_policy: str, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Convenient function to register a PIL commercial remix license to the registry. @@ -251,33 +263,34 @@ def register_commercial_remix_pil( """ try: complete_license_terms = self.license_terms_util.get_license_term_by_type( - self.license_terms_util.PIL_TYPE['COMMERCIAL_REMIX'], { - 'defaultMintingFee': default_minting_fee, - 'currency': currency, - 'commercialRevShare': commercial_rev_share, - 'royaltyPolicyAddress': royalty_policy, - }) + self.license_terms_util.PIL_TYPE["COMMERCIAL_REMIX"], + { + "defaultMintingFee": default_minting_fee, + "currency": currency, + "commercialRevShare": commercial_rev_share, + "royaltyPolicyAddress": royalty_policy, + }, + ) license_terms_id = self._get_license_terms_id(complete_license_terms) if license_terms_id and license_terms_id != 0: - return {'license_terms_id': license_terms_id} + return {"license_terms_id": license_terms_id} response = build_and_send_transaction( self.web3, self.account, self.license_template_client.build_registerLicenseTerms_transaction, complete_license_terms, - tx_options=tx_options + tx_options=tx_options, ) - if not response['tx_receipt'].logs: + if not response["tx_receipt"].logs: return None - target_logs = self._parse_tx_license_terms_registered_event(response['tx_receipt']) - return { - 'tx_hash': response['tx_hash'], - 'license_terms_id': target_logs - } + target_logs = self._parse_tx_license_terms_registered_event( + response["tx_receipt"] + ) + return {"tx_hash": response["tx_hash"], "license_terms_id": target_logs} except Exception as e: raise e @@ -289,20 +302,22 @@ def _parse_tx_license_terms_registered_event(self, tx_receipt: dict) -> int: :param tx_receipt dict: The transaction receipt. :return int: The ID of the license terms. """ - event_signature = self.web3.keccak(text="LicenseTermsRegistered(uint256,address,bytes)").hex() + event_signature = self.web3.keccak( + text="LicenseTermsRegistered(uint256,address,bytes)" + ).hex() - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - return int(log['topics'][1].hex(), 16) + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + return int(log["topics"][1].hex(), 16) return None - + def attach_license_terms( self, ip_id: str, license_template: str, license_terms_id: int, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Attaches license terms to an IP. @@ -321,15 +336,21 @@ def attach_license_terms( if not is_registered: raise ValueError(f"The IP with id {ip_id} is not registered.") - is_existed = self.license_registry_client.exists(license_template, license_terms_id) + is_existed = self.license_registry_client.exists( + license_template, license_terms_id + ) if not is_existed: raise ValueError(f"License terms id {license_terms_id} do not exist.") - is_attached_license_terms = self.license_registry_client.hasIpAttachedLicenseTerms( - ip_id, license_template, license_terms_id) + is_attached_license_terms = ( + self.license_registry_client.hasIpAttachedLicenseTerms( + ip_id, license_template, license_terms_id + ) + ) if is_attached_license_terms: raise ValueError( - f"License terms id {license_terms_id} is already attached to the IP with id {ip_id}.") + f"License terms id {license_terms_id} is already attached to the IP with id {ip_id}." + ) response = build_and_send_transaction( self.web3, @@ -338,14 +359,14 @@ def attach_license_terms( ip_id, license_template, license_terms_id, - tx_options=tx_options + tx_options=tx_options, ) - return {'tx_hash': response['tx_hash']} - + return {"tx_hash": response["tx_hash"]} + except Exception as e: raise e - + def mint_license_tokens( self, licensor_ip_id: str, @@ -355,7 +376,7 @@ def mint_license_tokens( receiver: str, max_minting_fee: int = 0, max_revenue_share: int = 0, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Mints license tokens for the license terms attached to an IP. @@ -373,23 +394,29 @@ def mint_license_tokens( try: if not self.web3.is_address(license_template): raise ValueError(f'Address "{license_template}" is invalid.') - + if not self.web3.is_address(receiver): raise ValueError(f'Address "{receiver}" is invalid.') is_registered = self.ip_asset_registry_client.isRegistered(licensor_ip_id) if not is_registered: - raise ValueError(f"The licensor IP with id {licensor_ip_id} is not registered.") + raise ValueError( + f"The licensor IP with id {licensor_ip_id} is not registered." + ) is_existed = self.license_template_client.exists(license_terms_id) if not is_existed: raise ValueError(f"License terms id {license_terms_id} do not exist.") - is_attached_license_terms = self.license_registry_client.hasIpAttachedLicenseTerms( - licensor_ip_id, license_template, license_terms_id) + is_attached_license_terms = ( + self.license_registry_client.hasIpAttachedLicenseTerms( + licensor_ip_id, license_template, license_terms_id + ) + ) if not is_attached_license_terms: raise ValueError( - f"License terms id {license_terms_id} is not attached to the IP with id {licensor_ip_id}.") + f"License terms id {license_terms_id} is not attached to the IP with id {licensor_ip_id}." + ) response = build_and_send_transaction( self.web3, @@ -403,15 +430,14 @@ def mint_license_tokens( ZERO_ADDRESS, # Zero address for royalty context max_minting_fee, self.license_terms_util.get_revenue_share(max_revenue_share), - tx_options=tx_options + tx_options=tx_options, ) - target_logs = self._parse_tx_license_tokens_minted_event(response['tx_receipt']) + target_logs = self._parse_tx_license_tokens_minted_event( + response["tx_receipt"] + ) - return { - 'tx_hash': response['tx_hash'], - 'license_token_ids': target_logs - } + return {"tx_hash": response["tx_hash"], "license_token_ids": target_logs} except Exception as e: raise e @@ -423,16 +449,18 @@ def _parse_tx_license_tokens_minted_event(self, tx_receipt: dict) -> list: :param tx_receipt dict: The transaction receipt. :return list: A list of license token IDs. """ - event_signature = self.web3.keccak(text="LicenseTokenMinted(address,address,uint256)").hex() + event_signature = self.web3.keccak( + text="LicenseTokenMinted(address,address,uint256)" + ).hex() token_ids = [] - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - start_license_token_id = int(log['topics'][3].hex(), 16) + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + start_license_token_id = int(log["topics"][3].hex(), 16) token_ids.append(start_license_token_id) return token_ids if token_ids else None - + def get_license_terms(self, selected_license_terms_id: int) -> dict: """ Gets License Terms of the given ID. @@ -441,7 +469,9 @@ def get_license_terms(self, selected_license_terms_id: int) -> dict: :return dict: An object containing all of the selected license terms. """ try: - return self.license_template_client.getLicenseTerms(selected_license_terms_id) + return self.license_template_client.getLicenseTerms( + selected_license_terms_id + ) except Exception as e: raise ValueError(f"Failed to get license terms: {str(e)}") @@ -452,7 +482,7 @@ def predict_minting_license_fee( amount: int, license_template: str = None, receiver: str = None, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Pre-compute the minting license fee for the given IP and license terms. @@ -468,7 +498,9 @@ def predict_minting_license_fee( try: # Check if IP is registered if not self.ip_asset_registry_client.isRegistered(licensor_ip_id): - raise ValueError(f"The licensor IP with id {licensor_ip_id} is not registered.") + raise ValueError( + f"The licensor IP with id {licensor_ip_id} is not registered." + ) # Check if license terms exist if not self.license_template_client.exists(license_terms_id): @@ -491,13 +523,10 @@ def predict_minting_license_fee( license_terms_id, amount, receiver, - ZERO_ADDRESS # Zero address for royalty context + ZERO_ADDRESS, # Zero address for royalty context ) - return { - 'currency': response[0], - 'amount': response[1] - } + return {"currency": response[0], "amount": response[1]} except Exception as e: raise ValueError(f"Failed to predict minting license fee: {str(e)}") @@ -508,7 +537,7 @@ def set_licensing_config( license_terms_id: int, licensing_config: dict, license_template: str = None, - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Sets the licensing configuration for a specific license terms of an IP. If both licenseTemplate and licenseTermsId are not specified then the licensing config apply to all licenses of given IP. @@ -531,14 +560,14 @@ def set_licensing_config( try: # Input validation required_params = { - 'isSet', - 'mintingFee', - 'hookData', - 'licensingHook', - 'commercialRevShare', - 'disabled', - 'expectMinimumGroupRewardShare', - 'expectGroupRewardPool' + "isSet", + "mintingFee", + "hookData", + "licensingHook", + "commercialRevShare", + "disabled", + "expectMinimumGroupRewardShare", + "expectGroupRewardPool", } # Check for missing parameters @@ -546,29 +575,39 @@ def set_licensing_config( if missing_params: raise ValueError( f"Missing required licensing_config parameters: {', '.join(missing_params)}. " - f"All parameters must be explicitly provided.") - - licensing_config['commercialRevShare'] = self.license_terms_util.get_revenue_share( - licensing_config['commercialRevShare']) + f"All parameters must be explicitly provided." + ) + + licensing_config["commercialRevShare"] = ( + self.license_terms_util.get_revenue_share( + licensing_config["commercialRevShare"] + ) + ) - if licensing_config['mintingFee'] < 0: + if licensing_config["mintingFee"] < 0: raise ValueError("The minting fee must be greater than 0.") if not license_template: license_template = ZERO_ADDRESS - - if license_template == ZERO_ADDRESS and licensing_config['commercialRevShare'] != 0: + + if ( + license_template == ZERO_ADDRESS + and licensing_config["commercialRevShare"] != 0 + ): raise ValueError( - "The license template cannot be zero address if commercial revenue share is not zero.") + "The license template cannot be zero address if commercial revenue share is not zero." + ) # Convert addresses to checksum format ip_id = self.web3.to_checksum_address(ip_id) if license_template: license_template = self.web3.to_checksum_address(license_template) - licensing_config['licensingHook'] = self.web3.to_checksum_address( - licensing_config['licensingHook']) - licensing_config['expectGroupRewardPool'] = self.web3.to_checksum_address( - licensing_config['expectGroupRewardPool']) + licensing_config["licensingHook"] = self.web3.to_checksum_address( + licensing_config["licensingHook"] + ) + licensing_config["expectGroupRewardPool"] = self.web3.to_checksum_address( + licensing_config["expectGroupRewardPool"] + ) # Check if IP is registered if not self.ip_asset_registry_client.isRegistered(ip_id): @@ -579,13 +618,17 @@ def set_licensing_config( raise ValueError(f"License terms id {license_terms_id} does not exist.") # Check if licensing hook is registered if provided - if licensing_config['licensingHook'] != ZERO_ADDRESS: - if not self.module_registry_client.isRegistered(licensing_config['licensingHook']): + if licensing_config["licensingHook"] != ZERO_ADDRESS: + if not self.module_registry_client.isRegistered( + licensing_config["licensingHook"] + ): raise ValueError("The licensing hook is not registered.") if license_template == ZERO_ADDRESS and license_terms_id != 0: - raise ValueError("The license template is zero address but license terms id is not zero.") - + raise ValueError( + "The license template is zero address but license terms id is not zero." + ) + response = build_and_send_transaction( self.web3, self.account, @@ -594,12 +637,12 @@ def set_licensing_config( license_template, license_terms_id, licensing_config, - tx_options=tx_options + tx_options=tx_options, ) return { - 'tx_hash': response['tx_hash'], - 'success': True if response.get('tx_receipt') else None + "tx_hash": response["tx_hash"], + "success": True if response.get("tx_receipt") else None, } except Exception as e: diff --git a/src/story_protocol_python_sdk/resources/NFTClient.py b/src/story_protocol_python_sdk/resources/NFTClient.py index 5023129..05d064d 100644 --- a/src/story_protocol_python_sdk/resources/NFTClient.py +++ b/src/story_protocol_python_sdk/resources/NFTClient.py @@ -1,13 +1,16 @@ -#src/story_protcol_python_sdk/resources/NFTClient.py +# src/story_protcol_python_sdk/resources/NFTClient.py from web3 import Web3 -from story_protocol_python_sdk.abi.RegistrationWorkflows.RegistrationWorkflows_client import RegistrationWorkflowsClient +from story_protocol_python_sdk.abi.RegistrationWorkflows.RegistrationWorkflows_client import ( + RegistrationWorkflowsClient, +) from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH + class NFTClient: """ NFTClient handles the creation of SPG NFT collections. @@ -15,7 +18,8 @@ class NFTClient: :param web3 Web3: An instance of Web3. :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. - """ + """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -23,10 +27,21 @@ def __init__(self, web3: Web3, account, chain_id: int): self.registration_workflows_client = RegistrationWorkflowsClient(web3) - def create_nft_collection(self, name: str, symbol: str, is_public_minting: bool, mint_open: bool, - mint_fee_recipient: str, contract_uri: str, base_uri: str = "", max_supply: int = None, - mint_fee: int = None, mint_fee_token: str = None, owner: str = None, - tx_options: dict = None) -> dict: + def create_nft_collection( + self, + name: str, + symbol: str, + is_public_minting: bool, + mint_open: bool, + mint_fee_recipient: str, + contract_uri: str, + base_uri: str = "", + max_supply: int = None, + mint_fee: int = None, + mint_fee_token: str = None, + owner: str = None, + tx_options: dict = None, + ) -> dict: """ Creates a new SPG NFT Collection. @@ -45,21 +60,27 @@ def create_nft_collection(self, name: str, symbol: str, is_public_minting: bool, :return dict: A dictionary with the transaction hash and collection address. """ try: - if mint_fee is not None and (mint_fee < 0 or not self.web3.is_address(mint_fee_token or "")): - raise ValueError("Invalid mint fee token address, mint fee is greater than 0.") + if mint_fee is not None and ( + mint_fee < 0 or not self.web3.is_address(mint_fee_token or "") + ): + raise ValueError( + "Invalid mint fee token address, mint fee is greater than 0." + ) spg_nft_init_params = { - 'name': name, - 'symbol': symbol, - 'baseURI': base_uri or "", - 'maxSupply': max_supply if max_supply is not None else 2**32 - 1, - 'mintFee': mint_fee if mint_fee is not None else 0, - 'mintFeeToken': mint_fee_token if mint_fee_token is not None else ZERO_ADDRESS, - 'owner': owner if owner else self.account.address, - 'mintFeeRecipient': self.web3.to_checksum_address(mint_fee_recipient), - 'mintOpen': mint_open, - 'isPublicMinting': is_public_minting, - 'contractURI': contract_uri + "name": name, + "symbol": symbol, + "baseURI": base_uri or "", + "maxSupply": max_supply if max_supply is not None else 2**32 - 1, + "mintFee": mint_fee if mint_fee is not None else 0, + "mintFeeToken": ( + mint_fee_token if mint_fee_token is not None else ZERO_ADDRESS + ), + "owner": owner if owner else self.account.address, + "mintFeeRecipient": self.web3.to_checksum_address(mint_fee_recipient), + "mintOpen": mint_open, + "isPublicMinting": is_public_minting, + "contractURI": contract_uri, } response = build_and_send_transaction( @@ -67,19 +88,18 @@ def create_nft_collection(self, name: str, symbol: str, is_public_minting: bool, self.account, self.registration_workflows_client.build_createCollection_transaction, spg_nft_init_params, - tx_options=tx_options + tx_options=tx_options, ) - collection_address = self._parse_tx_collection_created_event(response['tx_receipt']) + collection_address = self._parse_tx_collection_created_event( + response["tx_receipt"] + ) - return { - 'tx_hash': response['tx_hash'], - 'nft_contract': collection_address - } + return {"tx_hash": response["tx_hash"], "nft_contract": collection_address} except Exception as e: raise e - + def _parse_tx_collection_created_event(self, tx_receipt: dict) -> int: """ Parse the CollectionCreated event from a transaction receipt. @@ -89,38 +109,40 @@ def _parse_tx_collection_created_event(self, tx_receipt: dict) -> int: """ event_signature = self.web3.keccak(text="CollectionCreated(address)").hex() - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: - return self.web3.to_checksum_address('0x' + log['topics'][1].hex()[-40:]) + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: + return self.web3.to_checksum_address( + "0x" + log["topics"][1].hex()[-40:] + ) return None - + def get_mint_fee_token(self, nft_contract: str) -> str: """ Returns the current mint fee token of the collection. - + :param nft_contract str: The address of the NFT contract. :return str: The address of the mint fee token. """ try: nft_contract = self.web3.to_checksum_address(nft_contract) spg_nft_client = SPGNFTImplClient(self.web3, contract_address=nft_contract) - + return spg_nft_client.mintFeeToken() except Exception as e: raise ValueError(f"Failed to get mint fee token: {str(e)}") - + def get_mint_fee(self, nft_contract: str) -> int: """ Returns the current mint fee of the collection. - + :param nft_contract str: The address of the NFT contract. :return int: The mint fee amount. """ try: nft_contract = self.web3.to_checksum_address(nft_contract) spg_nft_client = SPGNFTImplClient(self.web3, contract_address=nft_contract) - + return spg_nft_client.mintFee() except Exception as e: raise ValueError(f"Failed to get mint fee: {str(e)}") diff --git a/src/story_protocol_python_sdk/resources/Permission.py b/src/story_protocol_python_sdk/resources/Permission.py index 125be36..bec23be 100644 --- a/src/story_protocol_python_sdk/resources/Permission.py +++ b/src/story_protocol_python_sdk/resources/Permission.py @@ -3,15 +3,22 @@ from web3 import Web3 import os, json -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.AccessController.AccessController_client import AccessControllerClient +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.AccessController.AccessController_client import ( + AccessControllerClient, +) from story_protocol_python_sdk.resources.IPAccount import IPAccount from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction from story_protocol_python_sdk.utils.validation import validate_address from story_protocol_python_sdk.utils.constants import DEFAULT_FUNCTION_SELECTOR from story_protocol_python_sdk.utils.sign import Sign + class Permission: """ A class to manage permissions for IP accounts. @@ -20,6 +27,7 @@ class Permission: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -29,8 +37,16 @@ def __init__(self, web3: Web3, account, chain_id: int): self.ip_account = IPAccount(web3, account, chain_id) self.access_controller_client = AccessControllerClient(web3) self.sign_util = Sign(web3, self.chain_id, self.account) - - def set_permission(self, ip_id: str, signer: str, to: str, permission: int, func: str = DEFAULT_FUNCTION_SELECTOR, tx_options: dict = None) -> dict: + + def set_permission( + self, + ip_id: str, + signer: str, + to: str, + permission: int, + func: str = DEFAULT_FUNCTION_SELECTOR, + tx_options: dict = None, + ) -> dict: """ Sets the permission for a specific function call. Each policy is represented as a mapping from an IP account address to a signer address to a recipient @@ -49,36 +65,36 @@ def set_permission(self, ip_id: str, signer: str, to: str, permission: int, func try: validate_address(signer) validate_address(to) - + self._check_is_registered(ip_id) - + data = self.access_controller_client.contract.encode_abi( abi_element_identifier="setPermission", args=[ self.web3.to_checksum_address(ip_id), self.web3.to_checksum_address(signer), self.web3.to_checksum_address(to), - Web3.keccak(text=func)[:4] if func else b'\x00\x00\x00\x00', - permission - ] + Web3.keccak(text=func)[:4] if func else b"\x00\x00\x00\x00", + permission, + ], ) - + response = self.ip_account.execute( to=self.access_controller_client.contract.address, value=0, ip_id=ip_id, data=data, - tx_options=tx_options + tx_options=tx_options, ) - - return { - 'tx_hash': response['tx_hash'] - } + + return {"tx_hash": response["tx_hash"]} except Exception as e: raise Exception(f"Failed to set permission for IP {ip_id}: {str(e)}") - def set_all_permissions(self, ip_id: str, signer: str, permission: int, tx_options: dict = None) -> dict: + def set_all_permissions( + self, ip_id: str, signer: str, permission: int, tx_options: dict = None + ) -> dict: """ Sets permission to a signer for all functions across all modules. @@ -90,36 +106,43 @@ def set_all_permissions(self, ip_id: str, signer: str, permission: int, tx_optio """ try: validate_address(signer) - + self._check_is_registered(ip_id) - + data = self.access_controller_client.contract.encode_abi( abi_element_identifier="setAllPermissions", args=[ self.web3.to_checksum_address(ip_id), self.web3.to_checksum_address(signer), - permission - ] + permission, + ], ) - + response = self.ip_account.execute( to=self.access_controller_client.contract.address, value=0, ip_id=ip_id, data=data, - tx_options=tx_options + tx_options=tx_options, ) - - return { - 'tx_hash': response['tx_hash'] - } + + return {"tx_hash": response["tx_hash"]} except Exception as e: - raise Exception(f"Failed to set all permissions for IP {ip_id} and signer {signer}: {str(e)}") + raise Exception( + f"Failed to set all permissions for IP {ip_id} and signer {signer}: {str(e)}" + ) - def create_set_permission_signature(self, ip_id: str, signer: str, to: str, permission: int, - func: str = DEFAULT_FUNCTION_SELECTOR, deadline: int = None, - tx_options: dict = None) -> dict: + def create_set_permission_signature( + self, + ip_id: str, + signer: str, + to: str, + permission: int, + func: str = DEFAULT_FUNCTION_SELECTOR, + deadline: int = None, + tx_options: dict = None, + ) -> dict: """ Specific permission overrides wildcard permission with signature. @@ -135,49 +158,51 @@ def create_set_permission_signature(self, ip_id: str, signer: str, to: str, perm try: validate_address(signer) validate_address(to) - + self._check_is_registered(ip_id) - + ip_account_client = IPAccountImplClient(self.web3, contract_address=ip_id) - + # Convert addresses to checksum format ip_id = self.web3.to_checksum_address(ip_id) signer = self.web3.to_checksum_address(signer) to = self.web3.to_checksum_address(to) - + data = self.access_controller_client.contract.encode_abi( abi_element_identifier="setTransientPermission", args=[ ip_id, signer, to, - Web3.keccak(text=func)[:4] if func else b'\x00\x00\x00\x00', - permission - ] + Web3.keccak(text=func)[:4] if func else b"\x00\x00\x00\x00", + permission, + ], ) - + # Get state and calculate deadline state = ip_account_client.state() - block_timestamp = self.web3.eth.get_block('latest').timestamp + block_timestamp = self.web3.eth.get_block("latest").timestamp calculated_deadline = self.sign_util.get_deadline(deadline) - + # Get permission signature signature_response = self.sign_util.get_permission_signature( ip_id=ip_id, deadline=calculated_deadline, state=state, - permissions=[{ - 'ipId': ip_id, - 'signer': signer, - 'to': to, - 'permission': permission, - 'func': func - }] + permissions=[ + { + "ipId": ip_id, + "signer": signer, + "to": to, + "permission": permission, + "func": func, + } + ], ) - + # Extract the signature string from the response signature_hex = signature_response["signature"] - + # Create and sign the transaction response = self.ip_account.execute_with_sig( to=self.access_controller_client.contract.address, @@ -187,15 +212,15 @@ def create_set_permission_signature(self, ip_id: str, signer: str, to: str, perm signer=signer, deadline=calculated_deadline, signature=self.web3.to_bytes(hexstr=signature_hex), - tx_options=tx_options + tx_options=tx_options, ) - - return { - 'tx_hash': response['tx_hash'] - } + + return {"tx_hash": response["tx_hash"]} except Exception as e: - raise Exception(f"Failed to create permission signature for IP {ip_id}, signer {signer}, to {to}: {str(e)}") + raise Exception( + f"Failed to create permission signature for IP {ip_id}, signer {signer}, to {to}: {str(e)}" + ) def _check_is_registered(self, ip_id: str) -> None: """ diff --git a/src/story_protocol_python_sdk/resources/Royalty.py b/src/story_protocol_python_sdk/resources/Royalty.py index aa52d4f..a09f302 100644 --- a/src/story_protocol_python_sdk/resources/Royalty.py +++ b/src/story_protocol_python_sdk/resources/Royalty.py @@ -1,18 +1,33 @@ -#src/story_protcol_python_sdk/resources/Royalty.py +# src/story_protcol_python_sdk/resources/Royalty.py from web3 import Web3 -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.RoyaltyPolicyLAP.RoyaltyPolicyLAP_client import RoyaltyPolicyLAPClient -from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import RoyaltyModuleClient -from story_protocol_python_sdk.abi.RoyaltyWorkflows.RoyaltyWorkflows_client import RoyaltyWorkflowsClient -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.RoyaltyPolicyLAP.RoyaltyPolicyLAP_client import ( + RoyaltyPolicyLAPClient, +) +from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( + RoyaltyModuleClient, +) +from story_protocol_python_sdk.abi.RoyaltyWorkflows.RoyaltyWorkflows_client import ( + RoyaltyWorkflowsClient, +) +from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( + IPAccountImplClient, +) from story_protocol_python_sdk.abi.MockERC20.MockERC20_client import MockERC20Client -from story_protocol_python_sdk.abi.RoyaltyPolicyLRP.RoyaltyPolicyLRP_client import RoyaltyPolicyLRPClient +from story_protocol_python_sdk.abi.RoyaltyPolicyLRP.RoyaltyPolicyLRP_client import ( + RoyaltyPolicyLRPClient, +) from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction + class Royalty: """ A class to claim and pay royalties on Story Protocol. @@ -21,6 +36,7 @@ class Royalty: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -48,7 +64,9 @@ def get_royalty_vault_address(self, ip_id: str) -> str: return self.royalty_module_client.ipRoyaltyVaults(ip_id) - def claimable_revenue(self, royalty_vault_ip_id: str, claimer: str, token: str) -> int: + def claimable_revenue( + self, royalty_vault_ip_id: str, claimer: str, token: str + ) -> int: """ Calculates the amount of revenue token claimable by a token holder. @@ -59,19 +77,27 @@ def claimable_revenue(self, royalty_vault_ip_id: str, claimer: str, token: str) """ try: proxy_address = self.get_royalty_vault_address(royalty_vault_ip_id) - ip_royalty_vault_client = IpRoyaltyVaultImplClient(self.web3, contract_address=proxy_address) + ip_royalty_vault_client = IpRoyaltyVaultImplClient( + self.web3, contract_address=proxy_address + ) claimable_revenue = ip_royalty_vault_client.claimableRevenue( - claimer=claimer, - token=token + claimer=claimer, token=token ) return claimable_revenue except Exception as e: raise e - - def pay_royalty_on_behalf(self, receiver_ip_id: str, payer_ip_id: str, token: str, amount: int, tx_options: dict = None) -> dict: + + def pay_royalty_on_behalf( + self, + receiver_ip_id: str, + payer_ip_id: str, + token: str, + amount: int, + tx_options: dict = None, + ) -> dict: """ Allows the function caller to pay royalties to the receiver IP asset on behalf of the payer IP asset. @@ -83,14 +109,22 @@ def pay_royalty_on_behalf(self, receiver_ip_id: str, payer_ip_id: str, token: st :return dict: A dictionary with the transaction hash. """ try: - is_receiver_registered = self.ip_asset_registry_client.isRegistered(receiver_ip_id) + is_receiver_registered = self.ip_asset_registry_client.isRegistered( + receiver_ip_id + ) if not is_receiver_registered: - raise ValueError(f"The receiver IP with id {receiver_ip_id} is not registered.") + raise ValueError( + f"The receiver IP with id {receiver_ip_id} is not registered." + ) - is_payer_registered = self.ip_asset_registry_client.isRegistered(payer_ip_id) + is_payer_registered = self.ip_asset_registry_client.isRegistered( + payer_ip_id + ) if not is_payer_registered: - raise ValueError(f"The payer IP with id {payer_ip_id} is not registered.") - + raise ValueError( + f"The payer IP with id {payer_ip_id} is not registered." + ) + response = build_and_send_transaction( self.web3, self.account, @@ -99,15 +133,24 @@ def pay_royalty_on_behalf(self, receiver_ip_id: str, payer_ip_id: str, token: st payer_ip_id, token, amount, - tx_options=tx_options + tx_options=tx_options, ) - return {'tx_hash': response['tx_hash']} - + return {"tx_hash": response["tx_hash"]} + except Exception as e: raise e - - def claim_all_revenue(self, ancestor_ip_id: str, claimer: str, child_ip_ids: list, royalty_policies: list, currency_tokens: list, claim_options: dict = None, tx_options: dict = None) -> dict: + + def claim_all_revenue( + self, + ancestor_ip_id: str, + claimer: str, + child_ip_ids: list, + royalty_policies: list, + currency_tokens: list, + claim_options: dict = None, + tx_options: dict = None, + ) -> dict: """ Claims all revenue from the child IPs of an ancestor IP, then optionally transfers and unwraps tokens. @@ -125,7 +168,7 @@ def claim_all_revenue(self, ancestor_ip_id: str, claimer: str, child_ip_ids: lis if not self.web3.is_address(ancestor_ip_id): raise ValueError("Invalid ancestor IP ID address") if not self.web3.is_address(claimer): - raise ValueError("Invalid claimer address") + raise ValueError("Invalid claimer address") if not all(self.web3.is_address(addr) for addr in child_ip_ids): raise ValueError("Invalid child IP ID address") if not all(self.web3.is_address(addr) for addr in royalty_policies): @@ -139,56 +182,57 @@ def claim_all_revenue(self, ancestor_ip_id: str, claimer: str, child_ip_ids: lis self.account, self.royalty_workflows_client.build_claimAllRevenue_transaction, ancestor_ip_id, - claimer, + claimer, child_ip_ids, royalty_policies, currency_tokens, - tx_options=tx_options + tx_options=tx_options, ) - tx_hashes = [response['tx_hash']] - + tx_hashes = [response["tx_hash"]] + # Determine if the claimer is an IP owned by the wallet. owns_claimer, is_claimer_ip, ip_account = self._get_claimer_info(claimer) # If wallet does not own the claimer then we cannot auto claim. # If owns_claimer is false, it means the claimer is neither an IP owned by the wallet nor the wallet address itself. if not owns_claimer: - return { - 'receipt': response['tx_receipt'], - 'tx_hashes': tx_hashes - } - - claimed_tokens = self._parse_tx_revenue_token_claimed_event(response['tx_receipt']) - - auto_transfer = claim_options.get('auto_transfer_all_claimed_tokens_from_ip', True) if claim_options else True + return {"receipt": response["tx_receipt"], "tx_hashes": tx_hashes} + + claimed_tokens = self._parse_tx_revenue_token_claimed_event( + response["tx_receipt"] + ) + + auto_transfer = ( + claim_options.get("auto_transfer_all_claimed_tokens_from_ip", True) + if claim_options + else True + ) # auto_unwrap = claim_options['auto_unwrap_ip_tokens'] # transfer claimed tokens from IP to wallet if wallet owns IP if auto_transfer and is_claimer_ip and owns_claimer: hashes = self._transfer_claimed_tokens_from_ip_to_wallet( - ancestor_ip_id, - ip_account, - claimed_tokens + ancestor_ip_id, ip_account, claimed_tokens ) tx_hashes.extend(hashes) return { - 'receipt': response['tx_receipt'], - 'claimed_tokens': claimed_tokens, - 'tx_hashes': tx_hashes + "receipt": response["tx_receipt"], + "claimed_tokens": claimed_tokens, + "tx_hashes": tx_hashes, } except Exception as e: raise ValueError(f"Failed to claim all revenue: {str(e)}") - + def transfer_to_vault( self, ip_id: str, ancestor_ip_id: str, token: str, royalty_policy: str = "LAP", - tx_options: dict = None + tx_options: dict = None, ) -> dict: """ Transfers to vault an amount of revenue tokens claimable via a royalty policy. @@ -203,7 +247,7 @@ def transfer_to_vault( try: if not self.web3.is_address(token): raise ValueError(f'Token address "{token}" is invalid.') - + # Determine which royalty policy to use if royalty_policy == "LAP": royalty_policy_client = self.royalty_policy_lap_client @@ -212,9 +256,15 @@ def transfer_to_vault( else: # If it's a custom address if not self.web3.is_address(royalty_policy): - raise ValueError(f'Royalty policy address "{royalty_policy}" is invalid.') - royalty_policy_client = self.royalty_policy_lap_client # Same ABI for all royalty policies - royalty_policy_client.contract.address = self.web3.to_checksum_address(royalty_policy) + raise ValueError( + f'Royalty policy address "{royalty_policy}" is invalid.' + ) + royalty_policy_client = ( + self.royalty_policy_lap_client + ) # Same ABI for all royalty policies + royalty_policy_client.contract.address = self.web3.to_checksum_address( + royalty_policy + ) response = build_and_send_transaction( self.web3, @@ -223,13 +273,10 @@ def transfer_to_vault( ip_id, ancestor_ip_id, token, - tx_options=tx_options + tx_options=tx_options, ) - return { - 'tx_hash': response['tx_hash'], - 'receipt': response['tx_receipt'] - } + return {"tx_hash": response["tx_hash"], "receipt": response["tx_receipt"]} except Exception as e: raise ValueError(f"Failed to transfer to vault: {str(e)}") @@ -237,7 +284,7 @@ def transfer_to_vault( def _get_claimer_info(self, claimer): """ Get information about the claimer address. - + :param claimer str: The claimer address to check :return dict: Dictionary containing: - owns_claimer (bool): Whether the wallet owns the claimer @@ -254,8 +301,10 @@ def _get_claimer_info(self, claimer): owns_claimer = ip_owner == self.account.address return owns_claimer, is_claimer_ip, ip_account - - def _transfer_claimed_tokens_from_ip_to_wallet(self, ancestor_ip_id: str, ip_account, claimed_tokens: list) -> list: + + def _transfer_claimed_tokens_from_ip_to_wallet( + self, ancestor_ip_id: str, ip_account, claimed_tokens: list + ) -> list: """ Transfer claimed tokens from an IP account to the wallet. @@ -267,16 +316,15 @@ def _transfer_claimed_tokens_from_ip_to_wallet(self, ancestor_ip_id: str, ip_acc tx_hashes = [] for claimed_token in claimed_tokens: - token = claimed_token['token'] - amount = claimed_token['amount'] + token = claimed_token["token"] + amount = claimed_token["amount"] if amount <= 0: continue # Build ERC20 transfer function data transfer_data = self.mock_erc20_client.contract.encode_abi( - abi_element_identifier="transfer", - args=[self.account.address, amount] + abi_element_identifier="transfer", args=[self.account.address, amount] ) # Execute transfer through IP account - use build_and_send_transaction to properly sign with account @@ -287,9 +335,9 @@ def _transfer_claimed_tokens_from_ip_to_wallet(self, ancestor_ip_id: str, ip_acc self.web3.to_checksum_address(token), 0, transfer_data, - 0 + 0, ) - tx_hashes.append(response['tx_hash']) + tx_hashes.append(response["tx_hash"]) return tx_hashes @@ -300,25 +348,37 @@ def _parse_tx_revenue_token_claimed_event(self, tx_receipt: dict) -> list: :param tx_receipt dict: The transaction receipt. :return list: List of claimed tokens with claimer address, token address and amount. """ - event_signature = self.web3.keccak(text="RevenueTokenClaimed(address,address,uint256)").hex() + event_signature = self.web3.keccak( + text="RevenueTokenClaimed(address,address,uint256)" + ).hex() claimed_tokens = [] - - for log in tx_receipt.get('logs', []): - if log['topics'][0].hex() == event_signature: - data = log['data'] - + + for log in tx_receipt.get("logs", []): + if log["topics"][0].hex() == event_signature: + data = log["data"] + # Convert HexBytes to hex string without '0x' prefix - data_hex = data.hex() if hasattr(data, 'hex') else data[2:] if isinstance(data, str) and data.startswith('0x') else data - + data_hex = ( + data.hex() + if hasattr(data, "hex") + else ( + data[2:] + if isinstance(data, str) and data.startswith("0x") + else data + ) + ) + # Each parameter is 32 bytes (64 hex chars) - claimer = "0x" + data_hex[24:64] # First 20 bytes of the first parameter - token = "0x" + data_hex[88:128] # First 20 bytes of the second parameter + claimer = ( + "0x" + data_hex[24:64] + ) # First 20 bytes of the first parameter + token = ( + "0x" + data_hex[88:128] + ) # First 20 bytes of the second parameter amount = int(data_hex[128:], 16) # Third parameter - - claimed_tokens.append({ - 'claimer': claimer, - 'token': token, - 'amount': amount - }) + + claimed_tokens.append( + {"claimer": claimer, "token": token, "amount": amount} + ) return claimed_tokens diff --git a/src/story_protocol_python_sdk/resources/WIP.py b/src/story_protocol_python_sdk/resources/WIP.py index 94319eb..f6d627b 100644 --- a/src/story_protocol_python_sdk/resources/WIP.py +++ b/src/story_protocol_python_sdk/resources/WIP.py @@ -5,6 +5,7 @@ from story_protocol_python_sdk.abi.WIP.WIP_client import WIPClient from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction + class WIP: """ A class to manage Wrapped IP (WIP) token operations. @@ -13,6 +14,7 @@ class WIP: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3: Web3, account, chain_id: int): self.web3 = web3 self.account = account @@ -35,20 +37,22 @@ def deposit(self, amount: int, tx_options: dict = None) -> dict: # Prepare transaction options transaction_options = tx_options or {} - transaction_options.update({ - 'from': self.account.address, - 'nonce': self.web3.eth.get_transaction_count(self.account.address), - 'value': amount - }) - + transaction_options.update( + { + "from": self.account.address, + "nonce": self.web3.eth.get_transaction_count(self.account.address), + "value": amount, + } + ) + response = build_and_send_transaction( self.web3, self.account, self.wip_client.build_deposit_transaction, - tx_options=transaction_options + tx_options=transaction_options, ) - return {'tx_hash': response['tx_hash']} + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to deposit IP for WIP: {str(e)}") @@ -70,10 +74,10 @@ def withdraw(self, amount: int, tx_options: dict = None) -> dict: self.account, self.wip_client.build_withdraw_transaction, amount, - tx_options=tx_options + tx_options=tx_options, ) - return {'tx_hash': response['tx_hash']} + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to withdraw WIP: {str(e)}") @@ -102,10 +106,10 @@ def approve(self, spender: str, amount: int, tx_options: dict = None) -> dict: self.wip_client.build_approve_transaction, spender, amount, - tx_options=tx_options + tx_options=tx_options, ) - return {'tx_hash': response['tx_hash']} + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to approve WIP: {str(e)}") @@ -151,15 +155,17 @@ def transfer(self, to: str, amount: int, tx_options: dict = None) -> dict: self.wip_client.build_transfer_transaction, to, amount, - tx_options=tx_options + tx_options=tx_options, ) - return {'tx_hash': response['tx_hash']} + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to transfer WIP: {str(e)}") - def transfer_from(self, from_address: str, to: str, amount: int, tx_options: dict = None) -> dict: + def transfer_from( + self, from_address: str, to: str, amount: int, tx_options: dict = None + ) -> dict: """ Transfers `amount` of WIP from `from_address` to a recipient `to`. @@ -189,14 +195,14 @@ def transfer_from(self, from_address: str, to: str, amount: int, tx_options: dic from_address, to, amount, - tx_options=tx_options + tx_options=tx_options, ) - return {'tx_hash': response['tx_hash']} + return {"tx_hash": response["tx_hash"]} except Exception as e: raise ValueError(f"Failed to transfer WIP from another address: {str(e)}") - + def allowance(self, owner: str, spender: str) -> int: """ Returns the amount of WIP tokens that `spender` is allowed to spend on behalf of `owner`. diff --git a/src/story_protocol_python_sdk/scripts/archive/generate_client.py b/src/story_protocol_python_sdk/scripts/archive/generate_client.py index ebda13d..ccc4b84 100644 --- a/src/story_protocol_python_sdk/scripts/archive/generate_client.py +++ b/src/story_protocol_python_sdk/scripts/archive/generate_client.py @@ -1,4 +1,4 @@ -#scripts/generate_client.py +# scripts/generate_client.py import requests import json @@ -8,22 +8,26 @@ import time # Load environment variables from .env file -load_dotenv(os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env')) +load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".env")) # Get the API key from environment variables -api_key = os.getenv('ETHERSCAN_API_KEY') +api_key = os.getenv("ETHERSCAN_API_KEY") if not api_key: raise ValueError("Please set ETHERSCAN_API_KEY in the .env file") + def fetch_abi(contract_address, api_key): url = f"https://api-sepolia.etherscan.io/api?module=contract&action=getabi&address={contract_address}&apikey={api_key}" response = requests.get(url) response_json = response.json() - if response_json.get('status') == '1': - abi = json.loads(response_json['result']) + if response_json.get("status") == "1": + abi = json.loads(response_json["result"]) return abi else: - raise Exception(f"Error fetching ABI for address {contract_address}: {response_json.get('message')}") + raise Exception( + f"Error fetching ABI for address {contract_address}: {response_json.get('message')}" + ) + def fetch_proxy_implementation_address(proxy_address, api_key): url = "https://api-sepolia.etherscan.io/api" @@ -33,34 +37,44 @@ def fetch_proxy_implementation_address(proxy_address, api_key): "address": proxy_address, "position": "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", "tag": "latest", - "apikey": api_key + "apikey": api_key, } response = requests.get(url, params=params) response_json = response.json() # print(f"Response JSON for {proxy_address}: {response_json}") # Debugging output - if 'result' in response_json: - storage_value = response_json['result'] - if storage_value and storage_value != '0x': + if "result" in response_json: + storage_value = response_json["result"] + if storage_value and storage_value != "0x": implementation_address = "0x" + storage_value[-40:] return implementation_address else: - raise Exception(f"No valid implementation address found in storage for proxy {proxy_address}") + raise Exception( + f"No valid implementation address found in storage for proxy {proxy_address}" + ) else: - raise Exception(f"Error fetching implementation address from storage for proxy {proxy_address}: {response_json}") + raise Exception( + f"Error fetching implementation address from storage for proxy {proxy_address}: {response_json}" + ) + def fetch_proxy_abi(proxy_address, api_key): try: - implementation_address = fetch_proxy_implementation_address(proxy_address, api_key) + implementation_address = fetch_proxy_implementation_address( + proxy_address, api_key + ) return fetch_abi(implementation_address, api_key) except Exception as e: print(f"Failed to fetch proxy implementation address for {proxy_address}: {e}") return None + def save_abi(abi, output_path): - with open(output_path, 'w') as abi_file: + with open(output_path, "w") as abi_file: json.dump(abi, abi_file, indent=2) -class_template = Template(''' + +class_template = Template( + """ import json import os from web3 import Web3 @@ -94,48 +108,54 @@ def build_{{ function.name }}_transaction(self, {% if function.inputs %}{{ funct return self.contract.functions.{{ function.name }}({% if function.inputs %}{{ function.inputs | join(', ') }}{% endif %}).build_transaction(tx_params) {% endif %} {% endfor %} -''') +""" +) + def generate_python_classes_from_abi(abi, contract_name, functions, output_dir): - class_name = contract_name + 'Client' # Use the contract_name directly for proper capitalization - + class_name = ( + contract_name + "Client" + ) # Use the contract_name directly for proper capitalization + selected_functions = [] for item in abi: - if item['type'] == 'function' and item['name'] in functions: + if item["type"] == "function" and item["name"] in functions: function = { - 'name': item['name'], - 'inputs': [input['name'] for input in item['inputs']], - 'stateMutability': item['stateMutability'] if 'stateMutability' in item else 'nonpayable' + "name": item["name"], + "inputs": [input["name"] for input in item["inputs"]], + "stateMutability": ( + item["stateMutability"] + if "stateMutability" in item + else "nonpayable" + ), } selected_functions.append(function) - + # Sort functions to have transact functions first and call functions after - selected_functions.sort(key=lambda x: x['stateMutability'] in ['view', 'pure']) - + selected_functions.sort(key=lambda x: x["stateMutability"] in ["view", "pure"]) + rendered_class = class_template.render( - class_name=class_name, - contract_name=contract_name, - functions=selected_functions + class_name=class_name, contract_name=contract_name, functions=selected_functions ) - + contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) output_file_path = os.path.join(contract_output_dir, f"{contract_name}_client.py") - with open(output_file_path, 'w') as output_file: + with open(output_file_path, "w") as output_file: output_file.write(rendered_class) - + print(f"Generated {class_name} class from ABI") + def main(config_path, output_dir): - with open(config_path, 'r') as config_file: + with open(config_path, "r") as config_file: config = json.load(config_file) - - for contract in config['contracts']: - contract_name = contract['contract_name'] - contract_address = contract['contract_address'] - functions = contract['functions'] + for contract in config["contracts"]: + contract_name = contract["contract_name"] + contract_address = contract["contract_address"] + functions = contract["functions"] for attempt in range(3): # Retry up to 3 times try: @@ -144,9 +164,16 @@ def main(config_path, output_dir): contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) - save_abi(abi, os.path.join(contract_output_dir, f'{contract_name}.json')) - generate_python_classes_from_abi(abi, contract_name, functions, output_dir) - time.sleep(1) # Wait for 1 seconds before moving to next contract + save_abi( + abi, + os.path.join(contract_output_dir, f"{contract_name}.json"), + ) + generate_python_classes_from_abi( + abi, contract_name, functions, output_dir + ) + time.sleep( + 1 + ) # Wait for 1 seconds before moving to next contract break # If successful, break out of the retry loop else: raise Exception("Failed to fetch ABI") @@ -154,8 +181,9 @@ def main(config_path, output_dir): print(f"Error on attempt {attempt + 1} for {contract_name}: {e}") time.sleep(2) # Wait for 2 seconds before retrying + if __name__ == "__main__": - config_path = os.path.join(os.path.dirname(__file__), 'config.json') - output_dir = os.path.join(os.path.dirname(__file__), '../abi') + config_path = os.path.join(os.path.dirname(__file__), "config.json") + output_dir = os.path.join(os.path.dirname(__file__), "../abi") os.makedirs(output_dir, exist_ok=True) main(config_path, output_dir) diff --git a/src/story_protocol_python_sdk/scripts/archive/generate_client_from_abi.py b/src/story_protocol_python_sdk/scripts/archive/generate_client_from_abi.py index c9f9d4a..8985670 100644 --- a/src/story_protocol_python_sdk/scripts/archive/generate_client_from_abi.py +++ b/src/story_protocol_python_sdk/scripts/archive/generate_client_from_abi.py @@ -3,9 +3,10 @@ from jinja2 import Template # Load ABIs from the correct jsons folder -JSONS_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'abi', 'jsons') +JSONS_FOLDER = os.path.join(os.path.dirname(__file__), "..", "abi", "jsons") -class_template = Template(''' +class_template = Template( + """ import json import os from web3 import Web3 @@ -39,60 +40,69 @@ def build_{{ function.name }}_transaction(self, {% if function.inputs %}{{ funct return self.contract.functions.{{ function.name }}({% if function.inputs %}{{ function.inputs | join(', ') }}{% endif %}).build_transaction(tx_params) {% endif %} {% endfor %} -''') +""" +) + def load_abi_from_file(contract_name): abi_path = os.path.join(JSONS_FOLDER, f"{contract_name}.json") if not os.path.exists(abi_path): - raise FileNotFoundError(f"ABI file for {contract_name} not found in {JSONS_FOLDER}") - with open(abi_path, 'r') as abi_file: + raise FileNotFoundError( + f"ABI file for {contract_name} not found in {JSONS_FOLDER}" + ) + with open(abi_path, "r") as abi_file: return json.load(abi_file) + def generate_python_classes_from_abi(abi, contract_name, functions, output_dir): - class_name = contract_name + 'Client' - + class_name = contract_name + "Client" + selected_functions = [] for item in abi: - if item['type'] == 'function' and item['name'] in functions: + if item["type"] == "function" and item["name"] in functions: function = { - 'name': item['name'], - 'inputs': [input['name'] for input in item['inputs']], - 'stateMutability': item['stateMutability'] if 'stateMutability' in item else 'nonpayable' + "name": item["name"], + "inputs": [input["name"] for input in item["inputs"]], + "stateMutability": ( + item["stateMutability"] + if "stateMutability" in item + else "nonpayable" + ), } selected_functions.append(function) - - selected_functions.sort(key=lambda x: x['stateMutability'] in ['view', 'pure']) - + + selected_functions.sort(key=lambda x: x["stateMutability"] in ["view", "pure"]) + rendered_class = class_template.render( - class_name=class_name, - contract_name=contract_name, - functions=selected_functions + class_name=class_name, contract_name=contract_name, functions=selected_functions ) - + contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) output_file_path = os.path.join(contract_output_dir, f"{contract_name}_client.py") - with open(output_file_path, 'w') as output_file: + with open(output_file_path, "w") as output_file: output_file.write(rendered_class) - + print(f"Generated {class_name} class from ABI") + def main(config_path, output_dir): - with open(config_path, 'r') as config_file: + with open(config_path, "r") as config_file: config = json.load(config_file) - - for contract in config['contracts']: - contract_name = contract['contract_name'] - functions = contract['functions'] + + for contract in config["contracts"]: + contract_name = contract["contract_name"] + functions = contract["functions"] try: abi = load_abi_from_file(contract_name) generate_python_classes_from_abi(abi, contract_name, functions, output_dir) except Exception as e: print(f"Error generating class for {contract_name}: {e}") + if __name__ == "__main__": - config_path = os.path.join(os.path.dirname(__file__), 'config.json') - output_dir = os.path.join(os.path.dirname(__file__), '../abi') + config_path = os.path.join(os.path.dirname(__file__), "config.json") + output_dir = os.path.join(os.path.dirname(__file__), "../abi") os.makedirs(output_dir, exist_ok=True) main(config_path, output_dir) diff --git a/src/story_protocol_python_sdk/scripts/archive/generate_client_impl.py b/src/story_protocol_python_sdk/scripts/archive/generate_client_impl.py index 08dcb82..eb13de0 100644 --- a/src/story_protocol_python_sdk/scripts/archive/generate_client_impl.py +++ b/src/story_protocol_python_sdk/scripts/archive/generate_client_impl.py @@ -6,28 +6,34 @@ import time # Load environment variables from .env file -load_dotenv(os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env')) +load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".env")) # Get the API key from environment variables -api_key = os.getenv('ETHERSCAN_API_KEY') +api_key = os.getenv("ETHERSCAN_API_KEY") if not api_key: raise ValueError("Please set ETHERSCAN_API_KEY in the .env file") + def fetch_abi(contract_address, api_key): url = f"https://api-sepolia.etherscan.io/api?module=contract&action=getabi&address={contract_address}&apikey={api_key}" response = requests.get(url) response_json = response.json() - if response_json.get('status') == '1': - abi = json.loads(response_json['result']) + if response_json.get("status") == "1": + abi = json.loads(response_json["result"]) return abi else: - raise Exception(f"Error fetching ABI for address {contract_address}: {response_json.get('message')}") + raise Exception( + f"Error fetching ABI for address {contract_address}: {response_json.get('message')}" + ) + def save_abi(abi, output_path): - with open(output_path, 'w') as abi_file: + with open(output_path, "w") as abi_file: json.dump(abi, abi_file, indent=2) -class_template = Template(''' + +class_template = Template( + """ import json import os from web3 import Web3 @@ -50,47 +56,54 @@ def build_{{ function.name }}_transaction(self, {% if function.inputs %}{{ funct return self.contract.functions.{{ function.name }}({% if function.inputs %}{{ function.inputs | join(', ') }}{% endif %}).build_transaction(tx_params) {% endif %} {% endfor %} -''') +""" +) + def generate_python_classes_from_abi(abi, contract_name, functions, output_dir): - class_name = contract_name + 'Client' # Use the contract_name directly for proper capitalization - + class_name = ( + contract_name + "Client" + ) # Use the contract_name directly for proper capitalization + selected_functions = [] for item in abi: - if item['type'] == 'function' and item['name'] in functions: + if item["type"] == "function" and item["name"] in functions: function = { - 'name': item['name'], - 'inputs': [input['name'] for input in item['inputs']], - 'stateMutability': item['stateMutability'] if 'stateMutability' in item else 'nonpayable' + "name": item["name"], + "inputs": [input["name"] for input in item["inputs"]], + "stateMutability": ( + item["stateMutability"] + if "stateMutability" in item + else "nonpayable" + ), } selected_functions.append(function) - + # Sort functions to have transact functions first and call functions after - selected_functions.sort(key=lambda x: x['stateMutability'] in ['view', 'pure']) - + selected_functions.sort(key=lambda x: x["stateMutability"] in ["view", "pure"]) + rendered_class = class_template.render( - class_name=class_name, - contract_name=contract_name, - functions=selected_functions + class_name=class_name, contract_name=contract_name, functions=selected_functions ) - + contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) output_file_path = os.path.join(contract_output_dir, f"{contract_name}_client.py") - with open(output_file_path, 'w') as output_file: + with open(output_file_path, "w") as output_file: output_file.write(rendered_class) - + print(f"Generated {class_name} class from ABI") + def main(config_path, output_dir): - with open(config_path, 'r') as config_file: + with open(config_path, "r") as config_file: config = json.load(config_file) - - for contract in config['contracts']: - contract_name = contract['contract_name'] - contract_address = contract['contract_address'] - functions = contract['functions'] + + for contract in config["contracts"]: + contract_name = contract["contract_name"] + contract_address = contract["contract_address"] + functions = contract["functions"] for attempt in range(3): # Retry up to 3 times try: @@ -99,9 +112,16 @@ def main(config_path, output_dir): contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) - save_abi(abi, os.path.join(contract_output_dir, f'{contract_name}.json')) - generate_python_classes_from_abi(abi, contract_name, functions, output_dir) - time.sleep(1) # Wait for 1 second before moving to the next contract + save_abi( + abi, + os.path.join(contract_output_dir, f"{contract_name}.json"), + ) + generate_python_classes_from_abi( + abi, contract_name, functions, output_dir + ) + time.sleep( + 1 + ) # Wait for 1 second before moving to the next contract break # If successful, break out of the retry loop else: raise Exception("Failed to fetch ABI") @@ -109,8 +129,9 @@ def main(config_path, output_dir): print(f"Error on attempt {attempt + 1} for {contract_name}: {e}") time.sleep(2) # Wait for 2 seconds before retrying + if __name__ == "__main__": - config_path = os.path.join(os.path.dirname(__file__), 'config_impl.json') - output_dir = os.path.join(os.path.dirname(__file__), '../abi') + config_path = os.path.join(os.path.dirname(__file__), "config_impl.json") + output_dir = os.path.join(os.path.dirname(__file__), "../abi") os.makedirs(output_dir, exist_ok=True) main(config_path, output_dir) diff --git a/src/story_protocol_python_sdk/scripts/archive/generate_client_impl_from_abi.py b/src/story_protocol_python_sdk/scripts/archive/generate_client_impl_from_abi.py index 145f647..501bcb4 100644 --- a/src/story_protocol_python_sdk/scripts/archive/generate_client_impl_from_abi.py +++ b/src/story_protocol_python_sdk/scripts/archive/generate_client_impl_from_abi.py @@ -3,9 +3,10 @@ from jinja2 import Template # Define the folder containing the ABI JSON files -JSONS_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'abi', 'jsons') +JSONS_FOLDER = os.path.join(os.path.dirname(__file__), "..", "abi", "jsons") -class_template = Template(''' +class_template = Template( + """ import json import os from web3 import Web3 @@ -28,65 +29,70 @@ def build_{{ function.name }}_transaction(self, {% if function.inputs %}{{ funct return self.contract.functions.{{ function.name }}({% if function.inputs %}{{ function.inputs | join(', ') }}{% endif %}).build_transaction(tx_params) {% endif %} {% endfor %} -''') +""" +) + def load_abi_from_file(contract_name): """Load ABI JSON from the jsons folder.""" abi_path = os.path.join(JSONS_FOLDER, f"{contract_name}.json") if not os.path.exists(abi_path): - raise FileNotFoundError(f"ABI file for {contract_name} not found in {JSONS_FOLDER}") - with open(abi_path, 'r') as abi_file: + raise FileNotFoundError( + f"ABI file for {contract_name} not found in {JSONS_FOLDER}" + ) + with open(abi_path, "r") as abi_file: return json.load(abi_file) + def generate_python_classes_from_abi(abi, contract_name, functions, output_dir): """Generate a Python class for interacting with the contract.""" - class_name = contract_name + 'Client' # Properly formatted class name - + class_name = contract_name + "Client" # Properly formatted class name + selected_functions = [] for item in abi: - if item['type'] == 'function' and item['name'] in functions: + if item["type"] == "function" and item["name"] in functions: function = { - 'name': item['name'], - 'inputs': [input['name'] for input in item['inputs']], - 'stateMutability': item.get('stateMutability', 'nonpayable') + "name": item["name"], + "inputs": [input["name"] for input in item["inputs"]], + "stateMutability": item.get("stateMutability", "nonpayable"), } selected_functions.append(function) - + # Sort functions: transact functions first, call functions later - selected_functions.sort(key=lambda x: x['stateMutability'] in ['view', 'pure']) - + selected_functions.sort(key=lambda x: x["stateMutability"] in ["view", "pure"]) + rendered_class = class_template.render( - class_name=class_name, - contract_name=contract_name, - functions=selected_functions + class_name=class_name, contract_name=contract_name, functions=selected_functions ) - + # Write the generated class to the output directory contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) output_file_path = os.path.join(contract_output_dir, f"{contract_name}_client.py") - with open(output_file_path, 'w') as output_file: + with open(output_file_path, "w") as output_file: output_file.write(rendered_class) - + print(f"Generated {class_name} class from ABI") + def main(config_path, output_dir): """Main function to generate client classes from ABIs.""" - with open(config_path, 'r') as config_file: + with open(config_path, "r") as config_file: config = json.load(config_file) - - for contract in config['contracts']: - contract_name = contract['contract_name'] - functions = contract['functions'] + + for contract in config["contracts"]: + contract_name = contract["contract_name"] + functions = contract["functions"] try: abi = load_abi_from_file(contract_name) generate_python_classes_from_abi(abi, contract_name, functions, output_dir) except Exception as e: print(f"Error generating class for {contract_name}: {e}") + if __name__ == "__main__": - config_path = os.path.join(os.path.dirname(__file__), 'config_impl.json') - output_dir = os.path.join(os.path.dirname(__file__), '../abi') + config_path = os.path.join(os.path.dirname(__file__), "config_impl.json") + output_dir = os.path.join(os.path.dirname(__file__), "../abi") os.makedirs(output_dir, exist_ok=True) main(config_path, output_dir) diff --git a/src/story_protocol_python_sdk/scripts/generate_clients.py b/src/story_protocol_python_sdk/scripts/generate_clients.py index 182f308..5eb14a2 100644 --- a/src/story_protocol_python_sdk/scripts/generate_clients.py +++ b/src/story_protocol_python_sdk/scripts/generate_clients.py @@ -3,10 +3,11 @@ from jinja2 import Template # Define the folder containing the ABI JSON files -JSONS_FOLDER = os.path.join(os.path.dirname(__file__), '..', 'abi', 'jsons') +JSONS_FOLDER = os.path.join(os.path.dirname(__file__), "..", "abi", "jsons") # Template for regular clients that read address from config -regular_class_template = Template(''' +regular_class_template = Template( + """ import json import os from web3 import Web3 @@ -40,10 +41,12 @@ def build_{{ function.python_name }}_transaction(self{% if function.inputs %}, { return self.contract.functions.{{ function.name }}({% if function.inputs %}{{ function.inputs | join(', ') }}{% endif %}).build_transaction(tx_params) {% endif %} {% endfor %} -''') +""" +) # Template for Impl clients that require address in constructor -impl_class_template = Template(''' +impl_class_template = Template( + """ import json import os from web3 import Web3 @@ -66,129 +69,154 @@ def build_{{ function.python_name }}_transaction(self{% if function.inputs %}, { return self.contract.functions.{{ function.name }}({% if function.inputs %}{{ function.inputs | join(', ') }}{% endif %}).build_transaction(tx_params) {% endif %} {% endfor %} -''') +""" +) + def load_abi_from_file(contract_name): """Load ABI JSON from the jsons folder.""" abi_path = os.path.join(JSONS_FOLDER, f"{contract_name}.json") if not os.path.exists(abi_path): - raise FileNotFoundError(f"ABI file for {contract_name} not found in {JSONS_FOLDER}") - with open(abi_path, 'r') as abi_file: + raise FileNotFoundError( + f"ABI file for {contract_name} not found in {JSONS_FOLDER}" + ) + with open(abi_path, "r") as abi_file: return json.load(abi_file) + def generate_python_class_from_abi(abi, contract_name, functions, output_dir): """Generate a Python class for interacting with the contract.""" - class_name = contract_name + 'Client' - + class_name = contract_name + "Client" + selected_functions = [] function_name_counts = {} - + for item in abi: - if item['type'] == 'function' and item['name'] in functions: + if item["type"] == "function" and item["name"] in functions: # Count occurrences of function names - if item['name'] in function_name_counts: - function_name_counts[item['name']] += 1 + if item["name"] in function_name_counts: + function_name_counts[item["name"]] += 1 else: - function_name_counts[item['name']] = 1 - + function_name_counts[item["name"]] = 1 + # Process inputs, replacing 'from' with 'from_address' inputs = [] - for input_param in item['inputs']: - param_name = input_param['name'] - if param_name == 'from': - param_name = 'from_address' + for input_param in item["inputs"]: + param_name = input_param["name"] + if param_name == "from": + param_name = "from_address" inputs.append(param_name) - + function = { - 'name': item['name'], - 'inputs': inputs, - 'stateMutability': item.get('stateMutability', 'nonpayable') + "name": item["name"], + "inputs": inputs, + "stateMutability": item.get("stateMutability", "nonpayable"), } selected_functions.append(function) - + # Add python_name to each function, with numbering for duplicates function_name_seen = {} for function in selected_functions: - name = function['name'] + name = function["name"] if name in function_name_seen: function_name_seen[name] += 1 - function['python_name'] = f"{name}{function_name_seen[name] + 1}" + function["python_name"] = f"{name}{function_name_seen[name] + 1}" else: function_name_seen[name] = 0 - function['python_name'] = name - + function["python_name"] = name + # Sort functions: transact functions first, call functions later - selected_functions.sort(key=lambda x: x['stateMutability'] in ['view', 'pure']) - + selected_functions.sort(key=lambda x: x["stateMutability"] in ["view", "pure"]) + # Choose template based on whether contract name ends with 'Impl' - template = impl_class_template if contract_name.endswith('Impl') else regular_class_template - + template = ( + impl_class_template + if contract_name.endswith("Impl") + else regular_class_template + ) + rendered_class = template.render( - class_name=class_name, - contract_name=contract_name, - functions=selected_functions + class_name=class_name, contract_name=contract_name, functions=selected_functions ) - + # Write the generated class to the output directory contract_output_dir = os.path.join(output_dir, contract_name) os.makedirs(contract_output_dir, exist_ok=True) output_file_path = os.path.join(contract_output_dir, f"{contract_name}_client.py") - with open(output_file_path, 'w') as output_file: + with open(output_file_path, "w") as output_file: output_file.write(rendered_class) - + print(f"Generated {class_name} class from ABI") + def fix_client_formatting(client_dir): """Fix formatting issues in generated client files.""" for root, dirs, files in os.walk(client_dir): for file in files: - if file.endswith('_client.py'): + if file.endswith("_client.py"): file_path = os.path.join(root, file) - with open(file_path, 'r') as f: + with open(file_path, "r") as f: content = f.read() - + # Fix empty lines between functions (ensure exactly one empty line) import re + # Replace multiple empty lines between function definitions with a single empty line - content = re.sub(r'(\n def [^\n]+\n)(\s*\n)+(\s+def)', r'\1\n\3', content) - + content = re.sub( + r"(\n def [^\n]+\n)(\s*\n)+(\s+def)", r"\1\n\3", content + ) + # Remove empty lines within function bodies - content = re.sub(r'(\n [^\n]+\n)(\s*\n)(\s{8})', r'\1\3', content) - + content = re.sub( + r"(\n [^\n]+\n)(\s*\n)(\s{8})", r"\1\3", content + ) + # Fix parameter formatting for functions with only 'self' - content = re.sub(r'def ([a-zA-Z0-9_]+)\(self, \):', r'def \1(self):', content) - + content = re.sub( + r"def ([a-zA-Z0-9_]+)\(self, \):", r"def \1(self):", content + ) + # Fix empty lines in view/pure function bodies - specifically target the empty line between function definition and return statement - content = re.sub(r'(def [a-zA-Z0-9_]+\(self(?:, [^\)]+)?\):\n\s*\n)(\s+return)', r'\1\2', content) - + content = re.sub( + r"(def [a-zA-Z0-9_]+\(self(?:, [^\)]+)?\):\n\s*\n)(\s+return)", + r"\1\2", + content, + ) + # Remove empty lines between function definition and return statement for all functions - content = re.sub(r'(def [a-zA-Z0-9_]+\(self(?:, [^\)]+)?\):\n)\s*\n(\s+return)', r'\1\2', content) - - with open(file_path, 'w') as f: + content = re.sub( + r"(def [a-zA-Z0-9_]+\(self(?:, [^\)]+)?\):\n)\s*\n(\s+return)", + r"\1\2", + content, + ) + + with open(file_path, "w") as f: f.write(content) - + # print(f"Fixed formatting for {file_path}") + def main(config_path, output_dir): """Main function to generate client classes from ABIs.""" - with open(config_path, 'r') as config_file: + with open(config_path, "r") as config_file: config = json.load(config_file) - - for contract in config['contracts']: - contract_name = contract['contract_name'] - functions = contract['functions'] + + for contract in config["contracts"]: + contract_name = contract["contract_name"] + functions = contract["functions"] try: abi = load_abi_from_file(contract_name) generate_python_class_from_abi(abi, contract_name, functions, output_dir) except Exception as e: print(f"Error generating class for {contract_name}: {e}") - + # Fix formatting in all generated client files fix_client_formatting(output_dir) + if __name__ == "__main__": - config_path = os.path.join(os.path.dirname(__file__), 'config.json') - output_dir = os.path.join(os.path.dirname(__file__), '../abi') + config_path = os.path.join(os.path.dirname(__file__), "config.json") + output_dir = os.path.join(os.path.dirname(__file__), "../abi") os.makedirs(output_dir, exist_ok=True) - main(config_path, output_dir) \ No newline at end of file + main(config_path, output_dir) diff --git a/src/story_protocol_python_sdk/story_client.py b/src/story_protocol_python_sdk/story_client.py index 88015fa..410881c 100644 --- a/src/story_protocol_python_sdk/story_client.py +++ b/src/story_protocol_python_sdk/story_client.py @@ -5,7 +5,7 @@ # Ensure the src directory is in the Python path current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..')) +src_path = os.path.abspath(os.path.join(current_dir, "..")) if src_path not in sys.path: sys.path.append(src_path) @@ -19,6 +19,7 @@ from story_protocol_python_sdk.resources.WIP import WIP from story_protocol_python_sdk.resources.Group import Group + class StoryClient: """ A client for interacting with Story Protocol, providing access to IPAsset, License, and Royalty resources. @@ -27,6 +28,7 @@ class StoryClient: :param account: The account to use for transactions. :param chain_id int: The ID of the blockchain network. """ + def __init__(self, web3, account, chain_id: int): """ Initialize the StoryClient with the given web3 instance, account, and chain ID. @@ -77,7 +79,7 @@ def License(self) -> License: if self._license is None: self._license = License(self.web3, self.account, self.chain_id) return self._license - + @property def Royalty(self) -> Royalty: """ @@ -88,7 +90,7 @@ def Royalty(self) -> Royalty: if self._royalty is None: self._royalty = Royalty(self.web3, self.account, self.chain_id) return self._royalty - + @property def IPAccount(self) -> IPAccount: """ @@ -99,7 +101,7 @@ def IPAccount(self) -> IPAccount: if self._ip_account is None: self._ip_account = IPAccount(self.web3, self.account, self.chain_id) return self._ip_account - + @property def Permission(self) -> Permission: """ @@ -110,7 +112,7 @@ def Permission(self) -> Permission: if self._permission is None: self._permission = Permission(self.web3, self.account, self.chain_id) return self._permission - + @property def NFTClient(self) -> NFTClient: """ @@ -132,7 +134,7 @@ def Dispute(self) -> Dispute: if self._dispute is None: self._dispute = Dispute(self.web3, self.account, self.chain_id) return self._dispute - + @property def WIP(self) -> WIP: """ @@ -143,7 +145,7 @@ def WIP(self) -> WIP: if self._wip is None: self._wip = WIP(self.web3, self.account, self.chain_id) return self._wip - + @property def Group(self) -> Group: """ @@ -152,7 +154,7 @@ def Group(self) -> Group: if self._group is None: self._group = Group(self.web3, self.account, self.chain_id) return self._group - + def get_wallet_balance(self) -> int: """ Get the WIP token balance of the current wallet. @@ -160,7 +162,7 @@ def get_wallet_balance(self) -> int: :return int: The WIP token balance of the current wallet. :raises ValueError: If no account is found. """ - if not self.account or not hasattr(self.account, 'address'): + if not self.account or not hasattr(self.account, "address"): raise ValueError("No account found in wallet") - - return self.web3.eth.get_balance(self.account.address) \ No newline at end of file + + return self.web3.eth.get_balance(self.account.address) diff --git a/src/story_protocol_python_sdk/utils/ipfs.py b/src/story_protocol_python_sdk/utils/ipfs.py index 2c6e83a..8914d93 100644 --- a/src/story_protocol_python_sdk/utils/ipfs.py +++ b/src/story_protocol_python_sdk/utils/ipfs.py @@ -3,53 +3,55 @@ V0_PREFIX = "1220" + def convert_cid_to_hash_ipfs(cid: str) -> str: """ Convert an IPFS CID to a hex hash. - + Args: cid: IPFS CID string - + Returns: Hex string starting with '0x' """ # Check if CID is v0 (starts with "Qm") is_v0 = cid.startswith("Qm") - + # Decode base58 CID bytes_array = base58.b58decode(cid) - + # Convert bytes to hex string - base16_cid = ''.join([f'{b:02x}' for b in bytes_array]) - + base16_cid = "".join([f"{b:02x}" for b in bytes_array]) + # Remove v0 prefix and add 0x - return "0x" + base16_cid[len(V0_PREFIX):] + return "0x" + base16_cid[len(V0_PREFIX) :] + def convert_hash_ipfs_to_cid(hash_str: str, version: str = "v0") -> str: """ Convert a hex hash back to IPFS CID. - + Args: hash_str: Hex string starting with '0x' version: CID version ("v0" or "v1"), defaults to "v0" - + Returns: IPFS CID string """ if not hash_str.startswith("0x"): raise ValueError("Hash must start with '0x'") - + # Add v0 prefix back base16_cid = V0_PREFIX + hash_str[2:] - + # Convert hex string to bytes bytes_array = bytes.fromhex(base16_cid) - + # Encode to base58 base58_cid = base58.b58encode(bytes_array).decode() - + # For now we only support v0 since v1 requires additional dependencies if version == "v1": raise NotImplementedError("CID v1 conversion not yet supported") - + return base58_cid diff --git a/src/story_protocol_python_sdk/utils/license_terms.py b/src/story_protocol_python_sdk/utils/license_terms.py index 6404c6a..cef571f 100644 --- a/src/story_protocol_python_sdk/utils/license_terms.py +++ b/src/story_protocol_python_sdk/utils/license_terms.py @@ -2,116 +2,146 @@ from web3 import Web3 -from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import RoyaltyModuleClient +from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( + RoyaltyModuleClient, +) from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ROYALTY_POLICY + class LicenseTerms: def __init__(self, web3: Web3): self.web3 = web3 self.royalty_module_client = RoyaltyModuleClient(web3) PIL_TYPE = { - 'NON_COMMERCIAL_REMIX': 'non_commercial_remix', - 'COMMERCIAL_USE': 'commercial_use', - 'COMMERCIAL_REMIX': 'commercial_remix' + "NON_COMMERCIAL_REMIX": "non_commercial_remix", + "COMMERCIAL_USE": "commercial_use", + "COMMERCIAL_REMIX": "commercial_remix", } def get_license_term_by_type(self, type, term=None): license_terms = { - 'transferable': True, - 'royaltyPolicy': "0x0000000000000000000000000000000000000000", - 'defaultMintingFee': 0, - 'expiration': 0, - 'commercialUse': False, - 'commercialAttribution': False, - 'commercializerChecker': "0x0000000000000000000000000000000000000000", - 'commercializerCheckerData': "0x0000000000000000000000000000000000000000", - 'commercialRevShare': 0, - 'commercialRevCeiling': 0, - 'derivativesAllowed': True, - 'derivativesAttribution': True, - 'derivativesApproval': False, - 'derivativesReciprocal': True, - 'derivativeRevCeiling': 0, - 'currency': "0x0000000000000000000000000000000000000000", - 'uri': "" + "transferable": True, + "royaltyPolicy": "0x0000000000000000000000000000000000000000", + "defaultMintingFee": 0, + "expiration": 0, + "commercialUse": False, + "commercialAttribution": False, + "commercializerChecker": "0x0000000000000000000000000000000000000000", + "commercializerCheckerData": "0x0000000000000000000000000000000000000000", + "commercialRevShare": 0, + "commercialRevCeiling": 0, + "derivativesAllowed": True, + "derivativesAttribution": True, + "derivativesApproval": False, + "derivativesReciprocal": True, + "derivativeRevCeiling": 0, + "currency": "0x0000000000000000000000000000000000000000", + "uri": "", } - if type == self.PIL_TYPE['NON_COMMERCIAL_REMIX']: - license_terms['commercializerCheckerData'] = "0x" + if type == self.PIL_TYPE["NON_COMMERCIAL_REMIX"]: + license_terms["commercializerCheckerData"] = "0x" return license_terms - elif type == self.PIL_TYPE['COMMERCIAL_USE']: - if not term or 'defaultMintingFee' not in term or 'currency' not in term: - raise ValueError("DefaultMintingFee, currency are required for commercial use PIL.") - - if term['royaltyPolicyAddress'] is None: - term['royaltyPolicyAddress'] = ROYALTY_POLICY - - license_terms.update({ - 'defaultMintingFee': int(term['defaultMintingFee']), - 'currency': term['currency'], - 'commercialUse': True, - 'commercialAttribution': True, - 'derivativesReciprocal': False, - 'royaltyPolicy': term['royaltyPolicyAddress'] - }) + elif type == self.PIL_TYPE["COMMERCIAL_USE"]: + if not term or "defaultMintingFee" not in term or "currency" not in term: + raise ValueError( + "DefaultMintingFee, currency are required for commercial use PIL." + ) + + if term["royaltyPolicyAddress"] is None: + term["royaltyPolicyAddress"] = ROYALTY_POLICY + + license_terms.update( + { + "defaultMintingFee": int(term["defaultMintingFee"]), + "currency": term["currency"], + "commercialUse": True, + "commercialAttribution": True, + "derivativesReciprocal": False, + "royaltyPolicy": term["royaltyPolicyAddress"], + } + ) return license_terms else: - if not term or 'defaultMintingFee' not in term or 'currency' not in term or 'commercialRevShare' not in term: - raise ValueError("DefaultMintingFee, currency and commercialRevShare are required for commercial remix PIL.") - - if 'royaltyPolicyAddress' not in term: + if ( + not term + or "defaultMintingFee" not in term + or "currency" not in term + or "commercialRevShare" not in term + ): + raise ValueError( + "DefaultMintingFee, currency and commercialRevShare are required for commercial remix PIL." + ) + + if "royaltyPolicyAddress" not in term: raise ValueError("royaltyPolicyAddress is required") - - if term['commercialRevShare'] < 0 or term['commercialRevShare'] > 100: + + if term["commercialRevShare"] < 0 or term["commercialRevShare"] > 100: raise ValueError("CommercialRevShare should be between 0 and 100.") - - license_terms.update({ - 'defaultMintingFee': int(term['defaultMintingFee']), - 'currency': term['currency'], - 'commercialUse': True, - 'commercialAttribution': True, - 'commercialRevShare': int((term['commercialRevShare'] / 100) * 100000000), - 'derivativesReciprocal': True, - 'royaltyPolicy': term['royaltyPolicyAddress'] - }) + + license_terms.update( + { + "defaultMintingFee": int(term["defaultMintingFee"]), + "currency": term["currency"], + "commercialUse": True, + "commercialAttribution": True, + "commercialRevShare": int( + (term["commercialRevShare"] / 100) * 100000000 + ), + "derivativesReciprocal": True, + "royaltyPolicy": term["royaltyPolicyAddress"], + } + ) return license_terms def validate_license_terms(self, params): - royalty_policy = params.get('royalty_policy') - currency = params.get('currency') + royalty_policy = params.get("royalty_policy") + currency = params.get("currency") if royalty_policy != ZERO_ADDRESS: - is_whitelisted = self.royalty_module_client.isWhitelistedRoyaltyPolicy(royalty_policy) + is_whitelisted = self.royalty_module_client.isWhitelistedRoyaltyPolicy( + royalty_policy + ) if not is_whitelisted: raise ValueError("The royalty policy is not whitelisted.") if currency != ZERO_ADDRESS: - is_whitelisted = self.royalty_module_client.isWhitelistedRoyaltyToken(currency) + is_whitelisted = self.royalty_module_client.isWhitelistedRoyaltyToken( + currency + ) if not is_whitelisted: raise ValueError("The currency token is not whitelisted.") if royalty_policy != ZERO_ADDRESS and currency == ZERO_ADDRESS: raise ValueError("Royalty policy requires currency token.") - params['default_minting_fee'] = int(params.get('default_minting_fee', 0)) - params['expiration'] = int(params.get('expiration', 0)) - params['commercial_rev_ceiling'] = int(params.get('commercial_rev_ceiling', 0)) - params['derivative_rev_ceiling'] = int(params.get('derivative_rev_ceiling', 0)) + params["default_minting_fee"] = int(params.get("default_minting_fee", 0)) + params["expiration"] = int(params.get("expiration", 0)) + params["commercial_rev_ceiling"] = int(params.get("commercial_rev_ceiling", 0)) + params["derivative_rev_ceiling"] = int(params.get("derivative_rev_ceiling", 0)) self.verify_commercial_use(params) self.verify_derivatives(params) - commercial_rev_share = params.get('commercial_rev_share', 0) + commercial_rev_share = params.get("commercial_rev_share", 0) if commercial_rev_share < 0 or commercial_rev_share > 100: raise ValueError("CommercialRevShare should be between 0 and 100.") else: - params['commercial_rev_share'] = int((commercial_rev_share / 100) * 100000000) + params["commercial_rev_share"] = int( + (commercial_rev_share / 100) * 100000000 + ) - commercializer_checker_data = params.get('commercializer_checker_data', ZERO_ADDRESS) + commercializer_checker_data = params.get( + "commercializer_checker_data", ZERO_ADDRESS + ) if isinstance(commercializer_checker_data, str): - params['commercializer_checker_data'] = Web3.to_bytes(hexstr=commercializer_checker_data) + params["commercializer_checker_data"] = Web3.to_bytes( + hexstr=commercializer_checker_data + ) - params['expect_minimum_group_reward_share'] = int(params.get('expect_minimum_group_reward_share', 0)) + params["expect_minimum_group_reward_share"] = int( + params.get("expect_minimum_group_reward_share", 0) + ) return params def validate_licensing_config(self, params): @@ -119,14 +149,14 @@ def validate_licensing_config(self, params): raise TypeError("Licensing config parameters must be a dictionary") required_params = { - 'is_set': bool, - 'minting_fee': int, - 'hook_data': str, - 'licensing_hook': str, - 'commercial_rev_share': int, - 'disabled': bool, - 'expect_minimum_group_reward_share': int, - 'expect_group_reward_pool': str + "is_set": bool, + "minting_fee": int, + "hook_data": str, + "licensing_hook": str, + "commercial_rev_share": int, + "disabled": bool, + "expect_minimum_group_reward_share": int, + "expect_group_reward_pool": str, } for param, expected_type in required_params.items(): @@ -135,64 +165,96 @@ def validate_licensing_config(self, params): raise TypeError(f"{param} must be of type {expected_type.__name__}") default_params = { - 'is_set': False, - 'minting_fee': 0, - 'hook_data': ZERO_ADDRESS, - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS + "is_set": False, + "minting_fee": 0, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, } - if not params.get('is_set', False): + if not params.get("is_set", False): return default_params - - if params.get('minting_fee', 0) < 0: + + if params.get("minting_fee", 0) < 0: raise ValueError("Minting fee cannot be negative") - - if params.get('commercial_rev_share', 0) < 0 or params.get('commercial_rev_share', 0) > 100: + + if ( + params.get("commercial_rev_share", 0) < 0 + or params.get("commercial_rev_share", 0) > 100 + ): raise ValueError("Commercial revenue share must be between 0 and 100") else: - params['commercial_rev_share'] = int((params['commercial_rev_share'] / 100) * 100000000) - - if params.get('expect_minimum_group_reward_share', 0) < 0 or params.get('expect_minimum_group_reward_share', 0) > 100: - raise ValueError("Expect minimum group reward share must be between 0 and 100") - - params['hook_data'] = Web3.to_bytes(hexstr=params['hook_data']) - + params["commercial_rev_share"] = int( + (params["commercial_rev_share"] / 100) * 100000000 + ) + + if ( + params.get("expect_minimum_group_reward_share", 0) < 0 + or params.get("expect_minimum_group_reward_share", 0) > 100 + ): + raise ValueError( + "Expect minimum group reward share must be between 0 and 100" + ) + + params["hook_data"] = Web3.to_bytes(hexstr=params["hook_data"]) + default_params.update(params) return default_params def verify_commercial_use(self, terms): - if not terms.get('commercial_use', False): - if terms.get('commercial_attribution'): - raise ValueError("Cannot add commercial attribution when commercial use is disabled.") - if terms.get('commercializer_checker') != ZERO_ADDRESS: - raise ValueError("Cannot add commercializerChecker when commercial use is disabled.") - if terms.get('commercial_rev_share', 0) > 0: - raise ValueError("Cannot add commercial revenue share when commercial use is disabled.") - if terms.get('commercial_rev_ceiling', 0) > 0: - raise ValueError("Cannot add commercial revenue ceiling when commercial use is disabled.") - if terms.get('derivative_rev_ceiling', 0) > 0: - raise ValueError("Cannot add derivative revenue ceiling when commercial use is disabled.") - if terms.get('royalty_policy') != ZERO_ADDRESS: - raise ValueError("Cannot add commercial royalty policy when commercial use is disabled.") + if not terms.get("commercial_use", False): + if terms.get("commercial_attribution"): + raise ValueError( + "Cannot add commercial attribution when commercial use is disabled." + ) + if terms.get("commercializer_checker") != ZERO_ADDRESS: + raise ValueError( + "Cannot add commercializerChecker when commercial use is disabled." + ) + if terms.get("commercial_rev_share", 0) > 0: + raise ValueError( + "Cannot add commercial revenue share when commercial use is disabled." + ) + if terms.get("commercial_rev_ceiling", 0) > 0: + raise ValueError( + "Cannot add commercial revenue ceiling when commercial use is disabled." + ) + if terms.get("derivative_rev_ceiling", 0) > 0: + raise ValueError( + "Cannot add derivative revenue ceiling when commercial use is disabled." + ) + if terms.get("royalty_policy") != ZERO_ADDRESS: + raise ValueError( + "Cannot add commercial royalty policy when commercial use is disabled." + ) else: - if terms.get('royalty_policy') == ZERO_ADDRESS: - raise ValueError("Royalty policy is required when commercial use is enabled.") + if terms.get("royalty_policy") == ZERO_ADDRESS: + raise ValueError( + "Royalty policy is required when commercial use is enabled." + ) def verify_derivatives(self, terms): - if not terms.get('derivatives_allowed', False): - if terms.get('derivatives_attribution'): - raise ValueError("Cannot add derivative attribution when derivative use is disabled.") - if terms.get('derivatives_approval'): - raise ValueError("Cannot add derivative approval when derivative use is disabled.") - if terms.get('derivatives_reciprocal'): - raise ValueError("Cannot add derivative reciprocal when derivative use is disabled.") - if terms.get('derivative_rev_ceiling', 0) > 0: - raise ValueError("Cannot add derivative revenue ceiling when derivative use is disabled.") + if not terms.get("derivatives_allowed", False): + if terms.get("derivatives_attribution"): + raise ValueError( + "Cannot add derivative attribution when derivative use is disabled." + ) + if terms.get("derivatives_approval"): + raise ValueError( + "Cannot add derivative approval when derivative use is disabled." + ) + if terms.get("derivatives_reciprocal"): + raise ValueError( + "Cannot add derivative reciprocal when derivative use is disabled." + ) + if terms.get("derivative_rev_ceiling", 0) > 0: + raise ValueError( + "Cannot add derivative revenue ceiling when derivative use is disabled." + ) def get_revenue_share(self, rev_share: int | str) -> int: """ @@ -211,4 +273,3 @@ def get_revenue_share(self, rev_share: int | str) -> int: MAX_ROYALTY_TOKEN = 100000000 return int((rev_share_number / 100) * MAX_ROYALTY_TOKEN) - \ No newline at end of file diff --git a/src/story_protocol_python_sdk/utils/oov3.py b/src/story_protocol_python_sdk/utils/oov3.py index 1ff9b3e..5900212 100644 --- a/src/story_protocol_python_sdk/utils/oov3.py +++ b/src/story_protocol_python_sdk/utils/oov3.py @@ -3,25 +3,35 @@ import json import os from web3 import Web3 -from story_protocol_python_sdk.abi.ArbitrationPolicyUMA.ArbitrationPolicyUMA_client import ArbitrationPolicyUMAClient +from story_protocol_python_sdk.abi.ArbitrationPolicyUMA.ArbitrationPolicyUMA_client import ( + ArbitrationPolicyUMAClient, +) -abi_path = os.path.join(os.path.dirname(__file__), '..', 'abi', 'jsons', 'ASSERTION_ABI.json') -with open(abi_path, 'r') as abi_file: +abi_path = os.path.join( + os.path.dirname(__file__), "..", "abi", "jsons", "ASSERTION_ABI.json" +) +with open(abi_path, "r") as abi_file: ASSERTION_ABI = json.load(abi_file) + def get_oov3_contract(arbitration_policy_uma_client: ArbitrationPolicyUMAClient) -> str: """ Get the OOv3 contract address. - + :param arbitration_policy_uma_client: The ArbitrationPolicyUMA client instance. :return str: The OOv3 contract address. """ return arbitration_policy_uma_client.oov3() -def get_assertion_bond(web3: Web3, arbitration_policy_uma_client: ArbitrationPolicyUMAClient, assertion_id: str) -> int: + +def get_assertion_bond( + web3: Web3, + arbitration_policy_uma_client: ArbitrationPolicyUMAClient, + assertion_id: str, +) -> int: """ Get assertion details to determine bond amount. - + :param web3: The Web3 instance. :param arbitration_policy_uma_client: The ArbitrationPolicyUMA client instance. :param assertion_id str: The ID of the assertion. @@ -29,11 +39,13 @@ def get_assertion_bond(web3: Web3, arbitration_policy_uma_client: ArbitrationPol """ try: oov3_contract_address = get_oov3_contract(arbitration_policy_uma_client) - - oov3_contract = web3.eth.contract(address=oov3_contract_address, abi=ASSERTION_ABI) - + + oov3_contract = web3.eth.contract( + address=oov3_contract_address, abi=ASSERTION_ABI + ) + assertion_data = oov3_contract.functions.getAssertion(assertion_id).call() - + return assertion_data[9] except Exception as e: - raise ValueError(f"Failed to get assertion details: {str(e)}") \ No newline at end of file + raise ValueError(f"Failed to get assertion details: {str(e)}") diff --git a/src/story_protocol_python_sdk/utils/sign.py b/src/story_protocol_python_sdk/utils/sign.py index f4ec5dd..88bcc4d 100644 --- a/src/story_protocol_python_sdk/utils/sign.py +++ b/src/story_protocol_python_sdk/utils/sign.py @@ -6,11 +6,16 @@ from eth_abi.abi import encode from datetime import datetime -from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import IPAccountImplClient -from story_protocol_python_sdk.abi.AccessController.AccessController_client import AccessControllerClient +from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import ( + IPAccountImplClient, +) +from story_protocol_python_sdk.abi.AccessController.AccessController_client import ( + AccessControllerClient, +) from story_protocol_python_sdk.utils.constants import ZERO_FUNC + class Sign: def __init__(self, web3: Web3, chain_id: int, account): self.web3 = web3 @@ -20,7 +25,14 @@ def __init__(self, web3: Web3, chain_id: int, account): self.ip_account_client = IPAccountImplClient(web3) self.access_controller_client = AccessControllerClient(web3) - def get_signature(self, state: str, to: str, encode_data: bytes, verifying_contract: str, deadline: int) -> dict: + def get_signature( + self, + state: str, + to: str, + encode_data: bytes, + verifying_contract: str, + deadline: int, + ) -> dict: """ Get the signature. @@ -33,22 +45,19 @@ def get_signature(self, state: str, to: str, encode_data: bytes, verifying_contr """ try: execute_data = self.ip_account_client.contract.encode_abi( - abi_element_identifier="execute", - args=[ - to, - 0, - encode_data - ] + abi_element_identifier="execute", args=[to, 0, encode_data] ) - #expected_state = nonce + # expected_state = nonce expected_state = Web3.keccak( encode( ["bytes32", "bytes"], [ state, # Current state (nonce) - Web3.to_bytes(hexstr=execute_data) # Convert hex string to bytes - ] + Web3.to_bytes( + hexstr=execute_data + ), # Convert hex string to bytes + ], ) ) @@ -77,17 +86,19 @@ def get_signature(self, state: str, to: str, encode_data: bytes, verifying_contr "deadline": deadline, } - signable_message = encode_typed_data(domain_data, message_types, message_data) + signable_message = encode_typed_data( + domain_data, message_types, message_data + ) signed_message = Account.sign_message(signable_message, self.account.key) return { "signature": signed_message.signature.hex(), - "nonce": expected_state + "nonce": expected_state, } except Exception as e: raise e - + def get_deadline(self, deadline: int = None) -> int: """ Calculate the deadline for a transaction. @@ -96,15 +107,22 @@ def get_deadline(self, deadline: int = None) -> int: :return int: The calculated deadline in milliseconds. """ current_timestamp = int(datetime.now().timestamp() * 1000) - + if deadline is not None: if not isinstance(deadline, int) or deadline < 0: raise ValueError("Invalid deadline value.") return current_timestamp + deadline else: return current_timestamp + 1000 - - def get_permission_signature(self, ip_id: str, deadline: int, permissions: list, permission_func: str = None, state: str = None) -> dict: + + def get_permission_signature( + self, + ip_id: str, + deadline: int, + permissions: list, + permission_func: str = None, + state: str = None, + ) -> dict: """ Get the signature for setting permissions. @@ -118,7 +136,11 @@ def get_permission_signature(self, ip_id: str, deadline: int, permissions: list, try: # Auto-select permission function based on number of permissions if not specified if permission_func is None: - permission_function = "setBatchTransientPermissions" if len(permissions) >= 2 else "setTransientPermission" + permission_function = ( + "setBatchTransientPermissions" + if len(permissions) >= 2 + else "setTransientPermission" + ) else: permission_function = permission_func @@ -130,30 +152,38 @@ def get_permission_signature(self, ip_id: str, deadline: int, permissions: list, encode_data = self.access_controller_client.contract.encode_abi( abi_element_identifier="setTransientPermission", args=[ - self.web3.to_checksum_address(permissions[0]['ipId']), - self.web3.to_checksum_address(permissions[0]['signer']), - self.web3.to_checksum_address(permissions[0]['to']), - Web3.keccak(text=permissions[0]['func'])[:4] if permissions[0].get('func') else b'\x00\x00\x00\x00', - permissions[0]['permission'] - ] + self.web3.to_checksum_address(permissions[0]["ipId"]), + self.web3.to_checksum_address(permissions[0]["signer"]), + self.web3.to_checksum_address(permissions[0]["to"]), + ( + Web3.keccak(text=permissions[0]["func"])[:4] + if permissions[0].get("func") + else b"\x00\x00\x00\x00" + ), + permissions[0]["permission"], + ], ) else: # Encode multiple permissions - format them correctly for the contract formatted_permissions = [] for p in permissions: formatted_permission = { - 'ipAccount': self.web3.to_checksum_address(p['ipId']), - 'signer': self.web3.to_checksum_address(p['signer']), - 'to': self.web3.to_checksum_address(p['to']), - 'func': Web3.keccak(text=p['func'])[:4] if p.get('func') else b'\x00\x00\x00\x00', - 'permission': p['permission'] + "ipAccount": self.web3.to_checksum_address(p["ipId"]), + "signer": self.web3.to_checksum_address(p["signer"]), + "to": self.web3.to_checksum_address(p["to"]), + "func": ( + Web3.keccak(text=p["func"])[:4] + if p.get("func") + else b"\x00\x00\x00\x00" + ), + "permission": p["permission"], } formatted_permissions.append(formatted_permission) # Pass the array as a single argument encode_data = self.access_controller_client.contract.encode_abi( abi_element_identifier=permission_function, - args=[formatted_permissions] + args=[formatted_permissions], ) return self.get_signature( @@ -161,7 +191,7 @@ def get_permission_signature(self, ip_id: str, deadline: int, permissions: list, to=access_address, encode_data=encode_data, verifying_contract=ip_id, - deadline=deadline + deadline=deadline, ) except Exception as e: diff --git a/src/story_protocol_python_sdk/utils/transaction_utils.py b/src/story_protocol_python_sdk/utils/transaction_utils.py index deeaa7f..8f58ef3 100644 --- a/src/story_protocol_python_sdk/utils/transaction_utils.py +++ b/src/story_protocol_python_sdk/utils/transaction_utils.py @@ -4,7 +4,10 @@ TRANSACTION_TIMEOUT = 300 -def build_and_send_transaction(web3: Web3, account, client_function, *client_args, tx_options: dict = None) -> dict: + +def build_and_send_transaction( + web3: Web3, account, client_function, *client_args, tx_options: dict = None +) -> dict: """ Builds and sends a transaction using the provided client function and arguments. @@ -20,36 +23,35 @@ def build_and_send_transaction(web3: Web3, account, client_function, *client_arg tx_options = tx_options or {} transaction_options = { - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), } # Add value if it exists in tx_options - if 'value' in tx_options: - transaction_options['value'] = tx_options['value'] + if "value" in tx_options: + transaction_options["value"] = tx_options["value"] - if 'gasPrice' in tx_options: - transaction_options['gasPrice'] = web3.to_wei(tx_options['gasPrice'], 'gwei') - if 'maxFeePerGas' in tx_options: - transaction_options['maxFeePerGas'] = tx_options['maxFeePerGas'] + if "gasPrice" in tx_options: + transaction_options["gasPrice"] = web3.to_wei( + tx_options["gasPrice"], "gwei" + ) + if "maxFeePerGas" in tx_options: + transaction_options["maxFeePerGas"] = tx_options["maxFeePerGas"] transaction = client_function(*client_args, transaction_options) # If encodedTxDataOnly is True, return the transaction data without sending - if tx_options.get('encodedTxDataOnly'): - return { - 'encodedTxData': transaction - } + if tx_options.get("encodedTxDataOnly"): + return {"encodedTxData": transaction} signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) - tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash, timeout=TRANSACTION_TIMEOUT) + tx_receipt = web3.eth.wait_for_transaction_receipt( + tx_hash, timeout=TRANSACTION_TIMEOUT + ) - return { - 'tx_hash': tx_hash.hex(), - 'tx_receipt': tx_receipt - } + return {"tx_hash": tx_hash.hex(), "tx_receipt": tx_receipt} except Exception as e: raise e diff --git a/src/story_protocol_python_sdk/utils/validation.py b/src/story_protocol_python_sdk/utils/validation.py index e1ea155..fdaaee1 100644 --- a/src/story_protocol_python_sdk/utils/validation.py +++ b/src/story_protocol_python_sdk/utils/validation.py @@ -1,9 +1,10 @@ from web3 import Web3 + def validate_address(address: str) -> str: """ Validates if the provided string is a valid Ethereum address. - + :param address str: The address to validate. :return str: The validated address. :raises ValueError: If the address is not valid. @@ -11,5 +12,3 @@ def validate_address(address: str) -> str: if not Web3.is_address(address): raise ValueError(f"Invalid address: {address}.") return address - - diff --git a/tests/__init__.py b/tests/__init__.py index c5a6e4c..539129d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -# This file makes the tests/unit directory a Python package \ No newline at end of file +# This file makes the tests/unit directory a Python package diff --git a/tests/demo/demo.py b/tests/demo/demo.py index 4b08035..28f1557 100644 --- a/tests/demo/demo.py +++ b/tests/demo/demo.py @@ -3,13 +3,22 @@ from web3 import Web3 from story_protocol_python_sdk import StoryClient -from demo_utils import get_token_id, MockERC721, MockERC20, mint_tokens, ROYALTY_POLICY, ROYALTY_MODULE, PIL_LICENSE_TEMPLATE +from demo_utils import ( + get_token_id, + MockERC721, + MockERC20, + mint_tokens, + ROYALTY_POLICY, + ROYALTY_MODULE, + PIL_LICENSE_TEMPLATE, +) + def main(): # 1. Set up your Story Config load_dotenv() - private_key = os.getenv('WALLET_PRIVATE_KEY') - rpc_url = os.getenv('RPC_PROVIDER_URL') + private_key = os.getenv("WALLET_PRIVATE_KEY") + rpc_url = os.getenv("RPC_PROVIDER_URL") if not private_key: raise ValueError("WALLET_PRIVATE_KEY environment variable is not set") @@ -31,62 +40,77 @@ def main(): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) registered_ip_asset_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id + ) + print( + f"Root IPA created at transaction hash {registered_ip_asset_response['txHash']}, IPA ID: {registered_ip_asset_response['ipId']}" ) - print(f"Root IPA created at transaction hash {registered_ip_asset_response['txHash']}, IPA ID: {registered_ip_asset_response['ipId']}") # 3. Register PIL Terms register_pil_terms_response = story_client.License.registerCommercialUsePIL( - default_minting_fee=1, - currency=MockERC20, - royalty_policy=ROYALTY_POLICY + default_minting_fee=1, currency=MockERC20, royalty_policy=ROYALTY_POLICY ) - if 'txHash' in register_pil_terms_response: - print(f"PIL Terms registered at transaction hash {register_pil_terms_response['txHash']}, License Terms ID: {register_pil_terms_response['licenseTermsId']}") + if "txHash" in register_pil_terms_response: + print( + f"PIL Terms registered at transaction hash {register_pil_terms_response['txHash']}, License Terms ID: {register_pil_terms_response['licenseTermsId']}" + ) else: - print(f"License Terms ID: {register_pil_terms_response['licenseTermsId']}, already registered.") + print( + f"License Terms ID: {register_pil_terms_response['licenseTermsId']}, already registered." + ) # 4. Attach License Terms to IP try: attach_license_terms_response = story_client.License.attachLicenseTerms( - ip_id=registered_ip_asset_response['ipId'], + ip_id=registered_ip_asset_response["ipId"], license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=register_pil_terms_response['licenseTermsId'] + license_terms_id=register_pil_terms_response["licenseTermsId"], + ) + print( + f"Attached License Terms to IP at transaction hash {attach_license_terms_response['txHash']}" ) - print(f"Attached License Terms to IP at transaction hash {attach_license_terms_response['txHash']}") except Exception as e: - print(f"License Terms ID {register_pil_terms_response['licenseTermsId']} already attached to this IPA.") + print( + f"License Terms ID {register_pil_terms_response['licenseTermsId']} already attached to this IPA." + ) - #Before you mint make sure you have enough ERC20 tokens according to the minting fee above + # Before you mint make sure you have enough ERC20 tokens according to the minting fee above token_ids = mint_tokens(MockERC20, web3, account, account.address, 10000) # 5. Mint License mint_license_response = story_client.License.mintLicenseTokens( - licensor_ip_id=registered_ip_asset_response['ipId'], + licensor_ip_id=registered_ip_asset_response["ipId"], license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=register_pil_terms_response['licenseTermsId'], + license_terms_id=register_pil_terms_response["licenseTermsId"], amount=1, receiver=account.address, max_minting_fee=1, - max_revenue_share=0 + max_revenue_share=0, + ) + print( + f"License Token minted at transaction hash {mint_license_response['txHash']}, License Token IDs: {mint_license_response['licenseTokenIds']}" ) - print(f"License Token minted at transaction hash {mint_license_response['txHash']}, License Token IDs: {mint_license_response['licenseTokenIds']}") # 6. Mint derivative IP Asset using your license - derivative_token_id = get_token_id(MockERC721, story_client.web3, story_client.account) + derivative_token_id = get_token_id( + MockERC721, story_client.web3, story_client.account + ) registered_ip_asset_derivative_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=derivative_token_id + nft_contract=MockERC721, token_id=derivative_token_id + ) + print( + f"Derivative IPA created at transaction hash {registered_ip_asset_derivative_response['txHash']}, IPA ID: {registered_ip_asset_derivative_response['ipId']}" ) - print(f"Derivative IPA created at transaction hash {registered_ip_asset_derivative_response['txHash']}, IPA ID: {registered_ip_asset_derivative_response['ipId']}") link_derivative_response = story_client.IPAsset.registerDerivativeWithLicenseTokens( - child_ip_id=registered_ip_asset_derivative_response['ipId'], - license_token_ids=mint_license_response['licenseTokenIds'], - max_rts=5 * 10 ** 6 + child_ip_id=registered_ip_asset_derivative_response["ipId"], + license_token_ids=mint_license_response["licenseTokenIds"], + max_rts=5 * 10**6, ) - print(f"Derivative IPA linked to parent at transaction hash {link_derivative_response['txHash']}") + print( + f"Derivative IPA linked to parent at transaction hash {link_derivative_response['txHash']}" + ) + if __name__ == "__main__": main() diff --git a/tests/demo/demo_utils.py b/tests/demo/demo_utils.py index 039254f..a55d01b 100644 --- a/tests/demo/demo_utils.py +++ b/tests/demo/demo_utils.py @@ -5,70 +5,79 @@ MockERC20 = "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -ROYALTY_POLICY="0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" #Royalty Policy LAP -ROYALTY_MODULE="0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086" -PIL_LICENSE_TEMPLATE="0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316" +ROYALTY_POLICY = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" # Royalty Policy LAP +ROYALTY_MODULE = "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086" +PIL_LICENSE_TEMPLATE = "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316" + def get_token_id(nft_contract, web3, account): contract_abi = [ { "inputs": [{"internalType": "address", "name": "to", "type": "address"}], "name": "mint", - "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], + "outputs": [ + {"internalType": "uint256", "name": "tokenId", "type": "uint256"} + ], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=nft_contract, abi=contract_abi) - + try: - transaction = contract.functions.mint(account.address).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000 - }) + transaction = contract.functions.mint(account.address).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + } + ) signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - logs = tx_receipt['logs'] - if len(logs) > 0 and len(logs[0]['topics']) > 3: - return int(logs[0]['topics'][3].hex(), 16) + logs = tx_receipt["logs"] + if len(logs) > 0 and len(logs[0]["topics"]) > 3: + return int(logs[0]["topics"][3].hex(), 16) raise ValueError(f"No token ID in logs: {tx_receipt}") - + except Exception as e: raise e + def mint_tokens(erc20_contract_address, web3, account, to_address, amount): contract_abi = [ { "inputs": [ {"internalType": "address", "name": "to", "type": "address"}, - {"internalType": "uint256", "name": "amount", "type": "uint256"} + {"internalType": "uint256", "name": "amount", "type": "uint256"}, ], "name": "mint", "outputs": [], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=erc20_contract_address, abi=contract_abi) - transaction = contract.functions.mint(to_address, amount).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000, - 'gasPrice': web3.to_wei('300', 'gwei') - }) - + transaction = contract.functions.mint(to_address, amount).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + "gasPrice": web3.to_wei("300", "gwei"), + } + ) + signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - + return tx_receipt + def approve(erc20_contract_address, web3, account, spender_address, amount): erc20_contract_address = web3.to_checksum_address(erc20_contract_address) spender_address = web3.to_checksum_address(spender_address) @@ -77,27 +86,27 @@ def approve(erc20_contract_address, web3, account, spender_address, amount): { "inputs": [ {"internalType": "address", "name": "spender", "type": "address"}, - {"internalType": "uint256", "name": "value", "type": "uint256"} + {"internalType": "uint256", "name": "value", "type": "uint256"}, ], "name": "approve", - "outputs": [ - {"internalType": "bool", "name": "", "type": "bool"} - ], + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=erc20_contract_address, abi=contract_abi) - transaction = contract.functions.approve(spender_address, amount).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000, - 'gasPrice': web3.to_wei('300', 'gwei') - }) - + transaction = contract.functions.approve(spender_address, amount).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + "gasPrice": web3.to_wei("300", "gwei"), + } + ) + signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - + return tx_receipt diff --git a/tests/integration/setup_for_integration.py b/tests/integration/setup_for_integration.py index 750ea54..3aecdce 100644 --- a/tests/integration/setup_for_integration.py +++ b/tests/integration/setup_for_integration.py @@ -6,7 +6,7 @@ # Ensure the src directory is in the Python path current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..')) +src_path = os.path.abspath(os.path.join(current_dir, "..", "..")) if src_path not in sys.path: sys.path.append(src_path) @@ -31,16 +31,16 @@ EVEN_SPLIT_GROUP_POOL, ROYALTY_POLICY_LRP, CORE_METADATA_MODULE, - mint_by_spg + mint_by_spg, ) # Load environment variables load_dotenv(override=True) -private_key = os.getenv('WALLET_PRIVATE_KEY') -private_key_2 = os.getenv('WALLET_PRIVATE_KEY_2') -rpc_url = os.getenv('RPC_PROVIDER_URL') -wallet_address = os.getenv('WALLET_ADDRESS') -wallet_address_2 = os.getenv('WALLET_ADDRESS_2') +private_key = os.getenv("WALLET_PRIVATE_KEY") +private_key_2 = os.getenv("WALLET_PRIVATE_KEY_2") +rpc_url = os.getenv("RPC_PROVIDER_URL") +wallet_address = os.getenv("WALLET_ADDRESS") +wallet_address_2 = os.getenv("WALLET_ADDRESS_2") if not private_key: raise ValueError("WALLET_PRIVATE_KEY environment variable is not set") @@ -56,42 +56,44 @@ account = web3.eth.account.from_key(private_key) account_2 = web3.eth.account.from_key(private_key_2) + @pytest.fixture(scope="session") def story_client(): return get_story_client_in_devnet(web3, account) + @pytest.fixture(scope="session") def story_client_2(): return get_story_client_in_devnet(web3, account_2) + # Export everything needed by test files __all__ = [ - 'web3', - 'account', - 'account_2', - 'story_client', - 'get_token_id', - 'mint_tokens', - 'approve', - 'get_block_timestamp', - 'check_event_in_tx', - 'MockERC721', - 'MockERC20', - 'ZERO_ADDRESS', - 'ROYALTY_POLICY' - 'ROYALTY_MODULE', - 'PIL_LICENSE_TEMPLATE', - 'ARBITRATION_POLICY_UMA', - 'account_2', - 'story_client_2', - 'generate_cid', - 'setup_royalty_vault', - 'WIP_TOKEN_ADDRESS', - 'wallet_address', - 'wallet_address_2', - 'private_key', - 'private_key_2', - 'EVEN_SPLIT_GROUP_POOL', - 'ROYALTY_POLICY_LRP', - 'mint_by_spg' -] \ No newline at end of file + "web3", + "account", + "account_2", + "story_client", + "get_token_id", + "mint_tokens", + "approve", + "get_block_timestamp", + "check_event_in_tx", + "MockERC721", + "MockERC20", + "ZERO_ADDRESS", + "ROYALTY_POLICY" "ROYALTY_MODULE", + "PIL_LICENSE_TEMPLATE", + "ARBITRATION_POLICY_UMA", + "account_2", + "story_client_2", + "generate_cid", + "setup_royalty_vault", + "WIP_TOKEN_ADDRESS", + "wallet_address", + "wallet_address_2", + "private_key", + "private_key_2", + "EVEN_SPLIT_GROUP_POOL", + "ROYALTY_POLICY_LRP", + "mint_by_spg", +] diff --git a/tests/integration/test_integration_dispute.py b/tests/integration/test_integration_dispute.py index 9fa63db..d489697 100644 --- a/tests/integration/test_integration_dispute.py +++ b/tests/integration/test_integration_dispute.py @@ -5,15 +5,16 @@ from setup_for_integration import ( web3, - account, + account, account_2, story_client, story_client_2, generate_cid, approve, - wallet_address + wallet_address, ) + class TestDispute: @pytest.fixture(scope="module") def target_ip_id(self, story_client, story_client_2): @@ -25,71 +26,74 @@ def target_ip_id(self, story_client, story_client_2): is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=account.address + mint_fee_recipient=account.address, ) - - nft_contract = txData['nft_contract'] + + nft_contract = txData["nft_contract"] metadata_a = { - 'ip_metadata_uri': "test-uri-a", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-a")), - 'nft_metadata_uri': "test-nft-uri-a", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-a")) + "ip_metadata_uri": "test-uri-a", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-a")), + "nft_metadata_uri": "test-nft-uri-a", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-a") + ), } - + response = story_client_2.IPAsset.mint_and_register_ip( - spg_nft_contract=nft_contract, - ip_metadata=metadata_a + spg_nft_contract=nft_contract, ip_metadata=metadata_a ) - return response['ip_id'] + return response["ip_id"] @pytest.fixture(scope="module") def dispute_id(self, story_client, target_ip_id): cid = generate_cid() bond_amount = 1000000000000000000 # 1 ETH in wei - + response = story_client.Dispute.raise_dispute( target_ip_id=target_ip_id, target_tag="IMPROPER_REGISTRATION", cid=cid, - liveness=0x278d, # 30 days in seconds - bond=bond_amount + liveness=0x278D, # 30 days in seconds + bond=bond_amount, ) - - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - assert 'dispute_id' in response - assert isinstance(response['dispute_id'], int) - assert response['dispute_id'] > 0 - - return response['dispute_id'] - + + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + assert "dispute_id" in response + assert isinstance(response["dispute_id"], int) + assert response["dispute_id"] > 0 + + return response["dispute_id"] + def test_raise_dispute(self, story_client, dispute_id): """Test raising a dispute""" assert dispute_id is not None - - def test_counter_dispute(self, story_client_2, story_client, target_ip_id, dispute_id): + + def test_counter_dispute( + self, story_client_2, story_client, target_ip_id, dispute_id + ): """Test countering a dispute""" # Get the assertion ID from the dispute ID assertion_id = story_client_2.Dispute.dispute_id_to_assertion_id(dispute_id) - + # Generate a CID for counter evidence counter_evidence_cid = generate_cid() - + deposit_response_2 = story_client_2.WIP.deposit( - amount=Web3.to_wei(1, 'ether') #1 IP + amount=Web3.to_wei(1, "ether") # 1 IP ) # Counter the dispute assertion with story_client_2 (the IP owner) response = story_client_2.Dispute.dispute_assertion( ip_id=target_ip_id, assertion_id=assertion_id, - counter_evidence_cid=counter_evidence_cid + counter_evidence_cid=counter_evidence_cid, ) - + # Verify the response - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index 0aa10b3..a838e63 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -6,7 +6,7 @@ from setup_for_integration import ( web3, - account, + account, story_client, get_token_id, MockERC721, @@ -14,7 +14,7 @@ ZERO_ADDRESS, PIL_LICENSE_TEMPLATE, EVEN_SPLIT_GROUP_POOL, - ROYALTY_POLICY_LRP + ROYALTY_POLICY_LRP, ) # class TestGroupBasicOperations: @@ -22,11 +22,11 @@ # response = story_client.Group.register_group( # group_pool=EVEN_SPLIT_GROUP_POOL # ) - + # assert 'tx_hash' in response # assert isinstance(response['tx_hash'], str) # assert len(response['tx_hash']) > 0 - + # assert 'group_id' in response # assert isinstance(response['group_id'], str) # assert response['group_id'].startswith("0x") @@ -44,7 +44,7 @@ # mint_fee_recipient=account.address, # ) # return tx_data['nft_contract'] - + # @pytest.fixture(scope="module") # def ip_with_license(self, story_client, nft_collection): # # Create initial IP with license terms @@ -82,7 +82,7 @@ # } # }] # ) - + # ip_id = response['ip_id'] # license_terms_id = response['license_terms_ids'][0] @@ -96,7 +96,7 @@ # 'expectMinimumGroupRewardShare': 0, # 'expectGroupRewardPool': EVEN_SPLIT_GROUP_POOL # } - + # # Set licensing config # story_client.License.set_licensing_config( # ip_id=ip_id, @@ -104,12 +104,12 @@ # license_template=PIL_LICENSE_TEMPLATE, # licensing_config=licensing_config # ) - + # return { # 'ip_id': ip_id, # 'license_terms_id': license_terms_id # } - + # @pytest.fixture(scope="module") # def group_with_license(self, story_client, ip_with_license): # response = story_client.Group.register_group_and_attach_license( @@ -136,7 +136,7 @@ # assert 'group_id' in response # assert response['group_id'] is not None # return response['group_id'] - + # def test_register_group_and_attach_license(self, group_with_license): # assert group_with_license is not None @@ -159,11 +159,11 @@ # }], # max_allowed_reward_share=5 # ) - + # assert 'tx_hash' in response # assert isinstance(response['tx_hash'], str) # assert len(response['tx_hash']) > 0 - + # assert 'ip_id' in response # assert isinstance(response['ip_id'], str) # assert response['ip_id'].startswith("0x") @@ -218,12 +218,12 @@ # } # }] # ) - + # return { # 'ip_id': response['ip_id'], # 'license_terms_id': response['license_terms_ids'][0] # } - + # @pytest.fixture(scope="module") # def group_id(self, story_client, ip_with_license): # # Create a group with license attached @@ -245,10 +245,10 @@ # } # ) # return response['group_id'] - + # def test_register_ip_and_attach_license_and_add_to_group(self, story_client, group_id, ip_with_license): # token_id = get_token_id(MockERC721, story_client.web3, story_client.account) - + # response = story_client.Group.register_ip_and_attach_license_and_add_to_group( # group_id=group_id, # nft_contract=MockERC721, @@ -268,15 +268,15 @@ # } # }] # ) - + # assert 'tx_hash' in response # assert isinstance(response['tx_hash'], str) # assert len(response['tx_hash']) > 0 - + # assert 'ip_id' in response # assert isinstance(response['ip_id'], str) # assert response['ip_id'].startswith("0x") - + # def test_register_group_and_attach_license_and_add_ips(self, story_client, ip_with_license): # response = story_client.Group.register_group_and_attach_license_and_add_ips( # group_pool=EVEN_SPLIT_GROUP_POOL, @@ -296,15 +296,15 @@ # } # } # ) - + # assert 'tx_hash' in response # assert isinstance(response['tx_hash'], str) # assert len(response['tx_hash']) > 0 - + # assert 'group_id' in response # assert isinstance(response['group_id'], str) # assert response['group_id'].startswith("0x") - + # def test_fail_add_unregistered_ip_to_group(self, story_client, ip_with_license): # with pytest.raises(ValueError, match="Failed to register group and attach license and add IPs"): # story_client.Group.register_group_and_attach_license_and_add_ips( @@ -326,6 +326,7 @@ # } # ) + class TestCollectRoyaltyAndClaimReward: @pytest.fixture(scope="module") def nft_collection(self, story_client): @@ -338,106 +339,116 @@ def nft_collection(self, story_client): contract_uri="test-uri", mint_fee_recipient=account.address, ) - return tx_data['nft_contract'] - + return tx_data["nft_contract"] + @pytest.fixture(scope="module") def setup_royalty_collection(self, story_client, nft_collection): # Create license terms data - license_terms_data = [{ - 'terms': { - 'commercial_attribution': True, - 'commercial_rev_ceiling': 10, - 'commercial_rev_share': 10, - 'commercial_use': True, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'currency': MockERC20, - 'derivative_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_approval': False, - 'derivatives_attribution': True, - 'derivatives_reciprocal': True, - 'expiration': 0, - 'default_minting_fee': 0, - 'royalty_policy': ROYALTY_POLICY_LRP, - 'transferable': True, - 'uri': "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json" - }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 0, - 'hook_data': ZERO_ADDRESS, - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 10, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': EVEN_SPLIT_GROUP_POOL + license_terms_data = [ + { + "terms": { + "commercial_attribution": True, + "commercial_rev_ceiling": 10, + "commercial_rev_share": 10, + "commercial_use": True, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "currency": MockERC20, + "derivative_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_approval": False, + "derivatives_attribution": True, + "derivatives_reciprocal": True, + "expiration": 0, + "default_minting_fee": 0, + "royalty_policy": ROYALTY_POLICY_LRP, + "transferable": True, + "uri": "https://github.com/piplabs/pil-document/blob/ad67bb632a310d2557f8abcccd428e4c9c798db1/off-chain-terms/CommercialRemix.json", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 0, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 10, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": EVEN_SPLIT_GROUP_POOL, + }, } - }] + ] # Create unique metadata for each IP metadata_1 = { - 'ip_metadata_uri': "test-uri-1", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-1")), - 'nft_metadata_uri': "test-nft-uri-1", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-a")) + "ip_metadata_uri": "test-uri-1", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-1")), + "nft_metadata_uri": "test-nft-uri-1", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-a") + ), } - + metadata_2 = { - 'ip_metadata_uri': "test-uri-2", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-2")), - 'nft_metadata_uri': "test-nft-uri-2", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-2")) + "ip_metadata_uri": "test-uri-2", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-2")), + "nft_metadata_uri": "test-nft-uri-2", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-2") + ), } # Create two IPs result1 = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract=nft_collection, terms=copy.deepcopy(license_terms_data), - ip_metadata=metadata_1 + ip_metadata=metadata_1, ) result2 = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract=nft_collection, terms=copy.deepcopy(license_terms_data), - ip_metadata=metadata_2 + ip_metadata=metadata_2, ) - - ip_ids = [result1['ip_id'], result2['ip_id']] - license_terms_id = result1['license_terms_ids'][0] - + + ip_ids = [result1["ip_id"], result2["ip_id"]] + license_terms_id = result1["license_terms_ids"][0] + # Register group and add IPs result3 = story_client.Group.register_group_and_attach_license_and_add_ips( group_pool=EVEN_SPLIT_GROUP_POOL, max_allowed_reward_share=100, ip_ids=ip_ids, license_data={ - 'license_terms_id': license_terms_id, - 'license_template': PIL_LICENSE_TEMPLATE, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 0, - 'hook_data': ZERO_ADDRESS, - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 10, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS - } - } + "license_terms_id": license_terms_id, + "license_template": PIL_LICENSE_TEMPLATE, + "licensing_config": { + "is_set": True, + "minting_fee": 0, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 10, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, + }, ) - - group_ip_id = result3['group_id'] - + + group_ip_id = result3["group_id"] + # Create derivative IPs - Step 1: Mint and register result4 = story_client.IPAsset.mint_and_register_ip( spg_nft_contract=nft_collection, ip_metadata={ - 'ip_metadata_uri': "test-derivative-uri-4", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-derivative-metadata-hash-1")), - 'nft_metadata_uri': "test-derivative-nft-uri-4", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-derivative-nft-metadata-hash-1")) - } + "ip_metadata_uri": "test-derivative-uri-4", + "ip_metadata_hash": web3.to_hex( + web3.keccak(text="test-derivative-metadata-hash-1") + ), + "nft_metadata_uri": "test-derivative-nft-uri-4", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-derivative-nft-metadata-hash-1") + ), + }, ) - child_ip_id1 = result4['ip_id'] + child_ip_id1 = result4["ip_id"] # Step 2: Register as derivative story_client.IPAsset.register_derivative( @@ -446,20 +457,24 @@ def setup_royalty_collection(self, story_client, nft_collection): license_terms_ids=[license_terms_id], max_minting_fee=0, max_rts=10, - max_revenue_share=0 + max_revenue_share=0, ) # Create second derivative IP - Step 1: Mint and register result5 = story_client.IPAsset.mint_and_register_ip( spg_nft_contract=nft_collection, ip_metadata={ - 'ip_metadata_uri': "test-derivative-uri-5", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-derivative-metadata-hash-2")), - 'nft_metadata_uri': "test-derivative-nft-uri-5", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-derivative-nft-metadata-hash-2")) - } + "ip_metadata_uri": "test-derivative-uri-5", + "ip_metadata_hash": web3.to_hex( + web3.keccak(text="test-derivative-metadata-hash-2") + ), + "nft_metadata_uri": "test-derivative-nft-uri-5", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-derivative-nft-metadata-hash-2") + ), + }, ) - child_ip_id2 = result5['ip_id'] + child_ip_id2 = result5["ip_id"] # Step 2: Register as derivative story_client.IPAsset.register_derivative( @@ -468,66 +483,63 @@ def setup_royalty_collection(self, story_client, nft_collection): license_terms_ids=[license_terms_id], max_minting_fee=0, max_rts=10, - max_revenue_share=0 + max_revenue_share=0, ) - + # Pay royalties from child IPs to group IP story_client.Royalty.pay_royalty_on_behalf( receiver_ip_id=child_ip_id1, payer_ip_id=group_ip_id, token=MockERC20, - amount=100 + amount=100, ) - + story_client.Royalty.pay_royalty_on_behalf( receiver_ip_id=child_ip_id2, payer_ip_id=group_ip_id, token=MockERC20, - amount=100 + amount=100, ) - + # Transfer to vault story_client.Royalty.transfer_to_vault( royalty_policy="LRP", ip_id=child_ip_id1, ancestor_ip_id=group_ip_id, - token=MockERC20 + token=MockERC20, ) - + story_client.Royalty.transfer_to_vault( royalty_policy="LRP", ip_id=child_ip_id2, ancestor_ip_id=group_ip_id, - token=MockERC20 + token=MockERC20, ) - return { - 'group_ip_id': group_ip_id, - 'ip_ids': ip_ids - } - - def test_collect_and_distribute_group_royalties(self, story_client, setup_royalty_collection): - group_ip_id = setup_royalty_collection['group_ip_id'] - ip_ids = setup_royalty_collection['ip_ids'] - + return {"group_ip_id": group_ip_id, "ip_ids": ip_ids} + + def test_collect_and_distribute_group_royalties( + self, story_client, setup_royalty_collection + ): + group_ip_id = setup_royalty_collection["group_ip_id"] + ip_ids = setup_royalty_collection["ip_ids"] + response = story_client.Group.collect_and_distribute_group_royalties( - group_ip_id=group_ip_id, - currency_tokens=[MockERC20], - member_ip_ids=ip_ids + group_ip_id=group_ip_id, currency_tokens=[MockERC20], member_ip_ids=ip_ids ) print("response is ", response) - - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - - assert 'collected_royalties' in response - - assert len(response['collected_royalties']) > 0 - assert response['collected_royalties'][0]['amount'] == 20 - - assert 'royalties_distributed' in response - assert len(response['royalties_distributed']) == 2 - assert response['royalties_distributed'][0]['amount'] == 10 - assert response['royalties_distributed'][1]['amount'] == 10 + + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + + assert "collected_royalties" in response + + assert len(response["collected_royalties"]) > 0 + assert response["collected_royalties"][0]["amount"] == 20 + + assert "royalties_distributed" in response + assert len(response["royalties_distributed"]) == 2 + assert response["royalties_distributed"][0]["amount"] == 10 + assert response["royalties_distributed"][1]["amount"] == 10 diff --git a/tests/integration/test_integration_ip_account.py b/tests/integration/test_integration_ip_account.py index c74f3cf..1cc13a7 100644 --- a/tests/integration/test_integration_ip_account.py +++ b/tests/integration/test_integration_ip_account.py @@ -8,59 +8,62 @@ from setup_for_integration import ( web3, - account, + account, story_client, get_token_id, mint_tokens, get_block_timestamp, MockERC721, MockERC20, - private_key + private_key, ) + class TestBasicIPAccountOperations: """Basic IP Account operations like execute and nonce retrieval""" - + def test_execute(self, story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) data = story_client.IPAccount.access_controller_client.contract.encode_abi( - abi_element_identifier="setTransientPermission", - args=[response['ip_id'], - account.address, - "0x89630Ccf23277417FBdfd3076C702F5248267e78", - Web3.keccak(text="function setAll(address,string,bytes32,bytes32)")[:4], - 1] + abi_element_identifier="setTransientPermission", + args=[ + response["ip_id"], + account.address, + "0x89630Ccf23277417FBdfd3076C702F5248267e78", + Web3.keccak(text="function setAll(address,string,bytes32,bytes32)")[:4], + 1, + ], ) response = story_client.IPAccount.execute( to=story_client.IPAccount.access_controller_client.contract.address, value=0, - ip_id=response['ip_id'], - data=data + ip_id=response["ip_id"], + data=data, ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'tx_hash' in response, "Response does not contain 'tx_hash'." - assert response['tx_hash'] is not None, "'tx_hash' is None." - assert isinstance(response['tx_hash'], str), "'tx_hash' is not a string." - assert len(response['tx_hash']) > 0, "'tx_hash' is empty." + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert "tx_hash" in response, "Response does not contain 'tx_hash'." + assert response["tx_hash"] is not None, "'tx_hash' is None." + assert isinstance(response["tx_hash"], str), "'tx_hash' is not a string." + assert len(response["tx_hash"]) > 0, "'tx_hash' is empty." def test_get_ip_account_nonce(self, story_client): """Test getting IP Account nonce.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] - + ip_id = register_response["ip_id"] + state = story_client.IPAccount.get_ip_account_nonce(ip_id) - + assert state is not None assert isinstance(state, bytes) @@ -68,45 +71,44 @@ def test_execute_with_encoded_data(self, story_client): """Test execute with pre-encoded function data.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] + ip_id = register_response["ip_id"] data = story_client.IPAccount.access_controller_client.contract.encode_abi( - abi_element_identifier="setTransientPermission", + abi_element_identifier="setTransientPermission", args=[ ip_id, - account.address, + account.address, "0x89630Ccf23277417FBdfd3076C702F5248267e78", Web3.keccak(text="function execute(address,uint256,bytes,uint8)")[:4], - 1 - ] + 1, + ], ) response = story_client.IPAccount.execute( to=story_client.IPAccount.access_controller_client.contract.address, value=0, ip_id=ip_id, - data=data + data=data, ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + class TestSignatureOperations: """Tests for operations involving signatures""" - + def test_execute_with_sig(self, story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = response['ip_id'] + ip_id = response["ip_id"] deadline = get_block_timestamp(web3) + 100 state = story_client.IPAccount.get_ip_account_nonce(ip_id) @@ -117,8 +119,8 @@ def test_execute_with_sig(self, story_client): account.address, "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", Web3.keccak(text="function setAll(address,string,bytes32,bytes32)")[:4], - 1 - ] + 1, + ], ) execute_data = story_client.IPAccount.ip_account_client.contract.encode_abi( @@ -126,15 +128,12 @@ def test_execute_with_sig(self, story_client): args=[ story_client.IPAccount.access_controller_client.contract.address, 0, - core_data - ] + core_data, + ], ) expected_state = Web3.keccak( - encode( - ["bytes32", "bytes"], - [state, Web3.to_bytes(hexstr=execute_data)] - ) + encode(["bytes32", "bytes"], [state, Web3.to_bytes(hexstr=execute_data)]) ) domain_data = { @@ -172,22 +171,21 @@ def test_execute_with_sig(self, story_client): data=core_data, signer=account.address, deadline=deadline, - signature=signed_message.signature + signature=signed_message.signature, ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 def test_execute_with_sig_multiple_permissions(self, story_client): """Test execute_with_sig setting multiple permissions.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] + ip_id = register_response["ip_id"] deadline = get_block_timestamp(web3) + 100 state = story_client.IPAccount.get_ip_account_nonce(ip_id) @@ -196,9 +194,9 @@ def test_execute_with_sig_multiple_permissions(self, story_client): function_signatures = [ "function setAll(address,string,bytes32,bytes32)", "function execute(address,uint256,bytes,uint8)", - "function registerDerivative(address,address[],uint256[],address,bytes)" + "function registerDerivative(address,address[],uint256[],address,bytes)", ] - + # Create individual permission data and combine them calls_data = [] for func_sig in function_signatures: @@ -209,15 +207,15 @@ def test_execute_with_sig_multiple_permissions(self, story_client): account.address, "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", Web3.keccak(text=func_sig)[:4], - 1 - ] + 1, + ], ) - if data.startswith('0x'): + if data.startswith("0x"): data = data[2:] calls_data.append(data) # Combine all encoded data - combined_data = '0x' + ''.join(calls_data) + combined_data = "0x" + "".join(calls_data) # Create the execute data that would be signed execute_data = story_client.IPAccount.ip_account_client.contract.encode_abi( @@ -225,15 +223,12 @@ def test_execute_with_sig_multiple_permissions(self, story_client): args=[ story_client.IPAccount.access_controller_client.contract.address, 0, - combined_data - ] + combined_data, + ], ) expected_state = Web3.keccak( - encode( - ["bytes32", "bytes"], - [state, Web3.to_bytes(hexstr=execute_data)] - ) + encode(["bytes32", "bytes"], [state, Web3.to_bytes(hexstr=execute_data)]) ) domain_data = { @@ -271,90 +266,83 @@ def test_execute_with_sig_multiple_permissions(self, story_client): data=combined_data, signer=account.address, deadline=deadline, - signature=signed_message.signature + signature=signed_message.signature, ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + class TestErrorCases: """Tests for error cases and validation""" - + def test_execute_invalid_address(self, story_client): """Test execute with invalid address should raise error.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] + ip_id = register_response["ip_id"] data = "0x" invalid_address = "0xinvalid" - + with pytest.raises(ValueError) as exc_info: story_client.IPAccount.execute( - to=invalid_address, - value=0, - ip_id=ip_id, - data=data + to=invalid_address, value=0, ip_id=ip_id, data=data ) - + assert "is not a valid address" in str(exc_info.value) def test_execute_unregistered_ip(self, story_client): """Test execute with unregistered IP should raise error.""" unregistered_ip = "0x1234567890123456789012345678901234567890" data = "0x" - + with pytest.raises(ValueError) as exc_info: story_client.IPAccount.execute( to=story_client.IPAccount.access_controller_client.contract.address, value=0, ip_id=unregistered_ip, - data=data + data=data, ) - + assert "is not registered" in str(exc_info.value) def test_execute_with_sig_wrong_signer(self, story_client): """Test execute_with_sig with a valid signature but wrong signer address.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] + ip_id = register_response["ip_id"] deadline = get_block_timestamp(web3) + 100 - state = story_client.IPAccount.get_ip_account_nonce(ip_id) + state = story_client.IPAccount.get_ip_account_nonce(ip_id) data = "0x" - + execute_data = story_client.IPAccount.ip_account_client.contract.encode_abi( abi_element_identifier="execute", args=[ story_client.IPAccount.access_controller_client.contract.address, 0, - data - ] + data, + ], ) - + expected_state = Web3.keccak( - encode( - ["bytes32", "bytes"], - [state, Web3.to_bytes(hexstr=execute_data)] - ) + encode(["bytes32", "bytes"], [state, Web3.to_bytes(hexstr=execute_data)]) ) - + domain_data = { "name": "Story Protocol IP Account", "version": "1", "chainId": 1315, "verifyingContract": ip_id, } - + message_types = { "Execute": [ {"name": "to", "type": "address"}, @@ -364,7 +352,7 @@ def test_execute_with_sig_wrong_signer(self, story_client): {"name": "deadline", "type": "uint256"}, ], } - + message_data = { "to": story_client.IPAccount.access_controller_client.contract.address, "value": 0, @@ -372,11 +360,11 @@ def test_execute_with_sig_wrong_signer(self, story_client): "nonce": expected_state, "deadline": deadline, } - + signable_message = encode_typed_data(domain_data, message_types, message_data) signed_message = Account.sign_message(signable_message, private_key) wrong_signer = "0x1234567890123456789012345678901234567890" - + with pytest.raises(Exception) as exc_info: story_client.IPAccount.execute_with_sig( ip_id=ip_id, @@ -385,33 +373,37 @@ def test_execute_with_sig_wrong_signer(self, story_client): data=data, signer=wrong_signer, # Wrong signer address deadline=deadline, - signature=signed_message.signature + signature=signed_message.signature, ) - error_hex = '0x3fd60002' - assert error_hex in str(exc_info.value), f"Expected error code {error_hex} for wrong signer" + error_hex = "0x3fd60002" + assert error_hex in str( + exc_info.value + ), f"Expected error code {error_hex} for wrong signer" + class TestSetIpMetadata: """Tests for setting IP metadata""" - + def test_set_ip_metadata(self, story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) response = story_client.IPAccount.set_ip_metadata( - ip_id=response['ip_id'], + ip_id=response["ip_id"], metadata_uri="https://example.com", - metadata_hash=web3.to_hex(web3.keccak(text="test-metadata-hash")) + metadata_hash=web3.to_hex(web3.keccak(text="test-metadata-hash")), ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'tx_hash' in response, "Response does not contain 'tx_hash'." - assert response['tx_hash'] is not None, "'tx_hash' is None." - assert isinstance(response['tx_hash'], str), "'tx_hash' is not a string." - assert len(response['tx_hash']) > 0, "'tx_hash' is empty." + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert "tx_hash" in response, "Response does not contain 'tx_hash'." + assert response["tx_hash"] is not None, "'tx_hash' is None." + assert isinstance(response["tx_hash"], str), "'tx_hash' is not a string." + assert len(response["tx_hash"]) > 0, "'tx_hash' is empty." class Testtransfer_erc20: @@ -421,21 +413,20 @@ def test_transfer_erc20(self, story_client): """Test transferring ERC20 tokens""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = response['ip_id'] + ip_id = response["ip_id"] # 1. Query token balance of ipId and wallet before - initial_erc20_balance_of_ip_id = story_client.Royalty.mock_erc20_client.balanceOf( - account=ip_id - ) - initial_erc20_balance_of_wallet = story_client.Royalty.mock_erc20_client.balanceOf( - account=story_client.account.address + initial_erc20_balance_of_ip_id = ( + story_client.Royalty.mock_erc20_client.balanceOf(account=ip_id) ) - initial_wip_balance_of_ip_id = story_client.WIP.balance_of( - address=ip_id + initial_erc20_balance_of_wallet = ( + story_client.Royalty.mock_erc20_client.balanceOf( + account=story_client.account.address + ) ) + initial_wip_balance_of_ip_id = story_client.WIP.balance_of(address=ip_id) initial_wip_balance_of_wallet = story_client.WIP.balance_of( address=story_client.account.address ) @@ -447,20 +438,15 @@ def test_transfer_erc20(self, story_client): web3=story_client.web3, account=story_client.account, to_address=ip_id, - amount=amount_to_mint + amount=amount_to_mint, ) - + # 3. Transfer WIP to the IP account # First deposit (wrap) IP to WIP - deposit_response = story_client.WIP.deposit( - amount=1 - ) - + deposit_response = story_client.WIP.deposit(amount=1) + # Then transfer WIP to the IP account - response = story_client.WIP.transfer( - to=ip_id, - amount=1 - ) + response = story_client.WIP.transfer(to=ip_id, amount=1) # 4. Transfer tokens from IP account to wallet address response = story_client.IPAccount.transfer_erc20( @@ -469,39 +455,41 @@ def test_transfer_erc20(self, story_client): { "address": story_client.WIP.wip_client.contract.address, "target": story_client.account.address, - "amount": 1 + "amount": 1, }, { "address": MockERC20, "target": story_client.account.address, - "amount": 1000000 # Equivalent to 0.001 ether + "amount": 1000000, # Equivalent to 0.001 ether }, { "address": MockERC20, "target": story_client.account.address, - "amount": 1000000 # Equivalent to 0.001 ether - } - ] + "amount": 1000000, # Equivalent to 0.001 ether + }, + ], ) - + # 5. Query token balance of ipId and wallet address after transfer final_erc20_balance_of_ip_id = story_client.Royalty.mock_erc20_client.balanceOf( account=ip_id ) - final_wip_balance_of_ip_id = story_client.WIP.balance_of( - address=ip_id - ) - final_erc20_balance_of_wallet = story_client.Royalty.mock_erc20_client.balanceOf( - account=story_client.account.address + final_wip_balance_of_ip_id = story_client.WIP.balance_of(address=ip_id) + final_erc20_balance_of_wallet = ( + story_client.Royalty.mock_erc20_client.balanceOf( + account=story_client.account.address + ) ) final_wip_balance_of_wallet = story_client.WIP.balance_of( address=story_client.account.address ) - assert isinstance(response['tx_hash'], str) and response['tx_hash'] != "" + assert isinstance(response["tx_hash"], str) and response["tx_hash"] != "" assert final_erc20_balance_of_ip_id == initial_erc20_balance_of_ip_id assert final_wip_balance_of_ip_id == initial_wip_balance_of_ip_id - assert final_erc20_balance_of_wallet == initial_erc20_balance_of_wallet + 2000000 + assert ( + final_erc20_balance_of_wallet == initial_erc20_balance_of_wallet + 2000000 + ) assert final_wip_balance_of_wallet == initial_wip_balance_of_wallet + 1 @pytest.mark.skip(reason="contract allows empty calls") @@ -509,27 +497,24 @@ def test_transfer_erc20_empty_tokens(self, story_client): """Test transfer_erc20 with empty tokens list.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] - + ip_id = register_response["ip_id"] + # Try to transfer with empty tokens list with pytest.raises(Exception) as exc_info: story_client.IPAccount.transfer_erc20( - ip_id=ip_id, - tokens=[] # Empty tokens list - ) - + ip_id=ip_id, tokens=[] # Empty tokens list + ) + def test_transfer_erc20_invalid_token_params(self, story_client): """Test transfer_erc20 with invalid token parameters.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) register_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - ip_id = register_response['ip_id'] - + ip_id = register_response["ip_id"] + with pytest.raises(ValueError) as exc_info: story_client.IPAccount.transfer_erc20( ip_id=ip_id, @@ -537,9 +522,10 @@ def test_transfer_erc20_invalid_token_params(self, story_client): { # Missing 'address' "target": story_client.account.address, - "amount": 1000000 + "amount": 1000000, } - ] + ], ) - assert "must include" in str(exc_info.value), "Error should mention missing parameter" - \ No newline at end of file + assert "must include" in str( + exc_info.value + ), "Error should mention missing parameter" diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 3b3143b..b92def1 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -5,7 +5,7 @@ from setup_for_integration import ( web3, - account, + account, story_client, get_token_id, MockERC721, @@ -14,26 +14,26 @@ ROYALTY_POLICY, PIL_LICENSE_TEMPLATE, WIP_TOKEN_ADDRESS, - mint_by_spg + mint_by_spg, ) + class TestIPAssetRegistration: @pytest.fixture(scope="module") def child_ip_id(self, story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) assert response is not None - assert 'ip_id' in response - assert response['ip_id'] is not None - return response['ip_id'] + assert "ip_id" in response + assert response["ip_id"] is not None + return response["ip_id"] def test_register_ip_asset(self, story_client, child_ip_id): assert child_ip_id is not None @@ -41,26 +41,29 @@ def test_register_ip_asset(self, story_client, child_ip_id): def test_register_ip_asset_with_metadata(self, story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) metadata = { - 'ip_metadata_uri': "test-uri", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash")), - 'nft_metadata_uri': "test-nft-uri", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) + "ip_metadata_uri": "test-uri", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash")), + "nft_metadata_uri": "test-nft-uri", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash") + ), } response = story_client.IPAsset.register( nft_contract=MockERC721, token_id=token_id, ip_metadata=metadata, - deadline=1000 + deadline=1000, ) - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) assert response is not None - assert 'ip_id' in response - assert response['ip_id'] is not None - assert isinstance(response['ip_id'], str) + assert "ip_id" in response + assert response["ip_id"] is not None + assert isinstance(response["ip_id"], str) + class TestIPAssetDerivatives: @pytest.fixture(scope="module") @@ -68,82 +71,88 @@ def child_ip_id(self, story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) assert response is not None - assert 'ip_id' in response - assert response['ip_id'] is not None - return response['ip_id'] - + assert "ip_id" in response + assert response["ip_id"] is not None + return response["ip_id"] + @pytest.fixture(scope="module") def non_commercial_license(self, story_client): - license_register_response = story_client.License.register_non_com_social_remixing_pil() - no_commercial_license_terms_id = license_register_response['license_terms_id'] + license_register_response = ( + story_client.License.register_non_com_social_remixing_pil() + ) + no_commercial_license_terms_id = license_register_response["license_terms_id"] return no_commercial_license_terms_id @pytest.fixture(scope="module") def parent_ip_id(self, story_client, non_commercial_license): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - attach_license_response = story_client.License.attach_license_terms(response['ip_id'], PIL_LICENSE_TEMPLATE, non_commercial_license) + attach_license_response = story_client.License.attach_license_terms( + response["ip_id"], PIL_LICENSE_TEMPLATE, non_commercial_license + ) - return response['ip_id'] + return response["ip_id"] - def test_register_derivative(self, story_client, child_ip_id, parent_ip_id, non_commercial_license): + def test_register_derivative( + self, story_client, child_ip_id, parent_ip_id, non_commercial_license + ): response = story_client.IPAsset.register_derivative( child_ip_id=child_ip_id, parent_ip_ids=[parent_ip_id], license_terms_ids=[non_commercial_license], max_minting_fee=0, - max_rts=5 * 10 ** 6, + max_rts=5 * 10**6, max_revenue_share=0, ) - - assert response is not None - assert 'tx_hash' in response - assert response['tx_hash'] is not None - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - def test_register_derivative_with_license_tokens(self, story_client, parent_ip_id, non_commercial_license): + assert response is not None + assert "tx_hash" in response + assert response["tx_hash"] is not None + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + + def test_register_derivative_with_license_tokens( + self, story_client, parent_ip_id, non_commercial_license + ): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) child_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - child_ip_id = child_response['ip_id'] + child_ip_id = child_response["ip_id"] license_token_response = story_client.License.mint_license_tokens( - licensor_ip_id=parent_ip_id, - license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=non_commercial_license, + licensor_ip_id=parent_ip_id, + license_template=PIL_LICENSE_TEMPLATE, + license_terms_id=non_commercial_license, amount=1, receiver=account.address, max_minting_fee=0, - max_revenue_share=1 + max_revenue_share=1, ) - license_token_ids = license_token_response['license_token_ids'] + license_token_ids = license_token_response["license_token_ids"] response = story_client.IPAsset.register_derivative_with_license_tokens( child_ip_id=child_ip_id, license_token_ids=license_token_ids, - max_rts=5 * 10 ** 6 + max_rts=5 * 10**6, ) - + assert response is not None - assert 'tx_hash' in response - assert response['tx_hash'] is not None - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert response["tx_hash"] is not None + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + class TestIPAssetMinting: @pytest.fixture(scope="module") @@ -157,79 +166,85 @@ def nft_collection(self, story_client): contract_uri="test-uri", mint_fee_recipient=account.address, ) - return tx_data['nft_contract'] + return tx_data["nft_contract"] def test_mint_register_attach_terms(self, story_client, nft_collection): metadata = { - 'ip_metadata_uri': "test-uri", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash")), - 'nft_metadata_uri': "test-nft-uri", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) + "ip_metadata_uri": "test-uri", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash")), + "nft_metadata_uri": "test-nft-uri", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash") + ), } response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract=nft_collection, - terms=[{ - 'terms': { - 'transferable': True, - 'royalty_policy': ROYALTY_POLICY, - 'default_minting_fee': 1, - 'expiration': 0, - 'commercial_use': True, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'commercial_rev_share': 90, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_attribution': True, - 'derivatives_approval': False, - 'derivatives_reciprocal': True, - 'derivative_rev_ceiling': 0, - 'currency': MockERC20, - 'uri': "" - }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 1, - 'hook_data': "", - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 90, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS + terms=[ + { + "terms": { + "transferable": True, + "royalty_policy": ROYALTY_POLICY, + "default_minting_fee": 1, + "expiration": 0, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 90, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": MockERC20, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 1, + "hook_data": "", + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 90, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, } - }], - ip_metadata=metadata + ], + ip_metadata=metadata, ) - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) - assert 'ip_id' in response - assert isinstance(response['ip_id'], str) - assert response['ip_id'].startswith("0x") + assert "ip_id" in response + assert isinstance(response["ip_id"], str) + assert response["ip_id"].startswith("0x") - assert 'token_id' in response - assert isinstance(response['token_id'], int) + assert "token_id" in response + assert isinstance(response["token_id"], int) - assert 'license_terms_ids' in response - assert isinstance(response['license_terms_ids'], list) - assert all(isinstance(id, int) for id in response['license_terms_ids']) + assert "license_terms_ids" in response + assert isinstance(response["license_terms_ids"], list) + assert all(isinstance(id, int) for id in response["license_terms_ids"]) def test_mint_register_ip(self, story_client, nft_collection): metadata = { - 'ip_metadata_uri': "test-uri", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash")), - 'nft_metadata_uri': "test-nft-uri", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) + "ip_metadata_uri": "test-uri", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash")), + "nft_metadata_uri": "test-nft-uri", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash") + ), } - + response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=nft_collection, - ip_metadata=metadata + spg_nft_contract=nft_collection, ip_metadata=metadata ) + class TestSPGNFTOperations: @pytest.fixture(scope="module") def nft_collection(self, story_client): @@ -242,49 +257,52 @@ def nft_collection(self, story_client): contract_uri="test-uri", mint_fee_recipient=account.address, ) - return tx_data['nft_contract'] + return tx_data["nft_contract"] @pytest.fixture(scope="module") def parent_ip_and_license_terms(self, story_client, nft_collection): response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract=nft_collection, - terms=[{ - 'terms': { - 'transferable': True, - 'royalty_policy': ROYALTY_POLICY, - 'default_minting_fee': 0, - 'expiration': 0, - 'commercial_use': True, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'commercial_rev_share': 90, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_attribution': True, - 'derivatives_approval': False, - 'derivatives_reciprocal': True, - 'derivative_rev_ceiling': 0, - 'currency': MockERC20, - 'uri': "" - }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 0, - 'hook_data': ZERO_ADDRESS, - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS + terms=[ + { + "terms": { + "transferable": True, + "royalty_policy": ROYALTY_POLICY, + "default_minting_fee": 0, + "expiration": 0, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 90, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": MockERC20, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 0, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, } - }] + ], ) return { - 'parent_ip_id': response['ip_id'], - 'license_terms_id': response['license_terms_ids'][0] + "parent_ip_id": response["ip_id"], + "license_terms_id": response["license_terms_ids"][0], } + # def test_register_ip_asset_with_metadata(self, story_client, nft_collection): # token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account, "test-metadata") @@ -305,7 +323,7 @@ def parent_ip_and_license_terms(self, story_client, nft_collection): # def test_register_derivative_ip(self, story_client, parent_ip_id, license_terms_id): # token_child_id = mint_by_spg(MockERC721, story_client.web3, story_client.account) - + # result = story_client.IPAsset.register_derivative_ip( # nft_contract=MockERC721, # token_id=token_child_id, @@ -323,7 +341,9 @@ def parent_ip_and_license_terms(self, story_client, nft_collection): # assert isinstance(result['tx_hash'], str) and result['tx_hash'] # assert isinstance(result['ip_id'], str) and result['ip_id'] - def test_register_ip_and_attach_pil_terms(self, story_client, nft_collection, parent_ip_and_license_terms): + def test_register_ip_and_attach_pil_terms( + self, story_client, nft_collection, parent_ip_and_license_terms + ): token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account) # Register IP and attach PIL terms @@ -333,76 +353,81 @@ def test_register_ip_and_attach_pil_terms(self, story_client, nft_collection, pa deadline=1000, license_terms_data=[ { - 'terms': { - 'transferable': True, - 'royalty_policy': ZERO_ADDRESS, - 'default_minting_fee': 0, - 'expiration': 0, - 'commercial_use': False, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_attribution': True, - 'derivatives_approval': False, - 'derivatives_reciprocal': True, - 'derivative_rev_ceiling': 0, - 'currency': WIP_TOKEN_ADDRESS, - 'uri': '', + "terms": { + "transferable": True, + "royalty_policy": ZERO_ADDRESS, + "default_minting_fee": 0, + "expiration": 0, + "commercial_use": False, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 0, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": WIP_TOKEN_ADDRESS, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 0, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 0, - 'licensing_hook': ZERO_ADDRESS, - 'hook_data': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS, - } }, { - 'terms': { - 'transferable': True, - 'royalty_policy': ROYALTY_POLICY, - 'default_minting_fee': 10000, - 'expiration': 1000, - 'commercial_use': True, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_attribution': True, - 'derivatives_approval': False, - 'derivatives_reciprocal': True, - 'derivative_rev_ceiling': 0, - 'currency': WIP_TOKEN_ADDRESS, - 'uri': 'test case', + "terms": { + "transferable": True, + "royalty_policy": ROYALTY_POLICY, + "default_minting_fee": 10000, + "expiration": 1000, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 0, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": WIP_TOKEN_ADDRESS, + "uri": "test case", }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 10000, - 'licensing_hook': ZERO_ADDRESS, - 'hook_data': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS, - } - } - ] + "licensing_config": { + "is_set": True, + "minting_fee": 10000, + "licensing_hook": ZERO_ADDRESS, + "hook_data": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, + }, + ], + ) + + assert isinstance(result["tx_hash"], str) and result["tx_hash"] + assert isinstance(result["ip_id"], str) and result["ip_id"] + assert ( + isinstance(result["license_terms_ids"], list) + and result["license_terms_ids"] ) - assert isinstance(result['tx_hash'], str) and result['tx_hash'] - assert isinstance(result['ip_id'], str) and result['ip_id'] - assert isinstance(result['license_terms_ids'], list) and result['license_terms_ids'] - + # Add this test class to your existing test_integration_ip_asset.py file + class TestIPAssetMint: @pytest.fixture(scope="module") def nft_collection(self, story_client): @@ -415,26 +440,26 @@ def nft_collection(self, story_client): contract_uri="test-mint-uri", mint_fee_recipient=account.address, ) - return tx_data['nft_contract'] + return tx_data["nft_contract"] def test_mint_basic(self, story_client, nft_collection): """Test basic minting functionality""" metadata_uri = "https://example.com/metadata/1.json" metadata_hash = web3.keccak(text="test-metadata-content") - + response = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=account.address, metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=False + allow_duplicates=False, ) - + assert response is not None assert isinstance(response, str) assert len(response) > 0 assert response.startswith("0x") - + # Wait for transaction confirmation to verify it was successful receipt = story_client.web3.eth.wait_for_transaction_receipt(response) assert receipt.status == 1 @@ -443,33 +468,33 @@ def test_mint_with_duplicates_allowed(self, story_client, nft_collection): """Test minting with duplicate metadata allowed""" metadata_uri = "https://example.com/metadata/duplicate.json" metadata_hash = web3.keccak(text="duplicate-metadata-content") - + # First mint response1 = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=account.address, metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=True + allow_duplicates=True, ) - + assert response1 is not None receipt1 = story_client.web3.eth.wait_for_transaction_receipt(response1) assert receipt1.status == 1 - + # Second mint with same metadata (should succeed with allow_duplicates=True) response2 = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=account.address, metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=True + allow_duplicates=True, ) - + assert response2 is not None receipt2 = story_client.web3.eth.wait_for_transaction_receipt(response2) assert receipt2.status == 1 - + # Verify different transaction hashes assert response1 != response2 @@ -478,18 +503,18 @@ def test_mint_to_different_address(self, story_client, nft_collection): # Create a different recipient address for testing different_account = story_client.web3.eth.account.create() recipient_address = different_account.address - + metadata_uri = "https://example.com/metadata/different-recipient.json" metadata_hash = web3.keccak(text="different-recipient-metadata") - + response = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=recipient_address, metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=False + allow_duplicates=False, ) - + assert response is not None receipt = story_client.web3.eth.wait_for_transaction_receipt(response) assert receipt.status == 1 @@ -499,33 +524,27 @@ def test_mint_with_various_metadata_formats(self, story_client, nft_collection): test_cases = [ { "uri": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG", - "content": "ipfs-metadata" + "content": "ipfs-metadata", }, { "uri": "https://gateway.pinata.cloud/ipfs/QmTest123", - "content": "pinata-gateway-metadata" + "content": "pinata-gateway-metadata", }, - { - "uri": "ar://abc123def456", - "content": "arweave-metadata" - }, - { - "uri": "", # Empty URI - "content": "empty-uri-metadata" - } + {"uri": "ar://abc123def456", "content": "arweave-metadata"}, + {"uri": "", "content": "empty-uri-metadata"}, # Empty URI ] - + for i, test_case in enumerate(test_cases): metadata_hash = web3.keccak(text=f"{test_case['content']}-{i}") - + response = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=account.address, metadata_uri=test_case["uri"], metadata_hash=metadata_hash, - allow_duplicates=False + allow_duplicates=False, ) - + assert response is not None receipt = story_client.web3.eth.wait_for_transaction_receipt(response) assert receipt.status == 1 @@ -533,26 +552,25 @@ def test_mint_with_various_metadata_formats(self, story_client, nft_collection): def test_mint_with_zero_hash(self, story_client, nft_collection): """Test minting with zero hash""" metadata_uri = "https://example.com/metadata/zero-hash.json" - zero_hash = b'\x00' * 32 # 32 bytes of zeros - + zero_hash = b"\x00" * 32 # 32 bytes of zeros + response = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=account.address, metadata_uri=metadata_uri, metadata_hash=zero_hash, - allow_duplicates=False + allow_duplicates=False, ) - + assert response is not None receipt = story_client.web3.eth.wait_for_transaction_receipt(response) assert receipt.status == 1 - def test_mint_error_cases(self, story_client, nft_collection): """Test various error cases for minting""" metadata_uri = "https://example.com/metadata/error-test.json" metadata_hash = web3.keccak(text="error-test-metadata") - + # Test with invalid address format (not a valid hex address) with pytest.raises(Exception): story_client.IPAsset.mint( @@ -560,9 +578,9 @@ def test_mint_error_cases(self, story_client, nft_collection): to_address="invalid-address-format", metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=False + allow_duplicates=False, ) - + # Test with invalid metadata hash format (wrong length - too short) with pytest.raises(Exception): story_client.IPAsset.mint( @@ -570,9 +588,9 @@ def test_mint_error_cases(self, story_client, nft_collection): to_address=account.address, metadata_uri=metadata_uri, metadata_hash=b"too_short", # bytes32 should be 32 bytes - allow_duplicates=False + allow_duplicates=False, ) - + # Test with invalid metadata hash format (wrong length - too long) with pytest.raises(Exception): story_client.IPAsset.mint( @@ -580,27 +598,29 @@ def test_mint_error_cases(self, story_client, nft_collection): to_address=account.address, metadata_uri=metadata_uri, metadata_hash=b"this_is_way_too_long_for_bytes32_format_and_should_fail", - allow_duplicates=False + allow_duplicates=False, ) - def test_mint_with_existing_metadata_hash_no_duplicates(self, story_client, nft_collection): + def test_mint_with_existing_metadata_hash_no_duplicates( + self, story_client, nft_collection + ): """Test that minting with existing metadata hash fails when duplicates not allowed""" metadata_uri = "https://example.com/metadata/no-duplicates.json" metadata_hash = web3.keccak(text="no-duplicates-metadata") - + # First mint should succeed response1 = story_client.IPAsset.mint( nft_contract=nft_collection, to_address=account.address, metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=False + allow_duplicates=False, ) - + assert response1 is not None receipt1 = story_client.web3.eth.wait_for_transaction_receipt(response1) assert receipt1.status == 1 - + # Second mint with same metadata hash should fail when allow_duplicates=False with pytest.raises(Exception): story_client.IPAsset.mint( @@ -608,5 +628,5 @@ def test_mint_with_existing_metadata_hash_no_duplicates(self, story_client, nft_ to_address=account.address, metadata_uri=metadata_uri, metadata_hash=metadata_hash, - allow_duplicates=False + allow_duplicates=False, ) diff --git a/tests/integration/test_integration_license.py b/tests/integration/test_integration_license.py index 935c56c..5f1c104 100644 --- a/tests/integration/test_integration_license.py +++ b/tests/integration/test_integration_license.py @@ -5,7 +5,7 @@ from setup_for_integration import ( web3, - account, + account, story_client, get_token_id, mint_tokens, @@ -15,18 +15,23 @@ ZERO_ADDRESS, ROYALTY_POLICY, ROYALTY_MODULE, - PIL_LICENSE_TEMPLATE + PIL_LICENSE_TEMPLATE, ) + def test_register_pil_terms(story_client): response = story_client.License.register_pil_terms( transferable=False, - royalty_policy=story_client.web3.to_checksum_address("0x0000000000000000000000000000000000000000"), + royalty_policy=story_client.web3.to_checksum_address( + "0x0000000000000000000000000000000000000000" + ), default_minting_fee=0, expiration=0, commercial_use=False, commercial_attribution=False, - commercializer_checker=story_client.web3.to_checksum_address("0x0000000000000000000000000000000000000000"), + commercializer_checker=story_client.web3.to_checksum_address( + "0x0000000000000000000000000000000000000000" + ), commercializer_checker_data="0x", commercial_rev_share=0, commercial_rev_ceiling=0, @@ -40,114 +45,143 @@ def test_register_pil_terms(story_client): ) assert response is not None - assert 'license_terms_id' in response - assert response['license_terms_id'] is not None - assert isinstance(response['license_terms_id'], int) + assert "license_terms_id" in response + assert response["license_terms_id"] is not None + assert isinstance(response["license_terms_id"], int) + def test_register_non_com_social_remixing_pil(story_client): response = story_client.License.register_non_com_social_remixing_pil() assert response is not None - assert 'license_terms_id' in response - assert response['license_terms_id'] is not None - assert isinstance(response['license_terms_id'], int) + assert "license_terms_id" in response + assert response["license_terms_id"] is not None + assert isinstance(response["license_terms_id"], int) + @pytest.fixture(scope="module") def register_commercial_use_pil(story_client): response = story_client.License.register_commercial_use_pil( - default_minting_fee=11, - currency=MockERC20, - royalty_policy=ROYALTY_POLICY + default_minting_fee=11, currency=MockERC20, royalty_policy=ROYALTY_POLICY ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'license_terms_id' in response, "Response does not contain 'license_terms_id'." - assert response['license_terms_id'] is not None, "'license_terms_id' is None." - assert isinstance(response['license_terms_id'], int), "'license_terms_id' is not an integer." + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert ( + "license_terms_id" in response + ), "Response does not contain 'license_terms_id'." + assert response["license_terms_id"] is not None, "'license_terms_id' is None." + assert isinstance( + response["license_terms_id"], int + ), "'license_terms_id' is not an integer." + + return response["license_terms_id"] + - return response['license_terms_id'] - def test_register_commercial_use_pil(story_client, register_commercial_use_pil): assert register_commercial_use_pil is not None + @pytest.fixture(scope="module") def register_commercial_remix_pil(story_client): response = story_client.License.register_commercial_remix_pil( default_minting_fee=1, currency=MockERC20, commercial_rev_share=100, - royalty_policy=ROYALTY_POLICY + royalty_policy=ROYALTY_POLICY, ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'license_terms_id' in response, "Response does not contain 'license_terms_id'." - assert response['license_terms_id'] is not None, "'license_terms_id' is None." - assert isinstance(response['license_terms_id'], int), "'license_terms_id' is not an integer." + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert ( + "license_terms_id" in response + ), "Response does not contain 'license_terms_id'." + assert response["license_terms_id"] is not None, "'license_terms_id' is None." + assert isinstance( + response["license_terms_id"], int + ), "'license_terms_id' is not an integer." + + return response["license_terms_id"] - return response['license_terms_id'] def test_register_commercial_remix_pil(story_client, register_commercial_remix_pil): assert register_commercial_remix_pil is not None + @pytest.fixture(scope="module") def ip_id(story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) - response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id - ) + response = story_client.IPAsset.register(nft_contract=MockERC721, token_id=token_id) token_ids = mint_tokens( - erc20_contract_address=MockERC20, - web3=web3, - account=account, - to_address=account.address, - amount=100000 * 10 ** 6 + erc20_contract_address=MockERC20, + web3=web3, + account=account, + to_address=account.address, + amount=100000 * 10**6, ) - + receipt = approve( - erc20_contract_address=MockERC20, - web3=web3, - account=account, - spender_address=ROYALTY_MODULE, - amount=100000 * 10 ** 6) + erc20_contract_address=MockERC20, + web3=web3, + account=account, + spender_address=ROYALTY_MODULE, + amount=100000 * 10**6, + ) assert response is not None - assert 'ip_id' in response - assert response['ip_id'] is not None + assert "ip_id" in response + assert response["ip_id"] is not None + + return response["ip_id"] - return response['ip_id'] def test_attach_license_terms(story_client, ip_id, register_commercial_use_pil): license_terms_id = register_commercial_use_pil - response = story_client.License.attach_license_terms(ip_id, PIL_LICENSE_TEMPLATE, license_terms_id) - - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'tx_hash' in response, "Response does not contain 'tx_hash'." - assert response['tx_hash'] is not None, "'tx_hash' is None." - assert isinstance(response['tx_hash'], str), "'tx_hash' is not a string." - assert len(response['tx_hash']) > 0, "'tx_hash' is empty." + response = story_client.License.attach_license_terms( + ip_id, PIL_LICENSE_TEMPLATE, license_terms_id + ) + + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert "tx_hash" in response, "Response does not contain 'tx_hash'." + assert response["tx_hash"] is not None, "'tx_hash' is None." + assert isinstance(response["tx_hash"], str), "'tx_hash' is not a string." + assert len(response["tx_hash"]) > 0, "'tx_hash' is empty." + def test_mint_license_tokens(story_client, ip_id, register_commercial_use_pil): response = story_client.License.mint_license_tokens( - licensor_ip_id=ip_id, - license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=register_commercial_use_pil, - amount=1, - receiver=account.address + licensor_ip_id=ip_id, + license_template=PIL_LICENSE_TEMPLATE, + license_terms_id=register_commercial_use_pil, + amount=1, + receiver=account.address, ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'tx_hash' in response, "Response does not contain 'tx_hash'." - assert response['tx_hash'] is not None, "'tx_hash' is None." - assert isinstance(response['tx_hash'], str), "'tx_hash' is not a string." - assert len(response['tx_hash']) > 0, "'tx_hash' is empty." - assert 'license_token_ids' in response, "Response does not contain 'license_token_ids'." - assert response['license_token_ids'] is not None, "'license_token_ids' is None." - assert isinstance(response['license_token_ids'], list), "'license_token_ids' is not a list." - assert all(isinstance(i, int) for i in response['license_token_ids']), "Not all elements in 'license_token_ids' are integers." + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert "tx_hash" in response, "Response does not contain 'tx_hash'." + assert response["tx_hash"] is not None, "'tx_hash' is None." + assert isinstance(response["tx_hash"], str), "'tx_hash' is not a string." + assert len(response["tx_hash"]) > 0, "'tx_hash' is empty." + assert ( + "license_token_ids" in response + ), "Response does not contain 'license_token_ids'." + assert response["license_token_ids"] is not None, "'license_token_ids' is None." + assert isinstance( + response["license_token_ids"], list + ), "'license_token_ids' is not a list." + assert all( + isinstance(i, int) for i in response["license_token_ids"] + ), "Not all elements in 'license_token_ids' are integers." + def test_get_license_terms(story_client): selectedLicenseTermsId = 3 @@ -156,59 +190,68 @@ def test_get_license_terms(story_client): assert response is not None, "Response is None, indicating the call failed." + def test_predict_minting_license_fee(story_client, ip_id, register_commercial_use_pil): response = story_client.License.predict_minting_license_fee( - licensor_ip_id=ip_id, - license_terms_id=register_commercial_use_pil, - amount=1 + licensor_ip_id=ip_id, license_terms_id=register_commercial_use_pil, amount=1 ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'currency' in response, "Response does not contain 'currency'." - assert response['currency'] is not None, "'currency' is None." - assert isinstance(response['currency'], str), "'currency' is not a string." - assert len(response['currency']) > 0, "'currency' is empty." - assert 'amount' in response, "Response does not contain 'amount'." - assert response['amount'] is not None, "'amount' is None." - assert isinstance(response['amount'], int), "'amount' is not an integer." + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert "currency" in response, "Response does not contain 'currency'." + assert response["currency"] is not None, "'currency' is None." + assert isinstance(response["currency"], str), "'currency' is not a string." + assert len(response["currency"]) > 0, "'currency' is empty." + assert "amount" in response, "Response does not contain 'amount'." + assert response["amount"] is not None, "'amount' is None." + assert isinstance(response["amount"], int), "'amount' is not an integer." + def test_set_licensing_config(story_client, ip_id, register_commercial_remix_pil): licensing_config = { - 'mintingFee': 1, - 'isSet': True, - 'licensingHook': "0x0000000000000000000000000000000000000000", - 'hookData': "0xFcd3243590d29B131a26B1554B0b21a5B43e622e", - 'commercialRevShare': 0, - 'disabled': False, - 'expectMinimumGroupRewardShare': 1, - 'expectGroupRewardPool': "0x0000000000000000000000000000000000000000" + "mintingFee": 1, + "isSet": True, + "licensingHook": "0x0000000000000000000000000000000000000000", + "hookData": "0xFcd3243590d29B131a26B1554B0b21a5B43e622e", + "commercialRevShare": 0, + "disabled": False, + "expectMinimumGroupRewardShare": 1, + "expectGroupRewardPool": "0x0000000000000000000000000000000000000000", } response = story_client.License.set_licensing_config( ip_id=ip_id, license_terms_id=register_commercial_remix_pil, licensing_config=licensing_config, - license_template=PIL_LICENSE_TEMPLATE + license_template=PIL_LICENSE_TEMPLATE, ) - assert response is not None, "Response is None, indicating the contract interaction failed." - assert 'tx_hash' in response, "Response does not contain 'tx_hash'" - assert response['tx_hash'] is not None, "'tx_hash' is None" - assert isinstance(response['tx_hash'], str), "'tx_hash' is not a string" - assert len(response['tx_hash']) > 0, "'tx_hash' is empty" - assert 'success' in response, "Response does not contain 'success'" - assert response['success'] is True, "'success' is not True" + assert ( + response is not None + ), "Response is None, indicating the contract interaction failed." + assert "tx_hash" in response, "Response does not contain 'tx_hash'" + assert response["tx_hash"] is not None, "'tx_hash' is None" + assert isinstance(response["tx_hash"], str), "'tx_hash' is not a string" + assert len(response["tx_hash"]) > 0, "'tx_hash' is empty" + assert "success" in response, "Response does not contain 'success'" + assert response["success"] is True, "'success' is not True" + def test_register_pil_terms_with_no_minting_fee(story_client): """Test registering PIL terms with no minting fee.""" response = story_client.License.register_pil_terms( transferable=False, - royalty_policy=story_client.web3.to_checksum_address("0x0000000000000000000000000000000000000000"), + royalty_policy=story_client.web3.to_checksum_address( + "0x0000000000000000000000000000000000000000" + ), default_minting_fee=0, # Minimal minting fee expiration=0, commercial_use=False, commercial_attribution=False, - commercializer_checker=story_client.web3.to_checksum_address("0x0000000000000000000000000000000000000000"), + commercializer_checker=story_client.web3.to_checksum_address( + "0x0000000000000000000000000000000000000000" + ), commercializer_checker_data="0x", commercial_rev_share=0, commercial_rev_ceiling=0, @@ -218,25 +261,26 @@ def test_register_pil_terms_with_no_minting_fee(story_client): derivatives_reciprocal=False, derivative_rev_ceiling=0, currency=MockERC20, - uri="" + uri="", ) assert response is not None - assert 'license_terms_id' in response - assert response['license_terms_id'] is not None - assert isinstance(response['license_terms_id'], int) + assert "license_terms_id" in response + assert response["license_terms_id"] is not None + assert isinstance(response["license_terms_id"], int) + def test_register_commercial_use_pil_without_royalty_policy(story_client): """Test registering commercial use PIL without specifying royalty policy.""" response = story_client.License.register_commercial_use_pil( - default_minting_fee=1, - currency=MockERC20 + default_minting_fee=1, currency=MockERC20 ) assert response is not None - assert 'license_terms_id' in response - assert response['license_terms_id'] is not None - assert isinstance(response['license_terms_id'], int) + assert "license_terms_id" in response + assert response["license_terms_id"] is not None + assert isinstance(response["license_terms_id"], int) + @pytest.fixture(scope="module") def setup_license_terms(story_client, ip_id): @@ -245,19 +289,18 @@ def setup_license_terms(story_client, ip_id): default_minting_fee=1, currency=MockERC20, commercial_rev_share=100, - royalty_policy=ROYALTY_POLICY + royalty_policy=ROYALTY_POLICY, ) - license_id = response['license_terms_id'] + license_id = response["license_terms_id"] # Attach the license terms story_client.License.attach_license_terms( - ip_id=ip_id, - license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=license_id + ip_id=ip_id, license_template=PIL_LICENSE_TEMPLATE, license_terms_id=license_id ) return license_id + def test_multi_token_minting(story_client, ip_id, setup_license_terms): """Test minting multiple license tokens at once.""" response = story_client.License.mint_license_tokens( @@ -265,60 +308,66 @@ def test_multi_token_minting(story_client, ip_id, setup_license_terms): license_template=PIL_LICENSE_TEMPLATE, license_terms_id=setup_license_terms, amount=3, # Mint multiple tokens - receiver=account.address + receiver=account.address, ) assert response is not None - assert 'tx_hash' in response - assert response['tx_hash'] is not None - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - assert 'license_token_ids' in response - assert isinstance(response['license_token_ids'], list) - assert len(response['license_token_ids']) > 0 - -def test_set_licensing_config_with_hooks(story_client, ip_id, register_commercial_remix_pil): + assert "tx_hash" in response + assert response["tx_hash"] is not None + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + assert "license_token_ids" in response + assert isinstance(response["license_token_ids"], list) + assert len(response["license_token_ids"]) > 0 + + +def test_set_licensing_config_with_hooks( + story_client, ip_id, register_commercial_remix_pil +): """Test setting licensing configuration with hooks enabled.""" licensing_config = { - 'mintingFee': 100, - 'isSet': True, - 'licensingHook': "0x0000000000000000000000000000000000000000", - 'hookData': "0x1234567890", # Different hook data - 'commercialRevShare': 100, # 50% revenue share - 'disabled': False, - 'expectMinimumGroupRewardShare': 10, # 10% minimum group reward - 'expectGroupRewardPool': "0x0000000000000000000000000000000000000000" + "mintingFee": 100, + "isSet": True, + "licensingHook": "0x0000000000000000000000000000000000000000", + "hookData": "0x1234567890", # Different hook data + "commercialRevShare": 100, # 50% revenue share + "disabled": False, + "expectMinimumGroupRewardShare": 10, # 10% minimum group reward + "expectGroupRewardPool": "0x0000000000000000000000000000000000000000", } response = story_client.License.set_licensing_config( ip_id=ip_id, license_terms_id=register_commercial_remix_pil, licensing_config=licensing_config, - license_template=PIL_LICENSE_TEMPLATE + license_template=PIL_LICENSE_TEMPLATE, ) assert response is not None - assert 'tx_hash' in response - assert response['tx_hash'] is not None - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - assert 'success' in response - assert response['success'] is True - -def test_predict_minting_fee_with_multiple_tokens(story_client, ip_id, setup_license_terms): + assert "tx_hash" in response + assert response["tx_hash"] is not None + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + assert "success" in response + assert response["success"] is True + + +def test_predict_minting_fee_with_multiple_tokens( + story_client, ip_id, setup_license_terms +): """Test predicting minting fee for multiple tokens.""" response = story_client.License.predict_minting_license_fee( licensor_ip_id=ip_id, license_terms_id=setup_license_terms, - amount=5 # Predict for 5 tokens + amount=5, # Predict for 5 tokens ) assert response is not None - assert 'currency' in response - assert response['currency'] is not None - assert isinstance(response['currency'], str) - assert len(response['currency']) > 0 - assert 'amount' in response - assert response['amount'] is not None - assert isinstance(response['amount'], int) - assert response['amount'] > 0 # Amount should be positive for multiple tokens + assert "currency" in response + assert response["currency"] is not None + assert isinstance(response["currency"], str) + assert len(response["currency"]) > 0 + assert "amount" in response + assert response["amount"] is not None + assert isinstance(response["amount"], int) + assert response["amount"] > 0 # Amount should be positive for multiple tokens diff --git a/tests/integration/test_integration_nft_client.py b/tests/integration/test_integration_nft_client.py index fa5a1e8..2c2fa26 100644 --- a/tests/integration/test_integration_nft_client.py +++ b/tests/integration/test_integration_nft_client.py @@ -9,6 +9,7 @@ MockERC20, ) + class TestNFTCollectionOperations: """Tests for NFT collection creation and management""" @@ -20,15 +21,15 @@ def test_create_basic_collection(self, story_client): is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - assert 'nft_contract' in response - assert Web3.is_address(response['nft_contract']) + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + assert "nft_contract" in response + assert Web3.is_address(response["nft_contract"]) def test_create_collection_with_supply(self, story_client): """Test creating collection with max supply""" @@ -39,12 +40,12 @@ def test_create_collection_with_supply(self, story_client): is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) assert response is not None - assert 'nft_contract' in response - assert Web3.is_address(response['nft_contract']) + assert "nft_contract" in response + assert Web3.is_address(response["nft_contract"]) def test_create_collection_with_mint_fee(self, story_client): """Test creating collection with mint fee configuration""" @@ -56,12 +57,12 @@ def test_create_collection_with_mint_fee(self, story_client): contract_uri="test-uri", mint_fee_recipient=story_client.account.address, mint_fee=1000, - mint_fee_token=MockERC20 + mint_fee_token=MockERC20, ) assert response is not None - assert 'nft_contract' in response - assert Web3.is_address(response['nft_contract']) + assert "nft_contract" in response + assert Web3.is_address(response["nft_contract"]) def test_create_collection_with_base_uri(self, story_client): """Test creating collection with base URI""" @@ -72,12 +73,13 @@ def test_create_collection_with_base_uri(self, story_client): mint_open=True, contract_uri="test-uri", mint_fee_recipient=story_client.account.address, - base_uri="https://api.example.com/nft/" + base_uri="https://api.example.com/nft/", ) assert response is not None - assert 'nft_contract' in response - assert Web3.is_address(response['nft_contract']) + assert "nft_contract" in response + assert Web3.is_address(response["nft_contract"]) + class TestErrorCases: """Tests for error handling in NFT collection creation""" @@ -93,7 +95,7 @@ def test_invalid_mint_fee_configuration(self, story_client): contract_uri="test-uri", mint_fee_recipient=story_client.account.address, mint_fee=1000, - mint_fee_token="0xinvalid" + mint_fee_token="0xinvalid", ) assert "Invalid mint fee token address" in str(exc_info.value) @@ -106,9 +108,12 @@ def test_invalid_recipient_address(self, story_client): is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient="0xinvalid" + mint_fee_recipient="0xinvalid", ) - assert "when sending a str, it must be a hex string. got: '0xinvalid'" in str(exc_info.value).lower() + assert ( + "when sending a str, it must be a hex string. got: '0xinvalid'" + in str(exc_info.value).lower() + ) def test_invalid_mint_fee_values(self, story_client): """Test with invalid mint fee values""" @@ -121,10 +126,10 @@ def test_invalid_mint_fee_values(self, story_client): contract_uri="test-uri", mint_fee_recipient=story_client.account.address, mint_fee=-100, # Negative mint fee - mint_fee_token=MockERC20 + mint_fee_token=MockERC20, ) assert "Invalid mint fee" in str(exc_info.value) - + try: huge_mint_fee = 2**256 - 1 # Max uint256 value story_client.NFTClient.create_nft_collection( @@ -135,15 +140,19 @@ def test_invalid_mint_fee_values(self, story_client): contract_uri="test-uri", mint_fee_recipient=story_client.account.address, mint_fee=huge_mint_fee, - mint_fee_token=MockERC20 + mint_fee_token=MockERC20, ) except Exception as e: - assert "overflow" in str(e).lower() or "revert" in str(e).lower() or "invalid" in str(e).lower() - + assert ( + "overflow" in str(e).lower() + or "revert" in str(e).lower() + or "invalid" in str(e).lower() + ) + def test_parameter_omission(self, story_client): """Test omitting required parameters""" - + with pytest.raises(TypeError) as exc_info: story_client.NFTClient.create_nft_collection( # name is omitted @@ -151,9 +160,9 @@ def test_parameter_omission(self, story_client): is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) - + with pytest.raises(TypeError) as exc_info: story_client.NFTClient.create_nft_collection( name="test-collection", @@ -161,9 +170,9 @@ def test_parameter_omission(self, story_client): is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) - + with pytest.raises(TypeError) as exc_info: story_client.NFTClient.create_nft_collection( name="test-collection", @@ -171,9 +180,9 @@ def test_parameter_omission(self, story_client): # is_public_minting is omitted mint_open=True, contract_uri="test-uri", - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) - + with pytest.raises(TypeError) as exc_info: story_client.NFTClient.create_nft_collection( name="test-collection", @@ -181,9 +190,9 @@ def test_parameter_omission(self, story_client): is_public_minting=True, # mint_open is omitted contract_uri="test-uri", - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) - + with pytest.raises(TypeError) as exc_info: story_client.NFTClient.create_nft_collection( name="test-collection", @@ -191,24 +200,24 @@ def test_parameter_omission(self, story_client): is_public_minting=True, mint_open=True, # contract_uri is omitted - mint_fee_recipient=story_client.account.address + mint_fee_recipient=story_client.account.address, ) - + with pytest.raises(TypeError) as exc_info: story_client.NFTClient.create_nft_collection( name="test-collection", symbol="TEST", is_public_minting=True, mint_open=True, - contract_uri="test-uri" + contract_uri="test-uri", # mint_fee_recipient is omitted ) - + def test_authorization_errors(self, story_client): """Test unauthorized operations""" - + different_owner = "0x1234567890123456789012345678901234567890" - + response = story_client.NFTClient.create_nft_collection( name="test-collection", symbol="TEST", @@ -216,12 +225,13 @@ def test_authorization_errors(self, story_client): mint_open=True, contract_uri="test-uri", mint_fee_recipient=story_client.account.address, - owner=different_owner + owner=different_owner, ) - + assert response is not None - assert 'nft_contract' in response - assert Web3.is_address(response['nft_contract']) + assert "nft_contract" in response + assert Web3.is_address(response["nft_contract"]) + class TestMintFee: """Tests for mint fee functionality in NFT collections""" @@ -238,9 +248,9 @@ def nft_contract(self, story_client): mint_fee_recipient=story_client.account.address, mint_fee=1000, mint_fee_token=MockERC20, - ) + ) - return response['nft_contract'] + return response["nft_contract"] def test_get_mint_fee_token(self, story_client, nft_contract): """Test successfully getting mint fee token""" diff --git a/tests/integration/test_integration_permission.py b/tests/integration/test_integration_permission.py index a27f504..a82aa4d 100644 --- a/tests/integration/test_integration_permission.py +++ b/tests/integration/test_integration_permission.py @@ -5,24 +5,24 @@ from setup_for_integration import ( web3, - account, + account, story_client, get_token_id, MockERC721, - CORE_METADATA_MODULE + CORE_METADATA_MODULE, ) + class TestPermissions: @pytest.fixture(scope="class") def ip_id(self, story_client): """Fixture to create an IP for testing permissions.""" token_id = get_token_id(MockERC721, story_client.web3, story_client.account) response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=token_id + nft_contract=MockERC721, token_id=token_id ) - assert 'ip_id' in response, "Failed to register IP" - return response['ip_id'] + assert "ip_id" in response, "Failed to register IP" + return response["ip_id"] def test_set_permission(self, story_client, ip_id): """Test setting permission successfully.""" @@ -31,156 +31,152 @@ def test_set_permission(self, story_client, ip_id): signer=account.address, to=CORE_METADATA_MODULE, permission=1, # ALLOW - func="function setAll(address,string,bytes32,bytes32)" + func="function setAll(address,string,bytes32,bytes32)", ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 def test_set_all_permissions(self, story_client, ip_id): """Test setting all permissions successfully.""" response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=account.address, - permission=1 # ALLOW + ip_id=ip_id, signer=account.address, permission=1 # ALLOW ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 def test_create_set_permission_signature(self, story_client, ip_id): """Test creating set permission signature.""" - deadline = web3.eth.get_block('latest')['timestamp'] + 60000 - + deadline = web3.eth.get_block("latest")["timestamp"] + 60000 + response = story_client.Permission.create_set_permission_signature( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, func="setAll(address,string,bytes32,bytes32)", permission=1, # ALLOW - deadline=deadline + deadline=deadline, ) assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 def test_set_permission_invalid_ip(self, story_client): """Test setting permission for an unregistered IP.""" unregistered_ip = "0x1234567890123456789012345678901234567890" - + with pytest.raises(Exception) as exc_info: story_client.Permission.set_permission( ip_id=unregistered_ip, signer=account.address, to=CORE_METADATA_MODULE, - permission=1 + permission=1, ) - + assert f"IP id with {unregistered_ip} is not registered" in str(exc_info.value) def test_set_permission_invalid_addresses(self, story_client, ip_id): """Test that set_permission raises proper exceptions for invalid addresses.""" invalid_signer = "0xinvalid_address" - + with pytest.raises(Exception) as exc_info: story_client.Permission.set_permission( ip_id=ip_id, signer=invalid_signer, to=CORE_METADATA_MODULE, - permission=1 # ALLOW + permission=1, # ALLOW ) - + assert "invalid address" in str(exc_info.value).lower() - + invalid_to = "not_a_hex_address" - + with pytest.raises(Exception) as exc_info: story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=invalid_to, - permission=1 # ALLOW + permission=1, # ALLOW ) - + assert "invalid address" in str(exc_info.value).lower() - + lowercase_address = account.address.lower() try: response = story_client.Permission.set_permission( ip_id=ip_id, signer=lowercase_address, to=CORE_METADATA_MODULE, - permission=1 + permission=1, ) - assert 'tx_hash' in response + assert "tx_hash" in response except Exception as e: - pytest.fail(f"set_permission should accept lowercase addresses, but raised: {e}") + pytest.fail( + f"set_permission should accept lowercase addresses, but raised: {e}" + ) def test_different_permission_levels(self, story_client, ip_id): """Test setting and changing different permission levels.""" DISALLOW = 0 ALLOW = 1 ABSTAIN = 2 - + response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=DISALLOW, - func="function setAll(address,string,bytes32,bytes32)" + func="function setAll(address,string,bytes32,bytes32)", ) - + assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=ALLOW, - func="function setAll(address,string,bytes32,bytes32)" + func="function setAll(address,string,bytes32,bytes32)", ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=ABSTAIN, - func="function setAll(address,string,bytes32,bytes32)" + func="function setAll(address,string,bytes32,bytes32)", ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=account.address, - permission=DISALLOW + ip_id=ip_id, signer=account.address, permission=DISALLOW ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=account.address, - permission=ABSTAIN + ip_id=ip_id, signer=account.address, permission=ABSTAIN ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + def test_different_function_selectors(self, story_client, ip_id): """Test setting permissions with different function selectors.""" ALLOW = 1 @@ -189,140 +185,130 @@ def test_different_function_selectors(self, story_client, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=1 + permission=1, # No func parameter provided - should use default ) - + assert response is not None - assert 'tx_hash' in response - assert isinstance(response['tx_hash'], str) - assert len(response['tx_hash']) > 0 - + assert "tx_hash" in response + assert isinstance(response["tx_hash"], str) + assert len(response["tx_hash"]) > 0 + response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, - func="setAll(address,string,bytes32,bytes32)" + permission=ALLOW, + func="setAll(address,string,bytes32,bytes32)", ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, - func="setName(address,string)" + permission=ALLOW, + func="setName(address,string)", ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=ALLOW, - func="setDescription(address,string)" + func="setDescription(address,string)", ) - + assert response is not None - assert 'tx_hash' in response - - deadline = web3.eth.get_block('latest')['timestamp'] + 60000 + assert "tx_hash" in response + + deadline = web3.eth.get_block("latest")["timestamp"] + 60000 response = story_client.Permission.create_set_permission_signature( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=ALLOW, # No func parameter provided - deadline=deadline + deadline=deadline, ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + def test_permission_hierarchies_and_overrides(self, story_client, ip_id): """Test permission hierarchies and how permissions override each other.""" DISALLOW = 0 ALLOW = 1 ABSTAIN = 2 - + response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=account.address, - permission=DISALLOW + ip_id=ip_id, signer=account.address, permission=DISALLOW ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + specific_func = "setName(address,string)" response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=ALLOW, - func=specific_func + func=specific_func, ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + alternate_signer = web3.eth.account.create() - + response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=alternate_signer.address, - permission=ALLOW + ip_id=ip_id, signer=alternate_signer.address, permission=ALLOW ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_permission( ip_id=ip_id, signer=alternate_signer.address, to=CORE_METADATA_MODULE, permission=DISALLOW, - func=specific_func + func=specific_func, ) - + assert response is not None - assert 'tx_hash' in response - - deadline = web3.eth.get_block('latest')['timestamp'] + 60000 - + assert "tx_hash" in response + + deadline = web3.eth.get_block("latest")["timestamp"] + 60000 + response = story_client.Permission.create_set_permission_signature( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, permission=ALLOW, func="setDescription(address,string)", - deadline=deadline + deadline=deadline, ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=account.address, - permission=ABSTAIN + ip_id=ip_id, signer=account.address, permission=ABSTAIN ) - + assert response is not None - assert 'tx_hash' in response - + assert "tx_hash" in response + response = story_client.Permission.set_all_permissions( - ip_id=ip_id, - signer=alternate_signer.address, - permission=ABSTAIN + ip_id=ip_id, signer=alternate_signer.address, permission=ABSTAIN ) - - assert response is not None - assert 'tx_hash' in response - + assert response is not None + assert "tx_hash" in response diff --git a/tests/integration/test_integration_royalty.py b/tests/integration/test_integration_royalty.py index 1179f30..5cfdcfe 100644 --- a/tests/integration/test_integration_royalty.py +++ b/tests/integration/test_integration_royalty.py @@ -19,40 +19,43 @@ PIL_LICENSE_TEMPLATE, ) + class TestRoyalty: @pytest.fixture(scope="module") def setup_ips_and_licenses(self, story_client): """Setup parent and child IPs with proper license relationships""" - - parent_token_id = get_token_id(MockERC721, story_client.web3, story_client.account) + + parent_token_id = get_token_id( + MockERC721, story_client.web3, story_client.account + ) parent_ip_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=parent_token_id + nft_contract=MockERC721, token_id=parent_token_id + ) + parent_ip_id = parent_ip_response["ip_id"] + + child_token_id = get_token_id( + MockERC721, story_client.web3, story_client.account ) - parent_ip_id = parent_ip_response['ip_id'] - - child_token_id = get_token_id(MockERC721, story_client.web3, story_client.account) child_ip_response = story_client.IPAsset.register( - nft_contract=MockERC721, - token_id=child_token_id + nft_contract=MockERC721, token_id=child_token_id ) - child_ip_id = child_ip_response['ip_id'] - + child_ip_id = child_ip_response["ip_id"] + license_terms_response = story_client.License.register_commercial_remix_pil( default_minting_fee=100000, currency=MockERC20, commercial_rev_share=10, - royalty_policy=ROYALTY_POLICY + royalty_policy=ROYALTY_POLICY, ) - license_terms_id = license_terms_response['license_terms_id'] - + license_terms_id = license_terms_response["license_terms_id"] + story_client.License.attach_license_terms( ip_id=parent_ip_id, license_template=PIL_LICENSE_TEMPLATE, - license_terms_id=license_terms_id + license_terms_id=license_terms_id, ) - + story_client.IPAsset.register_derivative( child_ip_id=child_ip_id, parent_ip_ids=[parent_ip_id], @@ -61,369 +64,371 @@ def setup_ips_and_licenses(self, story_client): max_rts=0, max_revenue_share=0, ) - + mint_tokens( - erc20_contract_address=MockERC20, - web3=web3, - account=account, - to_address=account.address, - amount=100000 * 10 ** 6 + erc20_contract_address=MockERC20, + web3=web3, + account=account, + to_address=account.address, + amount=100000 * 10**6, ) - + approve( - erc20_contract_address=MockERC20, - web3=web3, - account=account, - spender_address=ROYALTY_MODULE, - amount=100000 * 10 ** 6 + erc20_contract_address=MockERC20, + web3=web3, + account=account, + spender_address=ROYALTY_MODULE, + amount=100000 * 10**6, ) - + return { - 'parent_ip_id': parent_ip_id, - 'child_ip_id': child_ip_id, - 'license_terms_id': license_terms_id + "parent_ip_id": parent_ip_id, + "child_ip_id": child_ip_id, + "license_terms_id": license_terms_id, } def test_pay_royalty_on_behalf(self, story_client, setup_ips_and_licenses): """Test paying royalty on behalf of a payer IP to a receiver IP""" - parent_ip_id = setup_ips_and_licenses['parent_ip_id'] - child_ip_id = setup_ips_and_licenses['child_ip_id'] - + parent_ip_id = setup_ips_and_licenses["parent_ip_id"] + child_ip_id = setup_ips_and_licenses["child_ip_id"] + response = story_client.Royalty.pay_royalty_on_behalf( receiver_ip_id=parent_ip_id, payer_ip_id=child_ip_id, token=MockERC20, - amount=1 + amount=1, ) assert response is not None - assert response['tx_hash'] is not None and isinstance(response['tx_hash'], str) + assert response["tx_hash"] is not None and isinstance(response["tx_hash"], str) def test_claimable_revenue(self, story_client, setup_ips_and_licenses): """Test checking claimable revenue""" - parent_ip_id = setup_ips_and_licenses['parent_ip_id'] - + parent_ip_id = setup_ips_and_licenses["parent_ip_id"] + response = story_client.Royalty.claimable_revenue( - royalty_vault_ip_id=parent_ip_id, - claimer=account.address, - token=MockERC20 + royalty_vault_ip_id=parent_ip_id, claimer=account.address, token=MockERC20 ) assert isinstance(response, int) - - def test_pay_royalty_unregistered_receiver(self, story_client, setup_ips_and_licenses): + + def test_pay_royalty_unregistered_receiver( + self, story_client, setup_ips_and_licenses + ): """Test that paying royalty to unregistered IP fails appropriately""" - child_ip_id = setup_ips_and_licenses['child_ip_id'] + child_ip_id = setup_ips_and_licenses["child_ip_id"] unregistered_ip_id = "0x1234567890123456789012345678901234567890" - - with pytest.raises(ValueError, match=f"The receiver IP with id {unregistered_ip_id} is not registered"): + + with pytest.raises( + ValueError, + match=f"The receiver IP with id {unregistered_ip_id} is not registered", + ): story_client.Royalty.pay_royalty_on_behalf( receiver_ip_id=unregistered_ip_id, payer_ip_id=child_ip_id, token=MockERC20, - amount=1000 + amount=1000, ) def test_pay_royalty_invalid_amount(self, story_client, setup_ips_and_licenses): """Test that paying with invalid amount fails appropriately""" - parent_ip_id = setup_ips_and_licenses['parent_ip_id'] - child_ip_id = setup_ips_and_licenses['child_ip_id'] - - with pytest.raises(Exception): + parent_ip_id = setup_ips_and_licenses["parent_ip_id"] + child_ip_id = setup_ips_and_licenses["child_ip_id"] + + with pytest.raises(Exception): story_client.Royalty.pay_royalty_on_behalf( receiver_ip_id=parent_ip_id, payer_ip_id=child_ip_id, token=MockERC20, - amount=-1 + amount=-1, ) + class TestClaimAllRevenue: @pytest.fixture(scope="module") def setup_claim_all_revenue(self, story_client): # Create NFT collection collection_response = story_client.NFTClient.create_nft_collection( name="free-collection", - symbol="FREE", + symbol="FREE", max_supply=100, is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=ZERO_ADDRESS + mint_fee_recipient=ZERO_ADDRESS, ) - spg_nft_contract = collection_response['nft_contract'] + spg_nft_contract = collection_response["nft_contract"] # Define license terms data template - license_terms_template = [{ - 'terms': { - 'transferable': True, - 'royalty_policy': ROYALTY_POLICY, - 'default_minting_fee': 100, - 'expiration': 0, - 'commercial_use': True, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'commercial_rev_share': 10, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_attribution': True, - 'derivatives_approval': False, - 'derivatives_reciprocal': True, - 'derivative_rev_ceiling': 0, - 'currency': MockERC20, - 'uri': '' - }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 100, - 'hook_data': ZERO_ADDRESS, - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS + license_terms_template = [ + { + "terms": { + "transferable": True, + "royalty_policy": ROYALTY_POLICY, + "default_minting_fee": 100, + "expiration": 0, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 10, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": MockERC20, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 100, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, } - }] + ] # Create unique metadata for each IP metadata_a = { - 'ip_metadata_uri': "test-uri-a", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-a")), - 'nft_metadata_uri': "test-nft-uri-a", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-a")) + "ip_metadata_uri": "test-uri-a", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-a")), + "nft_metadata_uri": "test-nft-uri-a", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-a") + ), } - + metadata_b = { - 'ip_metadata_uri': "test-uri-b", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-b")), - 'nft_metadata_uri': "test-nft-uri-b", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-b")) + "ip_metadata_uri": "test-uri-b", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-b")), + "nft_metadata_uri": "test-nft-uri-b", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-b") + ), } - + metadata_c = { - 'ip_metadata_uri': "test-uri-c", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-c")), - 'nft_metadata_uri': "test-nft-uri-c", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-c")) + "ip_metadata_uri": "test-uri-c", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-c")), + "nft_metadata_uri": "test-nft-uri-c", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-c") + ), } - + metadata_d = { - 'ip_metadata_uri': "test-uri-d", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-d")), - 'nft_metadata_uri': "test-nft-uri-d", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-d")) + "ip_metadata_uri": "test-uri-d", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-d")), + "nft_metadata_uri": "test-nft-uri-d", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-d") + ), } # Register IP A with PIL terms ip_a_response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract=spg_nft_contract, terms=copy.deepcopy(license_terms_template), - ip_metadata=metadata_a + ip_metadata=metadata_a, ) - ip_a = ip_a_response['ip_id'] - license_terms_id = ip_a_response['license_terms_ids'][0] + ip_a = ip_a_response["ip_id"] + license_terms_id = ip_a_response["license_terms_ids"][0] # Register IP B as derivative of A ip_b_response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=spg_nft_contract, - ip_metadata=metadata_b + spg_nft_contract=spg_nft_contract, ip_metadata=metadata_b ) - ip_b = ip_b_response['ip_id'] + ip_b = ip_b_response["ip_id"] story_client.IPAsset.register_derivative( - child_ip_id=ip_b, - parent_ip_ids=[ip_a], - license_terms_ids=[license_terms_id] + child_ip_id=ip_b, parent_ip_ids=[ip_a], license_terms_ids=[license_terms_id] ) # Register IP C as derivative of B ip_c_response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=spg_nft_contract, - ip_metadata=metadata_c + spg_nft_contract=spg_nft_contract, ip_metadata=metadata_c ) - ip_c = ip_c_response['ip_id'] - story_client.IPAsset.register_derivative( - child_ip_id=ip_c, - parent_ip_ids=[ip_b], - license_terms_ids=[license_terms_id] + ip_c = ip_c_response["ip_id"] + story_client.IPAsset.register_derivative( + child_ip_id=ip_c, parent_ip_ids=[ip_b], license_terms_ids=[license_terms_id] ) # Register IP D as derivative of C ip_d_response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=spg_nft_contract, - ip_metadata=metadata_d + spg_nft_contract=spg_nft_contract, ip_metadata=metadata_d ) - ip_d = ip_d_response['ip_id'] + ip_d = ip_d_response["ip_id"] story_client.IPAsset.register_derivative( - child_ip_id=ip_d, - parent_ip_ids=[ip_c], - license_terms_ids=[license_terms_id] + child_ip_id=ip_d, parent_ip_ids=[ip_c], license_terms_ids=[license_terms_id] ) - - return { - 'ip_a': ip_a, - 'ip_b': ip_b, - 'ip_c': ip_c, - 'ip_d': ip_d - } + + return {"ip_a": ip_a, "ip_b": ip_b, "ip_c": ip_c, "ip_d": ip_d} def test_claim_all_revenue(self, setup_claim_all_revenue, story_client): response = story_client.Royalty.claim_all_revenue( - ancestor_ip_id=setup_claim_all_revenue['ip_a'], - claimer=setup_claim_all_revenue['ip_a'], - child_ip_ids=[setup_claim_all_revenue['ip_b'], setup_claim_all_revenue['ip_c']], + ancestor_ip_id=setup_claim_all_revenue["ip_a"], + claimer=setup_claim_all_revenue["ip_a"], + child_ip_ids=[ + setup_claim_all_revenue["ip_b"], + setup_claim_all_revenue["ip_c"], + ], royalty_policies=[ROYALTY_POLICY, ROYALTY_POLICY], - currency_tokens=[MockERC20, MockERC20] - ) + currency_tokens=[MockERC20, MockERC20], + ) assert response is not None - assert 'tx_hashes' in response - assert isinstance(response['tx_hashes'], list) - assert len(response['tx_hashes']) > 0 - assert response['claimed_tokens'][0]['amount'] == 120 + assert "tx_hashes" in response + assert isinstance(response["tx_hashes"], list) + assert len(response["tx_hashes"]) > 0 + assert response["claimed_tokens"][0]["amount"] == 120 @pytest.fixture(scope="module") def setup_claim_all_revenue_claim_options(self, story_client): # Create NFT collection collection_response = story_client.NFTClient.create_nft_collection( name="free-collection", - symbol="FREE", + symbol="FREE", max_supply=100, is_public_minting=True, mint_open=True, contract_uri="test-uri", - mint_fee_recipient=ZERO_ADDRESS + mint_fee_recipient=ZERO_ADDRESS, ) - spg_nft_contract = collection_response['nft_contract'] + spg_nft_contract = collection_response["nft_contract"] # Define license terms data template - license_terms_template = [{ - 'terms': { - 'transferable': True, - 'royalty_policy': ROYALTY_POLICY, - 'default_minting_fee': 100, - 'expiration': 0, - 'commercial_use': True, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': ZERO_ADDRESS, - 'commercial_rev_share': 10, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': True, - 'derivatives_attribution': True, - 'derivatives_approval': False, - 'derivatives_reciprocal': True, - 'derivative_rev_ceiling': 0, - 'currency': MockERC20, - 'uri': '' - }, - 'licensing_config': { - 'is_set': True, - 'minting_fee': 100, - 'hook_data': ZERO_ADDRESS, - 'licensing_hook': ZERO_ADDRESS, - 'commercial_rev_share': 0, - 'disabled': False, - 'expect_minimum_group_reward_share': 0, - 'expect_group_reward_pool': ZERO_ADDRESS + license_terms_template = [ + { + "terms": { + "transferable": True, + "royalty_policy": ROYALTY_POLICY, + "default_minting_fee": 100, + "expiration": 0, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": ZERO_ADDRESS, + "commercial_rev_share": 10, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": MockERC20, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 100, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, } - }] + ] # Create unique metadata for each IP metadata_a = { - 'ip_metadata_uri': "test-uri-a", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-a")), - 'nft_metadata_uri': "test-nft-uri-a", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-a")) + "ip_metadata_uri": "test-uri-a", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-a")), + "nft_metadata_uri": "test-nft-uri-a", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-a") + ), } - + metadata_b = { - 'ip_metadata_uri': "test-uri-b", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-b")), - 'nft_metadata_uri': "test-nft-uri-b", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-b")) + "ip_metadata_uri": "test-uri-b", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-b")), + "nft_metadata_uri": "test-nft-uri-b", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-b") + ), } - + metadata_c = { - 'ip_metadata_uri': "test-uri-c", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-c")), - 'nft_metadata_uri': "test-nft-uri-c", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-c")) + "ip_metadata_uri": "test-uri-c", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-c")), + "nft_metadata_uri": "test-nft-uri-c", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-c") + ), } - + metadata_d = { - 'ip_metadata_uri': "test-uri-d", - 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash-d")), - 'nft_metadata_uri': "test-nft-uri-d", - 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash-d")) + "ip_metadata_uri": "test-uri-d", + "ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-hash-d")), + "nft_metadata_uri": "test-nft-uri-d", + "nft_metadata_hash": web3.to_hex( + web3.keccak(text="test-nft-metadata-hash-d") + ), } # Register IP A with PIL terms ip_a_response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms( spg_nft_contract=spg_nft_contract, terms=copy.deepcopy(license_terms_template), - ip_metadata=metadata_a + ip_metadata=metadata_a, ) - ip_a = ip_a_response['ip_id'] - license_terms_id = ip_a_response['license_terms_ids'][0] + ip_a = ip_a_response["ip_id"] + license_terms_id = ip_a_response["license_terms_ids"][0] # Register IP B as derivative of A ip_b_response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=spg_nft_contract, - ip_metadata=metadata_b + spg_nft_contract=spg_nft_contract, ip_metadata=metadata_b ) - ip_b = ip_b_response['ip_id'] + ip_b = ip_b_response["ip_id"] ip_b_derivative_response = story_client.IPAsset.register_derivative( - child_ip_id=ip_b, - parent_ip_ids=[ip_a], - license_terms_ids=[license_terms_id] + child_ip_id=ip_b, parent_ip_ids=[ip_a], license_terms_ids=[license_terms_id] ) # Register IP C as derivative of B ip_c_response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=spg_nft_contract, - ip_metadata=metadata_c + spg_nft_contract=spg_nft_contract, ip_metadata=metadata_c ) - ip_c = ip_c_response['ip_id'] - story_client.IPAsset.register_derivative( - child_ip_id=ip_c, - parent_ip_ids=[ip_b], - license_terms_ids=[license_terms_id] + ip_c = ip_c_response["ip_id"] + story_client.IPAsset.register_derivative( + child_ip_id=ip_c, parent_ip_ids=[ip_b], license_terms_ids=[license_terms_id] ) # Register IP D as derivative of C ip_d_response = story_client.IPAsset.mint_and_register_ip( - spg_nft_contract=spg_nft_contract, - ip_metadata=metadata_d + spg_nft_contract=spg_nft_contract, ip_metadata=metadata_d ) - ip_d = ip_d_response['ip_id'] + ip_d = ip_d_response["ip_id"] story_client.IPAsset.register_derivative( - child_ip_id=ip_d, - parent_ip_ids=[ip_c], - license_terms_ids=[license_terms_id] + child_ip_id=ip_d, parent_ip_ids=[ip_c], license_terms_ids=[license_terms_id] ) - return { - 'ip_a': ip_a, - 'ip_b': ip_b, - 'ip_c': ip_c, - 'ip_d': ip_d - } + return {"ip_a": ip_a, "ip_b": ip_b, "ip_c": ip_c, "ip_d": ip_d} - def test_claim_all_revenue_claim_options(self, setup_claim_all_revenue_claim_options, story_client): + def test_claim_all_revenue_claim_options( + self, setup_claim_all_revenue_claim_options, story_client + ): """Test claiming all revenue with specific claim options""" response = story_client.Royalty.claim_all_revenue( - ancestor_ip_id=setup_claim_all_revenue_claim_options['ip_a'], - claimer=setup_claim_all_revenue_claim_options['ip_a'], - child_ip_ids=[setup_claim_all_revenue_claim_options['ip_b'], setup_claim_all_revenue_claim_options['ip_c']], + ancestor_ip_id=setup_claim_all_revenue_claim_options["ip_a"], + claimer=setup_claim_all_revenue_claim_options["ip_a"], + child_ip_ids=[ + setup_claim_all_revenue_claim_options["ip_b"], + setup_claim_all_revenue_claim_options["ip_c"], + ], royalty_policies=[ROYALTY_POLICY, ROYALTY_POLICY], currency_tokens=[MockERC20, MockERC20], - claim_options={ - 'auto_transfer_all_claimed_tokens_from_ip': True - } + claim_options={"auto_transfer_all_claimed_tokens_from_ip": True}, ) assert response is not None - assert 'tx_hashes' in response - assert isinstance(response['tx_hashes'], list) - assert len(response['tx_hashes']) > 0 - assert response['claimed_tokens'][0]['amount'] == 120 \ No newline at end of file + assert "tx_hashes" in response + assert isinstance(response["tx_hashes"], list) + assert len(response["tx_hashes"]) > 0 + assert response["claimed_tokens"][0]["amount"] == 120 diff --git a/tests/integration/test_integration_wip.py b/tests/integration/test_integration_wip.py index bf7aa95..51dae43 100644 --- a/tests/integration/test_integration_wip.py +++ b/tests/integration/test_integration_wip.py @@ -3,41 +3,37 @@ import pytest from web3 import Web3 -from setup_for_integration import ( - web3, - story_client, - wallet_address, - wallet_address_2 -) +from setup_for_integration import web3, story_client, wallet_address, wallet_address_2 + class TestWIPDeposit: def test_deposit(self, story_client): """Test depositing IP to WIP""" - ip_amt = Web3.to_wei(1, 'ether') # or Web3.to_wei("0.01", 'ether') - + ip_amt = Web3.to_wei(1, "ether") # or Web3.to_wei("0.01", 'ether') + # Get balances before deposit balance_before = story_client.get_wallet_balance() wip_before = story_client.WIP.balance_of(wallet_address) # Deposit IP to WIP - response = story_client.WIP.deposit( - amount=ip_amt - ) - + response = story_client.WIP.deposit(amount=ip_amt) + # Verify transaction hash - assert isinstance(response['tx_hash'], str) - + assert isinstance(response["tx_hash"], str) + # Get balances after deposit balance_after = story_client.get_wallet_balance() wip_after = story_client.WIP.balance_of(wallet_address) # Verify WIP balance increased by deposit amount assert wip_after == wip_before + ip_amt - + # Calculate gas cost - tx_receipt = web3.eth.wait_for_transaction_receipt(response["tx_hash"], timeout=300) - gas_cost = tx_receipt['gasUsed'] * tx_receipt['effectiveGasPrice'] - + tx_receipt = web3.eth.wait_for_transaction_receipt( + response["tx_hash"], timeout=300 + ) + gas_cost = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] + # Verify wallet balance decreased by deposit amount plus gas cost assert balance_after == balance_before - ip_amt - gas_cost @@ -45,29 +41,29 @@ def test_deposit(self, story_client): class TestWIPTransfer: def test_transfer(self, story_client): """Test transferring WIP""" - transfer_amount = Web3.to_wei("0.01", 'ether') - + transfer_amount = Web3.to_wei("0.01", "ether") + # Get balances before transfer sender_wip_before = story_client.WIP.balance_of(wallet_address) receiver_wip_before = story_client.WIP.balance_of(wallet_address_2) - + # Transfer WIP to wallet_address_2 response = story_client.WIP.transfer( to=wallet_address_2, amount=transfer_amount, - tx_options={"waitForTransaction": True} + tx_options={"waitForTransaction": True}, ) - + # Verify transaction hash - assert isinstance(response['tx_hash'], str) - + assert isinstance(response["tx_hash"], str) + # Get balances after transfer sender_wip_after = story_client.WIP.balance_of(wallet_address) receiver_wip_after = story_client.WIP.balance_of(wallet_address_2) - + # Verify sender's WIP balance decreased by transfer amount assert sender_wip_after == sender_wip_before - transfer_amount - + # Verify receiver's WIP balance increased by transfer amount assert receiver_wip_after == receiver_wip_before + transfer_amount # Note: We're not testing transferFrom here as it requires approval @@ -80,26 +76,27 @@ def test_withdraw(self, story_client): # Get balances before withdrawal balance_before = story_client.get_wallet_balance() wip_before = story_client.WIP.balance_of(wallet_address) - + # Withdraw all WIP response = story_client.WIP.withdraw( - amount=wip_before, - tx_options={"waitForTransaction": True} + amount=wip_before, tx_options={"waitForTransaction": True} ) - + # Verify transaction hash - assert isinstance(response['tx_hash'], str) - + assert isinstance(response["tx_hash"], str) + # Get balances after withdrawal wip_after = story_client.WIP.balance_of(wallet_address) balance_after = story_client.get_wallet_balance() - + # Verify WIP balance is now zero assert wip_after == 0 - + # Calculate gas cost - tx_receipt = web3.eth.wait_for_transaction_receipt(response["tx_hash"], timeout=300) - gas_cost = tx_receipt['gasUsed'] * tx_receipt['effectiveGasPrice'] - + tx_receipt = web3.eth.wait_for_transaction_receipt( + response["tx_hash"], timeout=300 + ) + gas_cost = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] + # Verify wallet balance increased by withdrawal amount minus gas cost assert balance_after == balance_before + wip_before - gas_cost diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 144590a..24eef74 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -13,135 +13,154 @@ # Mock ERC20 contract address (same as used in TypeScript tests) MockERC20 = "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E" -WIP_TOKEN_ADDRESS = "0x1514000000000000000000000000000000000000"; +WIP_TOKEN_ADDRESS = "0x1514000000000000000000000000000000000000" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -ROYALTY_POLICY="0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" #Royalty Policy LAP -ROYALTY_MODULE="0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086" -PIL_LICENSE_TEMPLATE="0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316" -ARBITRATION_POLICY_UMA="0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936" -EVEN_SPLIT_GROUP_POOL="0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89" -ROYALTY_POLICY_LRP="0x9156e603C949481883B1d3355c6f1132D191fC41" -CORE_METADATA_MODULE="0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16" +ROYALTY_POLICY = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" # Royalty Policy LAP +ROYALTY_MODULE = "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086" +PIL_LICENSE_TEMPLATE = "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316" +ARBITRATION_POLICY_UMA = "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936" +EVEN_SPLIT_GROUP_POOL = "0xf96f2c30b41Cb6e0290de43C8528ae83d4f33F89" +ROYALTY_POLICY_LRP = "0x9156e603C949481883B1d3355c6f1132D191fC41" +CORE_METADATA_MODULE = "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16" + def get_story_client_in_sepolia(web3: Web3, account) -> StoryClient: chain_id = 11155111 # Sepolia chain ID return StoryClient(web3, account, chain_id) + def get_story_client_in_iliad(web3: Web3, account) -> StoryClient: chain_id = 1513 # Sepolia chain ID return StoryClient(web3, account, chain_id) + def get_story_client_in_odyssey(web3: Web3, account) -> StoryClient: chain_id = 1516 # Odyssey chain ID return StoryClient(web3, account, chain_id) + def get_story_client_in_devnet(web3: Web3, account) -> StoryClient: chain_id = 1315 # Devnet chain ID return StoryClient(web3, account, chain_id) + def get_token_id(nft_contract, web3, account): contract_abi = [ { "inputs": [{"internalType": "address", "name": "to", "type": "address"}], "name": "mint", - "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], + "outputs": [ + {"internalType": "uint256", "name": "tokenId", "type": "uint256"} + ], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=nft_contract, abi=contract_abi) - + try: - transaction = contract.functions.mint(account.address).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000 - }) + transaction = contract.functions.mint(account.address).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + } + ) signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - logs = tx_receipt['logs'] - if len(logs) > 0 and len(logs[0]['topics']) > 3: - return int(logs[0]['topics'][3].hex(), 16) + logs = tx_receipt["logs"] + if len(logs) > 0 and len(logs[0]["topics"]) > 3: + return int(logs[0]["topics"][3].hex(), 16) raise ValueError(f"No token ID in logs: {tx_receipt}") - + except Exception as e: raise e + def mint_by_spg(nft_contract, web3, account, metadata_uri=""): contract_abi = [ { "inputs": [ {"internalType": "address", "name": "to", "type": "address"}, {"internalType": "string", "name": "nftMetadataURI", "type": "string"}, - {"internalType": "bytes32", "name": "nftMetadataHash", "type": "bytes32"}, - {"internalType": "bool", "name": "allowDuplicates", "type": "bool"} + { + "internalType": "bytes32", + "name": "nftMetadataHash", + "type": "bytes32", + }, + {"internalType": "bool", "name": "allowDuplicates", "type": "bool"}, ], "name": "mint", - "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], + "outputs": [ + {"internalType": "uint256", "name": "tokenId", "type": "uint256"} + ], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=nft_contract, abi=contract_abi) - + try: - zero_hash = '0x' + '0' * 64 + zero_hash = "0x" + "0" * 64 transaction = contract.functions.mint( - account.address, - metadata_uri, - zero_hash, - True - ).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000 - }) + account.address, metadata_uri, zero_hash, True + ).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + } + ) signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - logs = tx_receipt['logs'] - if len(logs) > 0 and len(logs[0]['topics']) > 3: - return int(logs[0]['topics'][3].hex(), 16) + logs = tx_receipt["logs"] + if len(logs) > 0 and len(logs[0]["topics"]) > 3: + return int(logs[0]["topics"][3].hex(), 16) raise ValueError(f"No token ID in logs: {tx_receipt}") - + except Exception as e: raise e + def mint_tokens(erc20_contract_address, web3, account, to_address, amount): contract_abi = [ { "inputs": [ {"internalType": "address", "name": "to", "type": "address"}, - {"internalType": "uint256", "name": "amount", "type": "uint256"} + {"internalType": "uint256", "name": "amount", "type": "uint256"}, ], "name": "mint", "outputs": [], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=erc20_contract_address, abi=contract_abi) - transaction = contract.functions.mint(to_address, amount).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000, - 'gasPrice': web3.to_wei('300', 'gwei') - }) - + transaction = contract.functions.mint(to_address, amount).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + "gasPrice": web3.to_wei("300", "gwei"), + } + ) + signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - + return tx_receipt + def approve(erc20_contract_address, web3, account, spender_address, amount): erc20_contract_address = web3.to_checksum_address(erc20_contract_address) spender_address = web3.to_checksum_address(spender_address) @@ -150,44 +169,47 @@ def approve(erc20_contract_address, web3, account, spender_address, amount): { "inputs": [ {"internalType": "address", "name": "spender", "type": "address"}, - {"internalType": "uint256", "name": "value", "type": "uint256"} + {"internalType": "uint256", "name": "value", "type": "uint256"}, ], "name": "approve", - "outputs": [ - {"internalType": "bool", "name": "", "type": "bool"} - ], + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", - "type": "function" + "type": "function", } ] contract = web3.eth.contract(address=erc20_contract_address, abi=contract_abi) - transaction = contract.functions.approve(spender_address, amount).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000, - 'gasPrice': web3.to_wei('300', 'gwei') - }) - + transaction = contract.functions.approve(spender_address, amount).build_transaction( + { + "from": account.address, + "nonce": web3.eth.get_transaction_count(account.address), + "gas": 2000000, + "gasPrice": web3.to_wei("300", "gwei"), + } + ) + signed_txn = account.sign_transaction(transaction) tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - + return tx_receipt + def get_block_timestamp(web3): - return (web3.eth.get_block('latest'))['timestamp'] + return (web3.eth.get_block("latest"))["timestamp"] + def check_event_in_tx(web3, tx_hash: str, event_text: str) -> bool: tx_receipt = web3.eth.get_transaction_receipt(tx_hash) event_signature = web3.keccak(text=event_text).hex() - for log in tx_receipt['logs']: - if log['topics'][0].hex() == event_signature: + for log in tx_receipt["logs"]: + if log["topics"][0].hex() == event_signature: return True return False + def generate_cid() -> str: """Generate a random CIDv0 for testing purposes""" # Generate random bytes @@ -197,21 +219,20 @@ def generate_cid() -> str: # Construct CIDv0 (SHA-256 + multihash prefix) multihash = bytes([0x12, 0x20]) + sha256_hash # Base58 encode - return base58.b58encode(multihash).decode('utf-8') + return base58.b58encode(multihash).decode("utf-8") + def setup_royalty_vault(story_client, parent_ip_id, account): - parent_ip_royalty_address = story_client.Royalty.getRoyaltyVaultAddress(parent_ip_id) + parent_ip_royalty_address = story_client.Royalty.getRoyaltyVaultAddress( + parent_ip_id + ) transfer_data = story_client.Royalty.ip_royalty_vault_client.contract.encode_abi( - abi_element_identifier="transfer", - args=[account.address, 10 * 10 ** 6] + abi_element_identifier="transfer", args=[account.address, 10 * 10**6] ) response = story_client.IPAccount.execute( - to=parent_ip_royalty_address, - value=0, - ip_id=parent_ip_id, - data=transfer_data + to=parent_ip_royalty_address, value=0, ip_id=parent_ip_id, data=transfer_data ) return response diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index c5a6e4c..539129d 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1 +1 @@ -# This file makes the tests/unit directory a Python package \ No newline at end of file +# This file makes the tests/unit directory a Python package diff --git a/tests/unit/resources/__init__.py b/tests/unit/resources/__init__.py index 3922977..366ef5c 100644 --- a/tests/unit/resources/__init__.py +++ b/tests/unit/resources/__init__.py @@ -1 +1 @@ -# This file makes the tests/unit/resources directory a Python package \ No newline at end of file +# This file makes the tests/unit/resources directory a Python package diff --git a/tests/unit/resources/test_ip_account.py b/tests/unit/resources/test_ip_account.py index 1f844cb..3317511 100644 --- a/tests/unit/resources/test_ip_account.py +++ b/tests/unit/resources/test_ip_account.py @@ -8,7 +8,7 @@ # Ensure the src directory is in the Python path current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) +src_path = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) if src_path not in sys.path: sys.path.append(src_path) @@ -20,58 +20,70 @@ VALID_IP_ID = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" TX_HASH = "0xe87b172eee35872179ced53ea4f3f314b12cd0f5d0034e7f0ae3c4efce9ba6f1" + # Web3 mock class MockWeb3: def __init__(self): self.eth = MagicMock() - + @staticmethod def to_checksum_address(address): if not is_address(address): raise ValueError(f"Invalid address: {address}") return to_checksum_address(address) - + @staticmethod def to_bytes(hexstr=None, **kwargs): return Web3.to_bytes(hexstr=hexstr, **kwargs) - + @staticmethod def to_wei(number, unit): return Web3.to_wei(number, unit) - + @staticmethod def is_address(address): return is_address(address) - + def is_connected(self): return True - + + @pytest.fixture def mock_web3(): return MockWeb3() + @pytest.fixture def mock_account(): account = MagicMock() account.address = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" return account + @pytest.fixture def ip_account(mock_web3, mock_account): chain_id = 11155111 # Sepolia chain ID return IPAccount(mock_web3, mock_account, chain_id) + @pytest.mark.unit class TestExecute: def test_invalid_recipient_address(self, ip_account): with pytest.raises(ValueError) as exc_info: - ip_account.execute("0xInvalidAddress", 1, VALID_IP_ID, "0x11111111111111111111111111111") - assert "The recipient of the transaction 0xInvalidAddress is not a valid address" in str(exc_info.value) + ip_account.execute( + "0xInvalidAddress", 1, VALID_IP_ID, "0x11111111111111111111111111111" + ) + assert ( + "The recipient of the transaction 0xInvalidAddress is not a valid address" + in str(exc_info.value) + ) def test_unregistered_ip_account(self, ip_account): - with patch.object(ip_account, '_is_registered', return_value=False): + with patch.object(ip_account, "_is_registered", return_value=False): with pytest.raises(ValueError) as exc_info: - ip_account.execute(ZERO_ADDRESS, 1, VALID_IP_ID, "0x11111111111111111111111111111") + ip_account.execute( + ZERO_ADDRESS, 1, VALID_IP_ID, "0x11111111111111111111111111111" + ) assert f"The IP id {VALID_IP_ID} is not registered" in str(exc_info.value) def test_successful_transaction(self, ip_account): @@ -81,7 +93,7 @@ def test_successful_transaction(self, ip_account): # Mock signed transaction mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" # Mock transaction hash with hex method that returns hash class MockTxHash: @@ -90,25 +102,31 @@ def hex(self): mock_tx_hash = MockTxHash() - ip_account.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch.object(ip_account.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_account.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_account.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1}): + with patch.object( + ip_account, "_is_registered", return_value=True + ), patch.object( + ip_account.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_account.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_account.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1}, + ): response = ip_account.execute(to_address, value, VALID_IP_ID, data) - assert response['tx_hash'] == TX_HASH + assert response["tx_hash"] == TX_HASH def test_wait_for_transaction(self, ip_account): to_address = "0xF9936a224b3Deb6f9A4645ccAfa66f7ECe83CF0A" data = "0x11111111111111111111111111111" value = 2 - tx_options = {'waitForTransaction': True} + tx_options = {"waitForTransaction": True} mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" class MockTxHash: def hex(self): @@ -118,53 +136,67 @@ def hex(self): ip_account.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch.object(ip_account.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_account.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_account.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1}): - - response = ip_account.execute(to_address, value, VALID_IP_ID, data, tx_options=tx_options) - assert response['tx_hash'] == TX_HASH + with patch.object( + ip_account, "_is_registered", return_value=True + ), patch.object( + ip_account.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_account.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_account.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1}, + ): + + response = ip_account.execute( + to_address, value, VALID_IP_ID, data, tx_options=tx_options + ) + assert response["tx_hash"] == TX_HASH def test_encoded_tx_data_only(self, ip_account): to_address = "0xF9936a224b3Deb6f9A4645ccAfa66f7ECe83CF0A" data = "0x11111111111111111111111111111" value = 2 - tx_options = {'encodedTxDataOnly': True} + tx_options = {"encodedTxDataOnly": True} encoded_data = "0x123456789" mock_tx = { - 'to': to_address, - 'value': value, - 'data': encoded_data, - 'gas': 2000000, - 'gasPrice': Web3.to_wei('50', 'gwei'), - 'nonce': 0, - 'chainId': 11155111 + "to": to_address, + "value": value, + "data": encoded_data, + "gas": 2000000, + "gasPrice": Web3.to_wei("50", "gwei"), + "nonce": 0, + "chainId": 11155111, } - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch('story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.build_execute_transaction', - return_value=mock_tx), \ - patch('web3.eth.Eth.get_transaction_count', - return_value=0): + with patch.object(ip_account, "_is_registered", return_value=True), patch( + "story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.build_execute_transaction", + return_value=mock_tx, + ), patch("web3.eth.Eth.get_transaction_count", return_value=0): + + response = ip_account.execute( + to_address, value, VALID_IP_ID, data, tx_options=tx_options + ) + assert "encodedTxData" in response + assert response["encodedTxData"] == mock_tx - response = ip_account.execute(to_address, value, VALID_IP_ID, data, tx_options=tx_options) - assert 'encodedTxData' in response - assert response['encodedTxData'] == mock_tx class TestExecuteWithSig: def test_invalid_recipient_address(self, ip_account): with pytest.raises(ValueError) as exc_info: ip_account.execute_with_sig( - VALID_IP_ID, - "0xInvalidAddress", + VALID_IP_ID, + "0xInvalidAddress", "0x11111111111111111111111111111", ZERO_ADDRESS, 20, - ZERO_ADDRESS + ZERO_ADDRESS, ) - assert "The recipient of the transaction 0xInvalidAddress is not a valid address" in str(exc_info.value) + assert ( + "The recipient of the transaction 0xInvalidAddress is not a valid address" + in str(exc_info.value) + ) def test_successful_transaction_with_sig(self, ip_account): to_address = "0xF9936a224b3Deb6f9A4645ccAfa66f7ECe83CF0A" @@ -175,23 +207,32 @@ def test_successful_transaction_with_sig(self, ip_account): signature = "0x11111111111111111111111111111" mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" class MockTxHash: def hex(self): return TX_HASH mock_tx_hash = MockTxHash() - + ip_account.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch.object(ip_account.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_account.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_account.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1}): + with patch.object( + ip_account, "_is_registered", return_value=True + ), patch.object( + ip_account.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_account.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_account.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1}, + ): - response = ip_account.execute_with_sig(VALID_IP_ID, to_address, data, signer, deadline, signature, value) - assert response['tx_hash'] == TX_HASH + response = ip_account.execute_with_sig( + VALID_IP_ID, to_address, data, signer, deadline, signature, value + ) + assert response["tx_hash"] == TX_HASH def test_wait_for_transaction_with_sig(self, ip_account): to_address = "0xF9936a224b3Deb6f9A4645ccAfa66f7ECe83CF0A" @@ -199,29 +240,42 @@ def test_wait_for_transaction_with_sig(self, ip_account): signer = ZERO_ADDRESS deadline = 20 signature = ZERO_ADDRESS - tx_options = {'waitForTransaction': True} + tx_options = {"waitForTransaction": True} mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' - + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" + class MockTxHash: def hex(self): return TX_HASH mock_tx_hash = MockTxHash() - + ip_account.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch.object(ip_account.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_account.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_account.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1}): + with patch.object( + ip_account, "_is_registered", return_value=True + ), patch.object( + ip_account.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_account.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_account.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1}, + ): response = ip_account.execute_with_sig( - VALID_IP_ID, to_address, data, signer, deadline, signature, - tx_options=tx_options + VALID_IP_ID, + to_address, + data, + signer, + deadline, + signature, + tx_options=tx_options, ) - assert response['tx_hash'] == TX_HASH + assert response["tx_hash"] == TX_HASH + class TestGetIpAccountNonce: def test_invalid_ip_id(self, ip_account): @@ -233,12 +287,14 @@ def test_successful_nonce_retrieval(self, ip_account): ip_id = Web3.to_checksum_address("0x73fcb515cee99e4991465ef586cfe2b072ebb512") expected_nonce = 1 - with patch('story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.state', - return_value=expected_nonce), \ - patch('web3.eth.Eth.contract', return_value=MagicMock()): + with patch( + "story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.state", + return_value=expected_nonce, + ), patch("web3.eth.Eth.contract", return_value=MagicMock()): nonce = ip_account.get_ip_account_nonce(ip_id) assert nonce == expected_nonce + class TestGetToken: def test_invalid_ip_id(self, ip_account): with pytest.raises(ValueError) as exc_info: @@ -248,129 +304,161 @@ def test_invalid_ip_id(self, ip_account): def test_successful_token_retrieval(self, ip_account): ip_id = Web3.to_checksum_address("0x73fcb515cee99e4991465ef586cfe2b072ebb512") expected_token = { - 'chain_id': 1513, - 'token_contract': ZERO_ADDRESS, - 'token_id': 1 + "chain_id": 1513, + "token_contract": ZERO_ADDRESS, + "token_id": 1, } - with patch('story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.token', - return_value=[1513, ZERO_ADDRESS, 1]), \ - patch('web3.eth.Eth.contract', return_value=MagicMock()): + with patch( + "story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.token", + return_value=[1513, ZERO_ADDRESS, 1], + ), patch("web3.eth.Eth.contract", return_value=MagicMock()): token = ip_account.get_token(ip_id) assert token == expected_token + class TestTransferERC20: def test_unregistered_ip_id(self, ip_account): - with patch.object(ip_account, '_is_registered', return_value=False): + with patch.object(ip_account, "_is_registered", return_value=False): with pytest.raises(ValueError) as exc_info: ip_account.transfer_erc20( ip_id=VALID_IP_ID, - tokens=[{ - 'address': ZERO_ADDRESS, - 'target': ZERO_ADDRESS, - 'amount': 1000 - }] + tokens=[ + { + "address": ZERO_ADDRESS, + "target": ZERO_ADDRESS, + "amount": 1000, + } + ], ) assert f"IP id {VALID_IP_ID} is not registered" in str(exc_info.value) - + def test_invalid_token_params(self, ip_account): - with patch.object(ip_account, '_is_registered', return_value=True): + with patch.object(ip_account, "_is_registered", return_value=True): with pytest.raises(ValueError) as exc_info: ip_account.transfer_erc20( ip_id=VALID_IP_ID, - tokens=[{ - # Missing 'address' - 'target': ZERO_ADDRESS, - 'amount': 1000 - }] + tokens=[ + { + # Missing 'address' + "target": ZERO_ADDRESS, + "amount": 1000, + } + ], ) - assert "Each token transfer must include 'address', 'target', and 'amount'" in str(exc_info.value) - + assert ( + "Each token transfer must include 'address', 'target', and 'amount'" + in str(exc_info.value) + ) + def test_successful_transfer(self, ip_account): tokens = [ { - 'address': "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", - 'target': "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c", - 'amount': 1000000 + "address": "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + "target": "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c", + "amount": 1000000, }, { - 'address': "0x2daAE3197Bc469Cb97B917aa460a12dD95c6627c", - 'target': "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c", - 'amount': 2000000 - } + "address": "0x2daAE3197Bc469Cb97B917aa460a12dD95c6627c", + "target": "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c", + "amount": 2000000, + }, ] - + mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' - + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" + class MockTxHash: def hex(self): return TX_HASH - + mock_tx_hash = MockTxHash() - + ip_account.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch.object(ip_account.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_account.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_account.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1}): - + + with patch.object( + ip_account, "_is_registered", return_value=True + ), patch.object( + ip_account.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_account.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_account.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1}, + ): + response = ip_account.transfer_erc20(VALID_IP_ID, tokens) - assert response['tx_hash'] == TX_HASH + assert response["tx_hash"] == TX_HASH + class TestSetIPMetadata: def test_unregistered_ip_id(self, ip_account): - with patch.object(ip_account, '_is_registered', return_value=False): + with patch.object(ip_account, "_is_registered", return_value=False): with pytest.raises(ValueError) as exc_info: ip_account.set_ip_metadata( ip_id=VALID_IP_ID, metadata_uri="ipfs://example", - metadata_hash=ZERO_HASH + metadata_hash=ZERO_HASH, ) assert f"IP id {VALID_IP_ID} is not registered" in str(exc_info.value) - + def test_successful_metadata_update(self, ip_account): metadata_uri = "ipfs://example" metadata_hash = ZERO_HASH - + mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' - + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" + class MockTxHash: def hex(self): return TX_HASH - + mock_tx_hash = MockTxHash() - + # Create a mock contract with a valid address mock_contract = MagicMock() - mock_contract.address = "0xF9936a224b3Deb6f9A4645ccAfa66f7ECe83CF0A" # Use a valid address - + mock_contract.address = ( + "0xF9936a224b3Deb6f9A4645ccAfa66f7ECe83CF0A" # Use a valid address + ) + ip_account.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - - with patch.object(ip_account, '_is_registered', return_value=True), \ - patch.object(ip_account.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_account.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_account.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1}), \ - patch.object(ip_account.web3.eth, 'contract', return_value=mock_contract), \ - patch.object(ip_account.web3, 'is_address', return_value=True): - - response = ip_account.set_ip_metadata(VALID_IP_ID, metadata_uri, metadata_hash) - assert response['tx_hash'] == TX_HASH + + with patch.object( + ip_account, "_is_registered", return_value=True + ), patch.object( + ip_account.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_account.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_account.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1}, + ), patch.object( + ip_account.web3.eth, "contract", return_value=mock_contract + ), patch.object( + ip_account.web3, "is_address", return_value=True + ): + + response = ip_account.set_ip_metadata( + VALID_IP_ID, metadata_uri, metadata_hash + ) + assert response["tx_hash"] == TX_HASH + class TestOwner: def test_invalid_ip_id(self, ip_account): with pytest.raises(ValueError) as exc_info: ip_account.owner("0x123") # invalid address assert "Invalid IP id address" in str(exc_info.value) - + def test_successful_owner_retrieval(self, ip_account): ip_id = Web3.to_checksum_address("0x73fcb515cee99e4991465ef586cfe2b072ebb512") expected_owner = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" - - with patch('story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.owner', - return_value=expected_owner), \ - patch('web3.eth.Eth.contract', return_value=MagicMock()): + + with patch( + "story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient.owner", + return_value=expected_owner, + ), patch("web3.eth.Eth.contract", return_value=MagicMock()): owner = ip_account.owner(ip_id) assert owner == expected_owner diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index b57525b..7c041df 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -6,7 +6,7 @@ from eth_utils import is_address, to_checksum_address current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) +src_path = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) if src_path not in sys.path: sys.path.append(src_path) @@ -15,63 +15,68 @@ ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000" ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + class MockWeb3: def __init__(self): self.eth = MagicMock() - + @staticmethod def to_checksum_address(address): if not is_address(address): raise ValueError(f"Invalid address: {address}") return to_checksum_address(address) - + @staticmethod def to_bytes(hexstr=None, **kwargs): return Web3.to_bytes(hexstr=hexstr, **kwargs) - + @staticmethod def to_wei(number, unit): return Web3.to_wei(number, unit) - + @staticmethod def is_address(address): return is_address(address) - + @staticmethod def keccak(text=None): return Web3.keccak(text=text) - + def is_connected(self): return True + @pytest.fixture def mock_web3(): return MockWeb3() + @pytest.fixture def mock_account(): account = MagicMock() account.address = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" return account + @pytest.fixture def ip_asset(mock_web3, mock_account): chain_id = 1516 return IPAsset(mock_web3, mock_account, chain_id) + class TestIPAssetRegister: def test_register_invalid_deadline_type(self, ip_asset): - with patch.object(ip_asset, '_get_ip_id', return_value="0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4"), \ - patch.object(ip_asset, '_is_registered', return_value=False): + with patch.object( + ip_asset, + "_get_ip_id", + return_value="0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4", + ), patch.object(ip_asset, "_is_registered", return_value=False): with pytest.raises(ValueError): ip_asset.register( nft_contract="0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", token_id=3, deadline="error", - ip_metadata={ - 'ip_metadata_uri': "1", - 'ip_metadata_hash': ZERO_HASH - } + ip_metadata={"ip_metadata_uri": "1", "ip_metadata_hash": ZERO_HASH}, ) def test_register_already_registered(self, ip_asset): @@ -79,11 +84,12 @@ def test_register_already_registered(self, ip_asset): token_id = 3 ip_id = "0xd142822Dc1674154EaF4DDF38bbF7EF8f0D8ECe4" - with patch.object(ip_asset, '_get_ip_id', return_value=ip_id), \ - patch.object(ip_asset, '_is_registered', return_value=True): + with patch.object(ip_asset, "_get_ip_id", return_value=ip_id), patch.object( + ip_asset, "_is_registered", return_value=True + ): response = ip_asset.register(token_contract, token_id) - assert response['ip_id'] == ip_id - assert response['tx_hash'] is None + assert response["ip_id"] == ip_id + assert response["tx_hash"] is None def test_register_successful(self, ip_asset): token_contract = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" @@ -91,7 +97,6 @@ def test_register_successful(self, ip_asset): ip_id = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" tx_hash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997" - class MockTxHash: def hex(self): return tx_hash @@ -99,21 +104,27 @@ def hex(self): mock_tx_hash = MockTxHash() mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" - ip_asset.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - with patch.object(ip_asset, '_get_ip_id', return_value=ip_id), \ - patch.object(ip_asset, '_is_registered', return_value=False), \ - patch.object(ip_asset.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_asset.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_asset.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1, 'logs': []}), \ - patch.object(ip_asset, '_parse_tx_ip_registered_event', return_value={'ip_id': ip_id}): + with patch.object(ip_asset, "_get_ip_id", return_value=ip_id), patch.object( + ip_asset, "_is_registered", return_value=False + ), patch.object( + ip_asset.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_asset.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_asset.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1, "logs": []}, + ), patch.object( + ip_asset, "_parse_tx_ip_registered_event", return_value={"ip_id": ip_id} + ): result = ip_asset.register(token_contract, token_id) - assert result['tx_hash'] == tx_hash - assert result['ip_id'] == ip_id + assert result["tx_hash"] == tx_hash + assert result["ip_id"] == ip_id def test_register_with_metadata(self, ip_asset): token_contract = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" @@ -122,10 +133,10 @@ def test_register_with_metadata(self, ip_asset): tx_hash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997" metadata = { - 'ip_metadata_uri': "", - 'ip_metadata_hash': ZERO_HASH, - 'nft_metadata_uri': "", - 'nft_metadata_hash': ZERO_HASH, + "ip_metadata_uri": "", + "ip_metadata_hash": ZERO_HASH, + "nft_metadata_uri": "", + "nft_metadata_hash": ZERO_HASH, } calculated_deadline = 1000 @@ -137,25 +148,38 @@ def hex(self): mock_tx_hash = MockTxHash() mock_signed_txn = MagicMock() - mock_signed_txn.raw_transaction = b'raw_transaction_bytes' + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" ip_asset.account.sign_transaction = MagicMock(return_value=mock_signed_txn) - with patch.object(ip_asset, '_get_ip_id', return_value=ip_id), \ - patch.object(ip_asset, '_is_registered', return_value=False), \ - patch.object(ip_asset.sign_util, 'get_deadline', return_value=calculated_deadline), \ - patch.object(ip_asset.sign_util, 'get_permission_signature', return_value={"signature": tx_hash}), \ - patch.object(ip_asset.web3.eth, 'get_transaction_count', return_value=0), \ - patch.object(ip_asset.web3.eth, 'send_raw_transaction', return_value=mock_tx_hash), \ - patch.object(ip_asset.web3.eth, 'wait_for_transaction_receipt', return_value={'status': 1, 'logs': []}), \ - patch.object(ip_asset, '_parse_tx_ip_registered_event', return_value={'ip_id': ip_id, 'token_id': token_id}): + with patch.object(ip_asset, "_get_ip_id", return_value=ip_id), patch.object( + ip_asset, "_is_registered", return_value=False + ), patch.object( + ip_asset.sign_util, "get_deadline", return_value=calculated_deadline + ), patch.object( + ip_asset.sign_util, + "get_permission_signature", + return_value={"signature": tx_hash}, + ), patch.object( + ip_asset.web3.eth, "get_transaction_count", return_value=0 + ), patch.object( + ip_asset.web3.eth, "send_raw_transaction", return_value=mock_tx_hash + ), patch.object( + ip_asset.web3.eth, + "wait_for_transaction_receipt", + return_value={"status": 1, "logs": []}, + ), patch.object( + ip_asset, + "_parse_tx_ip_registered_event", + return_value={"ip_id": ip_id, "token_id": token_id}, + ): result = ip_asset.register( nft_contract=token_contract, token_id=token_id, ip_metadata=metadata, - deadline=1000 + deadline=1000, ) - assert result['tx_hash'] == tx_hash - assert result['ip_id'] == ip_id \ No newline at end of file + assert result["tx_hash"] == tx_hash + assert result["ip_id"] == ip_id diff --git a/tests/unit/resources/test_license.py b/tests/unit/resources/test_license.py index 425b693..259e43c 100644 --- a/tests/unit/resources/test_license.py +++ b/tests/unit/resources/test_license.py @@ -7,7 +7,7 @@ from eth_utils import is_address, to_checksum_address current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) +src_path = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) if src_path not in sys.path: sys.path.append(src_path) @@ -17,103 +17,142 @@ VALID_ADDRESS = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" TX_HASH = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997" + class MockWeb3: """Mock Web3 instance with required functionality.""" + def __init__(self): self.eth = MagicMock() - + @staticmethod def to_checksum_address(address): if not is_address(address): raise ValueError(f'Address "{address}" is invalid') return to_checksum_address(address) - + @staticmethod def to_bytes(hexstr=None, **kwargs): return Web3.to_bytes(hexstr=hexstr, **kwargs) - + @staticmethod def to_wei(number, unit): return Web3.to_wei(number, unit) - + @staticmethod def is_address(address): return is_address(address) - + @staticmethod def keccak(text=None, hexstr=None, primitive=None): return Web3.keccak(text=text, hexstr=hexstr) - + def is_connected(self): return True + class MockTxHash: """Mock transaction hash that returns hash without '0x' prefix.""" + def hex(self): return TX_HASH[2:] + @pytest.fixture def mock_web3(): return MockWeb3() + @pytest.fixture def mock_account(): account = MagicMock() account.address = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" return account + @pytest.fixture def mock_signed_txn(): signed_txn = MagicMock() - signed_txn.rawTransaction = b'signed_tx' + signed_txn.rawTransaction = b"signed_tx" return signed_txn + @pytest.fixture def license_client(mock_web3, mock_account): - chain_id = 1315 + chain_id = 1315 return License(mock_web3, mock_account, chain_id) + class TestPILTermsRegistration: """Tests for PIL (Programmable IP License) terms registration.""" def test_register_pil_terms_license_terms_id_registered(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=1), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyToken', return_value=True): - + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=1 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyToken", + return_value=True, + ): + license_terms = { - 'default_minting_fee': 1513, - 'currency': VALID_ADDRESS, - 'royalty_policy': VALID_ADDRESS, - 'transferable': False, - 'expiration': 0, - 'commercial_use': True, - 'commercial_attribution': False, - 'commercializer_checker': ZERO_ADDRESS, - 'commercializer_checker_data': '0x', - 'commercial_rev_share': 0, - 'commercial_rev_ceiling': 0, - 'derivatives_allowed': False, - 'derivatives_attribution': False, - 'derivatives_approval': False, - 'derivatives_reciprocal': False, - 'derivative_rev_ceiling': 0, - 'uri': '' + "default_minting_fee": 1513, + "currency": VALID_ADDRESS, + "royalty_policy": VALID_ADDRESS, + "transferable": False, + "expiration": 0, + "commercial_use": True, + "commercial_attribution": False, + "commercializer_checker": ZERO_ADDRESS, + "commercializer_checker_data": "0x", + "commercial_rev_share": 0, + "commercial_rev_ceiling": 0, + "derivatives_allowed": False, + "derivatives_attribution": False, + "derivatives_approval": False, + "derivatives_reciprocal": False, + "derivative_rev_ceiling": 0, + "uri": "", } - + response = license_client.register_pil_terms(**license_terms) - assert response['license_terms_id'] == 1 - assert 'tx_hash' not in response - - def test_register_pil_terms_success(self, license_client, mock_signed_txn, mock_account): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyToken', return_value=True), \ - patch.object(license_client.license_template_client, 'build_registerLicenseTerms_transaction', - return_value={'from': mock_account.address, 'nonce': 1, 'gas': 2000000, 'gasPrice': Web3.to_wei('100', 'gwei')}), \ - patch.object(mock_account, 'sign_transaction', return_value=mock_signed_txn), \ - patch.object(license_client.web3.eth, 'send_raw_transaction', return_value=MockTxHash()), \ - patch.object(license_client.web3.eth, 'wait_for_transaction_receipt', return_value=MagicMock()): + assert response["license_terms_id"] == 1 + assert "tx_hash" not in response + + def test_register_pil_terms_success( + self, license_client, mock_signed_txn, mock_account + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyToken", + return_value=True, + ), patch.object( + license_client.license_template_client, + "build_registerLicenseTerms_transaction", + return_value={ + "from": mock_account.address, + "nonce": 1, + "gas": 2000000, + "gasPrice": Web3.to_wei("100", "gwei"), + }, + ), patch.object( + mock_account, "sign_transaction", return_value=mock_signed_txn + ), patch.object( + license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() + ), patch.object( + license_client.web3.eth, + "wait_for_transaction_receipt", + return_value=MagicMock(), + ): response = license_client.register_pil_terms( transferable=False, @@ -123,7 +162,7 @@ def test_register_pil_terms_success(self, license_client, mock_signed_txn, mock_ commercial_use=True, commercial_attribution=False, commercializer_checker=ZERO_ADDRESS, - commercializer_checker_data='0x', + commercializer_checker_data="0x", commercial_rev_share=90, commercial_rev_ceiling=0, derivatives_allowed=False, @@ -132,19 +171,31 @@ def test_register_pil_terms_success(self, license_client, mock_signed_txn, mock_ derivatives_reciprocal=False, derivative_rev_ceiling=0, currency=VALID_ADDRESS, - uri='' + uri="", ) - - assert 'tx_hash' in response - assert response['tx_hash'] == TX_HASH[2:] - assert isinstance(response['tx_hash'], str) - - def test_register_pil_terms_commercial_rev_share_error_more_than_100(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyToken', return_value=True): - with pytest.raises(ValueError, match='CommercialRevShare should be between 0 and 100.'): + assert "tx_hash" in response + assert response["tx_hash"] == TX_HASH[2:] + assert isinstance(response["tx_hash"], str) + + def test_register_pil_terms_commercial_rev_share_error_more_than_100( + self, license_client + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyToken", + return_value=True, + ): + + with pytest.raises( + ValueError, match="CommercialRevShare should be between 0 and 100." + ): license_client.register_pil_terms( transferable=False, royalty_policy=VALID_ADDRESS, @@ -153,7 +204,7 @@ def test_register_pil_terms_commercial_rev_share_error_more_than_100(self, licen commercial_use=True, commercial_attribution=False, commercializer_checker=ZERO_ADDRESS, - commercializer_checker_data='0x', + commercializer_checker_data="0x", commercial_rev_share=101, commercial_rev_ceiling=0, derivatives_allowed=False, @@ -162,15 +213,27 @@ def test_register_pil_terms_commercial_rev_share_error_more_than_100(self, licen derivatives_reciprocal=False, derivative_rev_ceiling=0, currency=VALID_ADDRESS, - uri='' + uri="", ) - def test_register_pil_terms_commercial_rev_share_error_less_than_0(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyToken', return_value=True): - - with pytest.raises(ValueError, match='CommercialRevShare should be between 0 and 100.'): + def test_register_pil_terms_commercial_rev_share_error_less_than_0( + self, license_client + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyToken", + return_value=True, + ): + + with pytest.raises( + ValueError, match="CommercialRevShare should be between 0 and 100." + ): license_client.register_pil_terms( transferable=False, royalty_policy=VALID_ADDRESS, @@ -179,7 +242,7 @@ def test_register_pil_terms_commercial_rev_share_error_less_than_0(self, license commercial_use=True, commercial_attribution=False, commercializer_checker=ZERO_ADDRESS, - commercializer_checker_data='0x', + commercializer_checker_data="0x", commercial_rev_share=-1, commercial_rev_ceiling=0, derivatives_allowed=False, @@ -188,390 +251,591 @@ def test_register_pil_terms_commercial_rev_share_error_less_than_0(self, license derivatives_reciprocal=False, derivative_rev_ceiling=0, currency=VALID_ADDRESS, - uri='' + uri="", ) + + class TestNonComSocialRemixingPIL: """Tests for non-commercial social remixing PIL functionality.""" - def test_register_non_com_social_remixing_pil_license_terms_id_registered(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=1): + def test_register_non_com_social_remixing_pil_license_terms_id_registered( + self, license_client + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=1 + ): response = license_client.register_non_com_social_remixing_pil() - assert response['license_terms_id'] == 1 - assert 'tx_hash' not in response - - def test_register_non_com_social_remixing_pil_success(self, license_client, mock_signed_txn, mock_account): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_template_client, 'build_registerLicenseTerms_transaction', - return_value={'from': mock_account.address, 'nonce': 1, 'gas': 2000000, 'gasPrice': Web3.to_wei('100', 'gwei')}), \ - patch.object(mock_account, 'sign_transaction', return_value=mock_signed_txn), \ - patch.object(license_client.web3.eth, 'send_raw_transaction', return_value=MockTxHash()), \ - patch.object(license_client.web3.eth, 'get_transaction_count', return_value=1), \ - patch.object(license_client.web3.eth, 'wait_for_transaction_receipt', return_value=MagicMock()), \ - patch.object(license_client, '_parse_tx_license_terms_registered_event', return_value=1): + assert response["license_terms_id"] == 1 + assert "tx_hash" not in response + + def test_register_non_com_social_remixing_pil_success( + self, license_client, mock_signed_txn, mock_account + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_template_client, + "build_registerLicenseTerms_transaction", + return_value={ + "from": mock_account.address, + "nonce": 1, + "gas": 2000000, + "gasPrice": Web3.to_wei("100", "gwei"), + }, + ), patch.object( + mock_account, "sign_transaction", return_value=mock_signed_txn + ), patch.object( + license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() + ), patch.object( + license_client.web3.eth, "get_transaction_count", return_value=1 + ), patch.object( + license_client.web3.eth, + "wait_for_transaction_receipt", + return_value=MagicMock(), + ), patch.object( + license_client, "_parse_tx_license_terms_registered_event", return_value=1 + ): response = license_client.register_non_com_social_remixing_pil() - assert 'tx_hash' in response - assert response['tx_hash'] == TX_HASH[2:] - assert isinstance(response['tx_hash'], str) - assert 'license_terms_id' in response - assert response['license_terms_id'] == 1 + assert "tx_hash" in response + assert response["tx_hash"] == TX_HASH[2:] + assert isinstance(response["tx_hash"], str) + assert "license_terms_id" in response + assert response["license_terms_id"] == 1 def test_register_non_com_social_remixing_pil_error(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_template_client, 'build_registerLicenseTerms_transaction', - side_effect=Exception("request fail.")): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_template_client, + "build_registerLicenseTerms_transaction", + side_effect=Exception("request fail."), + ): with pytest.raises(Exception, match="request fail."): license_client.register_non_com_social_remixing_pil() + class TestCommercialUsePIL: """Tests for commercial use PIL functionality.""" - - def test_register_commercial_use_pil_license_terms_id_registered(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=1): + + def test_register_commercial_use_pil_license_terms_id_registered( + self, license_client + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=1 + ): response = license_client.register_commercial_use_pil( - default_minting_fee=1, - currency=ZERO_ADDRESS + default_minting_fee=1, currency=ZERO_ADDRESS ) - assert response['license_terms_id'] == 1 - assert 'tx_hash' not in response - - def test_register_commercial_use_pil_success(self, license_client, mock_signed_txn, mock_account): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_template_client, 'build_registerLicenseTerms_transaction', - return_value={'from': mock_account.address, 'nonce': 1, 'gas': 2000000, 'gasPrice': Web3.to_wei('100', 'gwei')}), \ - patch.object(mock_account, 'sign_transaction', return_value=mock_signed_txn), \ - patch.object(license_client.web3.eth, 'send_raw_transaction', return_value=MockTxHash()), \ - patch.object(license_client.web3.eth, 'get_transaction_count', return_value=1), \ - patch.object(license_client.web3.eth, 'wait_for_transaction_receipt', return_value=MagicMock()), \ - patch.object(license_client, '_parse_tx_license_terms_registered_event', return_value=1): + assert response["license_terms_id"] == 1 + assert "tx_hash" not in response + + def test_register_commercial_use_pil_success( + self, license_client, mock_signed_txn, mock_account + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_template_client, + "build_registerLicenseTerms_transaction", + return_value={ + "from": mock_account.address, + "nonce": 1, + "gas": 2000000, + "gasPrice": Web3.to_wei("100", "gwei"), + }, + ), patch.object( + mock_account, "sign_transaction", return_value=mock_signed_txn + ), patch.object( + license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() + ), patch.object( + license_client.web3.eth, "get_transaction_count", return_value=1 + ), patch.object( + license_client.web3.eth, + "wait_for_transaction_receipt", + return_value=MagicMock(), + ), patch.object( + license_client, "_parse_tx_license_terms_registered_event", return_value=1 + ): response = license_client.register_commercial_use_pil( - default_minting_fee=1, - currency=ZERO_ADDRESS + default_minting_fee=1, currency=ZERO_ADDRESS ) - assert 'tx_hash' in response - assert response['tx_hash'] == TX_HASH[2:] - assert isinstance(response['tx_hash'], str) - assert 'license_terms_id' in response - assert response['license_terms_id'] == 1 + assert "tx_hash" in response + assert response["tx_hash"] == TX_HASH[2:] + assert isinstance(response["tx_hash"], str) + assert "license_terms_id" in response + assert response["license_terms_id"] == 1 def test_register_commercial_use_pil_error(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_template_client, 'build_registerLicenseTerms_transaction', - side_effect=Exception("request fail.")): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_template_client, + "build_registerLicenseTerms_transaction", + side_effect=Exception("request fail."), + ): with pytest.raises(Exception, match="request fail."): license_client.register_commercial_use_pil( - default_minting_fee=1, - currency=ZERO_ADDRESS + default_minting_fee=1, currency=ZERO_ADDRESS ) + class TestCommercialRemixPIL: """Tests for commercial remix PIL functionality.""" - def test_register_commercial_remix_pil_license_terms_id_registered(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=1), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True): + def test_register_commercial_remix_pil_license_terms_id_registered( + self, license_client + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=1 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ): response = license_client.register_commercial_remix_pil( default_minting_fee=1, commercial_rev_share=100, currency=ZERO_ADDRESS, - royalty_policy=ZERO_ADDRESS + royalty_policy=ZERO_ADDRESS, ) - assert response['license_terms_id'] == 1 - assert 'tx_hash' not in response - - def test_register_commercial_remix_pil_success(self, license_client, mock_signed_txn, mock_account): - with patch.object(license_client.license_template_client, 'getLicenseTermsId', return_value=0), \ - patch.object(license_client.license_terms_util.royalty_module_client, 'isWhitelistedRoyaltyPolicy', return_value=True), \ - patch.object(license_client.license_template_client, 'build_registerLicenseTerms_transaction', - return_value={'from': mock_account.address, 'nonce': 1, 'gas': 2000000, 'gasPrice': Web3.to_wei('100', 'gwei')}), \ - patch.object(mock_account, 'sign_transaction', return_value=mock_signed_txn), \ - patch.object(license_client.web3.eth, 'send_raw_transaction', return_value=MockTxHash()), \ - patch.object(license_client.web3.eth, 'get_transaction_count', return_value=1), \ - patch.object(license_client.web3.eth, 'wait_for_transaction_receipt', return_value=MagicMock()), \ - patch.object(license_client, '_parse_tx_license_terms_registered_event', return_value=1): + assert response["license_terms_id"] == 1 + assert "tx_hash" not in response + + def test_register_commercial_remix_pil_success( + self, license_client, mock_signed_txn, mock_account + ): + with patch.object( + license_client.license_template_client, "getLicenseTermsId", return_value=0 + ), patch.object( + license_client.license_terms_util.royalty_module_client, + "isWhitelistedRoyaltyPolicy", + return_value=True, + ), patch.object( + license_client.license_template_client, + "build_registerLicenseTerms_transaction", + return_value={ + "from": mock_account.address, + "nonce": 1, + "gas": 2000000, + "gasPrice": Web3.to_wei("100", "gwei"), + }, + ), patch.object( + mock_account, "sign_transaction", return_value=mock_signed_txn + ), patch.object( + license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() + ), patch.object( + license_client.web3.eth, "get_transaction_count", return_value=1 + ), patch.object( + license_client.web3.eth, + "wait_for_transaction_receipt", + return_value=MagicMock(), + ), patch.object( + license_client, "_parse_tx_license_terms_registered_event", return_value=1 + ): response = license_client.register_commercial_remix_pil( default_minting_fee=1, commercial_rev_share=100, currency=ZERO_ADDRESS, - royalty_policy=ZERO_ADDRESS + royalty_policy=ZERO_ADDRESS, ) - assert 'tx_hash' in response - assert response['tx_hash'] == TX_HASH[2:] - assert isinstance(response['tx_hash'], str) - assert response['license_terms_id'] == 1 + assert "tx_hash" in response + assert response["tx_hash"] == TX_HASH[2:] + assert isinstance(response["tx_hash"], str) + assert response["license_terms_id"] == 1 + class TestLicenseAttachment: """Tests for license attachment functionality.""" def test_attach_license_terms_ip_not_registered(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=False): - with pytest.raises(ValueError, match=f'The IP with id {ZERO_ADDRESS} is not registered.'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=False + ): + with pytest.raises( + ValueError, match=f"The IP with id {ZERO_ADDRESS} is not registered." + ): license_client.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, - license_terms_id=1 + license_terms_id=1, ) def test_attach_license_terms_license_terms_not_exist(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.license_registry_client, 'exists', return_value=False): - with pytest.raises(ValueError, match='License terms id 1 do not exist.'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.license_registry_client, "exists", return_value=False + ): + with pytest.raises(ValueError, match="License terms id 1 do not exist."): license_client.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, - license_terms_id=1 + license_terms_id=1, ) def test_attach_license_terms_already_attached(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.license_registry_client, 'exists', return_value=True), \ - patch.object(license_client.license_registry_client, 'hasIpAttachedLicenseTerms', return_value=True): - with pytest.raises(ValueError, - match=f'License terms id 1 is already attached to the IP with id {ZERO_ADDRESS}.'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.license_registry_client, "exists", return_value=True + ), patch.object( + license_client.license_registry_client, + "hasIpAttachedLicenseTerms", + return_value=True, + ): + with pytest.raises( + ValueError, + match=f"License terms id 1 is already attached to the IP with id {ZERO_ADDRESS}.", + ): license_client.attach_license_terms( ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, - license_terms_id=1 + license_terms_id=1, ) - def test_attach_license_terms_success(self, license_client, mock_signed_txn, mock_account): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.license_registry_client, 'exists', return_value=True), \ - patch.object(license_client.license_registry_client, 'hasIpAttachedLicenseTerms', return_value=False), \ - patch.object(license_client.licensing_module_client, 'build_attachLicenseTerms_transaction', - return_value={'from': mock_account.address, 'nonce': 1, 'gas': 2000000, 'gasPrice': Web3.to_wei('100', 'gwei')}), \ - patch.object(mock_account, 'sign_transaction', return_value=mock_signed_txn), \ - patch.object(license_client.web3.eth, 'send_raw_transaction', return_value=MockTxHash()), \ - patch.object(license_client.web3.eth, 'get_transaction_count', return_value=1), \ - patch.object(license_client.web3.eth, 'wait_for_transaction_receipt', return_value=MagicMock()), \ - patch.object(license_client, '_parse_tx_license_terms_registered_event', return_value=1): + def test_attach_license_terms_success( + self, license_client, mock_signed_txn, mock_account + ): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.license_registry_client, "exists", return_value=True + ), patch.object( + license_client.license_registry_client, + "hasIpAttachedLicenseTerms", + return_value=False, + ), patch.object( + license_client.licensing_module_client, + "build_attachLicenseTerms_transaction", + return_value={ + "from": mock_account.address, + "nonce": 1, + "gas": 2000000, + "gasPrice": Web3.to_wei("100", "gwei"), + }, + ), patch.object( + mock_account, "sign_transaction", return_value=mock_signed_txn + ), patch.object( + license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() + ), patch.object( + license_client.web3.eth, "get_transaction_count", return_value=1 + ), patch.object( + license_client.web3.eth, + "wait_for_transaction_receipt", + return_value=MagicMock(), + ), patch.object( + license_client, "_parse_tx_license_terms_registered_event", return_value=1 + ): response = license_client.attach_license_terms( - ip_id=ZERO_ADDRESS, - license_template=ZERO_ADDRESS, - license_terms_id=1 + ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1 ) - assert 'tx_hash' in response - assert response['tx_hash'] == TX_HASH[2:] - assert isinstance(response['tx_hash'], str) + assert "tx_hash" in response + assert response["tx_hash"] == TX_HASH[2:] + assert isinstance(response["tx_hash"], str) + class TestLicenseTokens: """Tests for license token minting functionality.""" def test_mint_license_tokens_licensor_ip_not_registered(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=False): - with pytest.raises(ValueError, match=f"The licensor IP with id {ZERO_ADDRESS} is not registered."): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=False + ): + with pytest.raises( + ValueError, + match=f"The licensor IP with id {ZERO_ADDRESS} is not registered.", + ): license_client.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, amount=1, - receiver=ZERO_ADDRESS + receiver=ZERO_ADDRESS, ) def test_mint_license_tokens_license_terms_not_exist(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.license_template_client, 'exists', return_value=False): - with pytest.raises(ValueError, match='License terms id 1 do not exist.'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.license_template_client, "exists", return_value=False + ): + with pytest.raises(ValueError, match="License terms id 1 do not exist."): license_client.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, amount=1, - receiver=ZERO_ADDRESS + receiver=ZERO_ADDRESS, ) def test_mint_license_tokens_not_attached(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.license_template_client, 'exists', return_value=True), \ - patch.object(license_client.license_registry_client, 'hasIpAttachedLicenseTerms', return_value=False): - with pytest.raises(ValueError, - match=f'License terms id 1 is not attached to the IP with id {ZERO_ADDRESS}.'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.license_template_client, "exists", return_value=True + ), patch.object( + license_client.license_registry_client, + "hasIpAttachedLicenseTerms", + return_value=False, + ): + with pytest.raises( + ValueError, + match=f"License terms id 1 is not attached to the IP with id {ZERO_ADDRESS}.", + ): license_client.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, amount=1, - receiver=ZERO_ADDRESS + receiver=ZERO_ADDRESS, ) def test_mint_license_tokens_invalid_template(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True): - with pytest.raises(ValueError, match='Address "invalid address" is invalid'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ): + with pytest.raises( + ValueError, match='Address "invalid address" is invalid' + ): license_client.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template="invalid address", license_terms_id=1, amount=1, - receiver=ZERO_ADDRESS + receiver=ZERO_ADDRESS, ) def test_mint_license_tokens_invalid_receiver(self, license_client): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True): - with pytest.raises(ValueError, match='Address "invalid address" is invalid'): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ): + with pytest.raises( + ValueError, match='Address "invalid address" is invalid' + ): license_client.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, amount=1, - receiver="invalid address" + receiver="invalid address", ) - def test_mint_license_tokens_success(self, license_client, mock_signed_txn, mock_account): - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.license_template_client, 'exists', return_value=True), \ - patch.object(license_client.license_registry_client, 'hasIpAttachedLicenseTerms', return_value=True), \ - patch.object(license_client.licensing_module_client, 'build_mintLicenseTokens_transaction', - return_value={'from': mock_account.address, 'nonce': 1, 'gas': 2000000, 'gasPrice': Web3.to_wei('100', 'gwei')}), \ - patch.object(mock_account, 'sign_transaction', return_value=mock_signed_txn), \ - patch.object(license_client.web3.eth, 'send_raw_transaction', return_value=MockTxHash()), \ - patch.object(license_client.web3.eth, 'wait_for_transaction_receipt', return_value=MagicMock()), \ - patch.object(license_client, '_parse_tx_license_terms_registered_event', return_value=1): + def test_mint_license_tokens_success( + self, license_client, mock_signed_txn, mock_account + ): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.license_template_client, "exists", return_value=True + ), patch.object( + license_client.license_registry_client, + "hasIpAttachedLicenseTerms", + return_value=True, + ), patch.object( + license_client.licensing_module_client, + "build_mintLicenseTokens_transaction", + return_value={ + "from": mock_account.address, + "nonce": 1, + "gas": 2000000, + "gasPrice": Web3.to_wei("100", "gwei"), + }, + ), patch.object( + mock_account, "sign_transaction", return_value=mock_signed_txn + ), patch.object( + license_client.web3.eth, "send_raw_transaction", return_value=MockTxHash() + ), patch.object( + license_client.web3.eth, + "wait_for_transaction_receipt", + return_value=MagicMock(), + ), patch.object( + license_client, "_parse_tx_license_terms_registered_event", return_value=1 + ): response = license_client.mint_license_tokens( licensor_ip_id=ZERO_ADDRESS, license_template=ZERO_ADDRESS, license_terms_id=1, amount=1, - receiver=ZERO_ADDRESS + receiver=ZERO_ADDRESS, ) - assert 'tx_hash' in response - assert response['tx_hash'] == TX_HASH[2:] - assert isinstance(response['tx_hash'], str) + assert "tx_hash" in response + assert response["tx_hash"] == TX_HASH[2:] + assert isinstance(response["tx_hash"], str) + class TestLicenseTerms: """Tests for retrieving license terms.""" def test_get_license_terms_success(self, license_client): mock_response = { - 'terms': { - 'transferable': True, - 'royaltyPolicy': ZERO_ADDRESS, - 'defaultMintingFee': 1, - 'expiration': 1, - 'commercialUse': True, - 'commercialAttribution': True, - 'commercializerChecker': ZERO_ADDRESS, - 'commercializerCheckerData': ZERO_ADDRESS, - 'commercialRevShare': 100, - 'commercialRevCeiling': 1, - 'derivativesAllowed': True, - 'derivativesAttribution': True, - 'derivativesApproval': True, - 'derivativesReciprocal': True, - 'derivativeRevCeiling': 1, - 'currency': ZERO_ADDRESS, - 'uri': "string" + "terms": { + "transferable": True, + "royaltyPolicy": ZERO_ADDRESS, + "defaultMintingFee": 1, + "expiration": 1, + "commercialUse": True, + "commercialAttribution": True, + "commercializerChecker": ZERO_ADDRESS, + "commercializerCheckerData": ZERO_ADDRESS, + "commercialRevShare": 100, + "commercialRevCeiling": 1, + "derivativesAllowed": True, + "derivativesAttribution": True, + "derivativesApproval": True, + "derivativesReciprocal": True, + "derivativeRevCeiling": 1, + "currency": ZERO_ADDRESS, + "uri": "string", } } - with patch.object(license_client.license_template_client, 'getLicenseTerms', return_value=mock_response): + with patch.object( + license_client.license_template_client, + "getLicenseTerms", + return_value=mock_response, + ): response = license_client.get_license_terms(1) assert response == mock_response def test_get_license_terms_not_exist(self, license_client): - with patch.object(license_client.license_template_client, 'getLicenseTerms', - side_effect=Exception("Given licenseTermsId is not exist.")): - with pytest.raises(ValueError, match="Failed to get license terms: Given licenseTermsId is not exist."): + with patch.object( + license_client.license_template_client, + "getLicenseTerms", + side_effect=Exception("Given licenseTermsId is not exist."), + ): + with pytest.raises( + ValueError, + match="Failed to get license terms: Given licenseTermsId is not exist.", + ): license_client.get_license_terms(1) + class TestLicensingConfig: """Tests for license configuration functionality.""" def test_set_licensing_config_missing_params(self, license_client): incomplete_config = { - 'isSet': True, - 'mintingFee': 0, + "isSet": True, + "mintingFee": 0, } - with pytest.raises(ValueError, match="Missing required licensing_config parameters:"): + with pytest.raises( + ValueError, match="Missing required licensing_config parameters:" + ): license_client.set_licensing_config( ip_id=ZERO_ADDRESS, license_terms_id=1, - licensing_config=incomplete_config + licensing_config=incomplete_config, ) def test_set_licensing_config_negative_minting_fee(self, license_client): config = { - 'isSet': True, - 'mintingFee': -1, - 'hookData': "0x", - 'licensingHook': ZERO_ADDRESS, - 'commercialRevShare': 0, - 'disabled': False, - 'expectMinimumGroupRewardShare': 0, - 'expectGroupRewardPool': ZERO_ADDRESS + "isSet": True, + "mintingFee": -1, + "hookData": "0x", + "licensingHook": ZERO_ADDRESS, + "commercialRevShare": 0, + "disabled": False, + "expectMinimumGroupRewardShare": 0, + "expectGroupRewardPool": ZERO_ADDRESS, } - with pytest.raises(ValueError, match="Failed to set licensing config: The minting fee must be greater than 0."): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The minting fee must be greater than 0.", + ): license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, - license_terms_id=1, - licensing_config=config + ip_id=ZERO_ADDRESS, license_terms_id=1, licensing_config=config ) def test_set_licensing_config_unregistered_licensing_hook(self, license_client): custom_address = "0x1234567890123456789012345678901234567890" config = { - 'isSet': True, - 'mintingFee': 1, - 'hookData': "0x", - 'licensingHook': custom_address, - 'commercialRevShare': 0, - 'disabled': False, - 'expectMinimumGroupRewardShare': 0, - 'expectGroupRewardPool': ZERO_ADDRESS + "isSet": True, + "mintingFee": 1, + "hookData": "0x", + "licensingHook": custom_address, + "commercialRevShare": 0, + "disabled": False, + "expectMinimumGroupRewardShare": 0, + "expectGroupRewardPool": ZERO_ADDRESS, } - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True), \ - patch.object(license_client.module_registry_client, 'isRegistered', return_value=False): - with pytest.raises(ValueError, match="Failed to set licensing config: The licensing hook is not registered."): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ), patch.object( + license_client.module_registry_client, "isRegistered", return_value=False + ): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The licensing hook is not registered.", + ): license_client.set_licensing_config( - ip_id=ZERO_ADDRESS, - license_terms_id=1, - licensing_config=config + ip_id=ZERO_ADDRESS, license_terms_id=1, licensing_config=config ) def test_set_licensing_config_template_terms_mismatch(self, license_client): config = { - 'isSet': True, - 'mintingFee': 1, - 'hookData': "0x", - 'licensingHook': ZERO_ADDRESS, - 'commercialRevShare': 0, - 'disabled': False, - 'expectMinimumGroupRewardShare': 0, - 'expectGroupRewardPool': ZERO_ADDRESS + "isSet": True, + "mintingFee": 1, + "hookData": "0x", + "licensingHook": ZERO_ADDRESS, + "commercialRevShare": 0, + "disabled": False, + "expectMinimumGroupRewardShare": 0, + "expectGroupRewardPool": ZERO_ADDRESS, } - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True): - with pytest.raises(ValueError, match="Failed to set licensing config: The license template is zero address but license terms id is not zero."): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The license template is zero address but license terms id is not zero.", + ): license_client.set_licensing_config( ip_id=ZERO_ADDRESS, license_terms_id=1, license_template=ZERO_ADDRESS, - licensing_config=config + licensing_config=config, ) def test_set_licensing_config_zero_address_with_rev_share(self, license_client): config = { - 'isSet': True, - 'mintingFee': 1, - 'hookData': "0x", - 'licensingHook': ZERO_ADDRESS, - 'commercialRevShare': 10, - 'disabled': False, - 'expectMinimumGroupRewardShare': 0, - 'expectGroupRewardPool': ZERO_ADDRESS + "isSet": True, + "mintingFee": 1, + "hookData": "0x", + "licensingHook": ZERO_ADDRESS, + "commercialRevShare": 10, + "disabled": False, + "expectMinimumGroupRewardShare": 0, + "expectGroupRewardPool": ZERO_ADDRESS, } - with patch.object(license_client.ip_asset_registry_client, 'isRegistered', return_value=True): - with pytest.raises(ValueError, match="Failed to set licensing config: The license template cannot be zero address if commercial revenue share is not zero."): + with patch.object( + license_client.ip_asset_registry_client, "isRegistered", return_value=True + ): + with pytest.raises( + ValueError, + match="Failed to set licensing config: The license template cannot be zero address if commercial revenue share is not zero.", + ): license_client.set_licensing_config( ip_id=ZERO_ADDRESS, license_terms_id=0, license_template=ZERO_ADDRESS, - licensing_config=config - ) \ No newline at end of file + licensing_config=config, + ) diff --git a/tests/unit/resources/test_royalty.py b/tests/unit/resources/test_royalty.py index 767d82b..d4c901d 100644 --- a/tests/unit/resources/test_royalty.py +++ b/tests/unit/resources/test_royalty.py @@ -4,7 +4,7 @@ # Ensure the src directory is in the Python path current_dir = os.path.dirname(__file__) -src_path = os.path.abspath(os.path.join(current_dir, '..', '..', '..')) +src_path = os.path.abspath(os.path.join(current_dir, "..", "..", "..")) if src_path not in sys.path: sys.path.append(src_path) @@ -12,9 +12,10 @@ # Load environment variables from .env file from dotenv import load_dotenv + load_dotenv() -private_key = os.getenv('WALLET_PRIVATE_KEY') -rpc_url = os.getenv('RPC_PROVIDER_URL') +private_key = os.getenv("WALLET_PRIVATE_KEY") +rpc_url = os.getenv("RPC_PROVIDER_URL") # Initialize Web3 web3 = Web3(Web3.HTTPProvider(rpc_url)) @@ -26,67 +27,125 @@ # Set up the account with the private key account = web3.eth.account.from_key(private_key) + @pytest.fixture def royalty_client(): chain_id = 1315 return Royalty(web3, account, chain_id) + def test_claimable_revenue_royalty_vault_ip_id_error(royalty_client): - with patch.object(royalty_client.ip_asset_registry_client, 'isRegistered', return_value=False): + with patch.object( + royalty_client.ip_asset_registry_client, "isRegistered", return_value=False + ): child_ip_id = "0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c" account_address = account.address token = "0xB132A6B7AE652c974EE1557A3521D53d18F6739f" - with pytest.raises(ValueError, match=f"The IP with id {child_ip_id} is not registered."): + with pytest.raises( + ValueError, match=f"The IP with id {child_ip_id} is not registered." + ): royalty_client.claimable_revenue(child_ip_id, account_address, token) + def test_claimable_revenue_success(royalty_client): - with patch.object(royalty_client.ip_asset_registry_client, 'isRegistered', return_value=True): - with patch.object(royalty_client, 'get_royalty_vault_address', return_value="0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c"): - with patch('story_protocol_python_sdk.abi.IpRoyaltyVaultImpl.IpRoyaltyVaultImpl_client.IpRoyaltyVaultImplClient.claimableRevenue', return_value=0): + with patch.object( + royalty_client.ip_asset_registry_client, "isRegistered", return_value=True + ): + with patch.object( + royalty_client, + "get_royalty_vault_address", + return_value="0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c", + ): + with patch( + "story_protocol_python_sdk.abi.IpRoyaltyVaultImpl.IpRoyaltyVaultImpl_client.IpRoyaltyVaultImplClient.claimableRevenue", + return_value=0, + ): parent_ip_id = "0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c" account_address = account.address token = "0xB132A6B7AE652c974EE1557A3521D53d18F6739f" - response = royalty_client.claimable_revenue(parent_ip_id, account_address, token) + response = royalty_client.claimable_revenue( + parent_ip_id, account_address, token + ) assert response == 0 + def test_pay_royalty_on_behalf_receiver_ip_id_error(royalty_client): - with patch.object(royalty_client.ip_asset_registry_client, 'isRegistered', return_value=False): + with patch.object( + royalty_client.ip_asset_registry_client, "isRegistered", return_value=False + ): receiver_ip_id = "0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c" payer_ip_id = "0x9C098DF37b2324aaC8792dDc7BcEF7Bb0057A9C7" ERC20 = "0xB132A6B7AE652c974EE1557A3521D53d18F6739f" amount = 1 - with pytest.raises(ValueError, match=f"The receiver IP with id {receiver_ip_id} is not registered."): - royalty_client.pay_royalty_on_behalf(receiver_ip_id, payer_ip_id, ERC20, amount) + with pytest.raises( + ValueError, + match=f"The receiver IP with id {receiver_ip_id} is not registered.", + ): + royalty_client.pay_royalty_on_behalf( + receiver_ip_id, payer_ip_id, ERC20, amount + ) + def test_pay_royalty_on_behalf_payer_ip_id_error(royalty_client): - with patch.object(royalty_client.ip_asset_registry_client, 'isRegistered', side_effect=[True, False]): + with patch.object( + royalty_client.ip_asset_registry_client, + "isRegistered", + side_effect=[True, False], + ): receiver_ip_id = "0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c" payer_ip_id = "0x9C098DF37b2324aaC8792dDc7BcEF7Bb0057A9C7" ERC20 = "0xB132A6B7AE652c974EE1557A3521D53d18F6739f" amount = 1 - with pytest.raises(ValueError, match=f"The payer IP with id {payer_ip_id} is not registered."): - royalty_client.pay_royalty_on_behalf(receiver_ip_id, payer_ip_id, ERC20, amount) + with pytest.raises( + ValueError, match=f"The payer IP with id {payer_ip_id} is not registered." + ): + royalty_client.pay_royalty_on_behalf( + receiver_ip_id, payer_ip_id, ERC20, amount + ) + def test_pay_royalty_on_behalf_success(royalty_client): - with patch.object(royalty_client.ip_asset_registry_client, 'isRegistered', return_value=True): - with patch('story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client.RoyaltyModuleClient.build_payRoyaltyOnBehalf_transaction', return_value={ - 'data': '0x', - 'nonce': 0, - 'gas': 2000000, - 'gasPrice': Web3.to_wei('300', 'gwei') - }): - with patch('web3.eth.Eth.send_raw_transaction', return_value=Web3.to_bytes(hexstr='0xbadf64f2c220e27407c4d2ccbc772fb72c7dc590ac25000dc316e4dc519fbfa2')): - with patch('web3.eth.Eth.wait_for_transaction_receipt', return_value={'status': 1, 'logs': []}): + with patch.object( + royalty_client.ip_asset_registry_client, "isRegistered", return_value=True + ): + with patch( + "story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client.RoyaltyModuleClient.build_payRoyaltyOnBehalf_transaction", + return_value={ + "data": "0x", + "nonce": 0, + "gas": 2000000, + "gasPrice": Web3.to_wei("300", "gwei"), + }, + ): + with patch( + "web3.eth.Eth.send_raw_transaction", + return_value=Web3.to_bytes( + hexstr="0xbadf64f2c220e27407c4d2ccbc772fb72c7dc590ac25000dc316e4dc519fbfa2" + ), + ): + with patch( + "web3.eth.Eth.wait_for_transaction_receipt", + return_value={"status": 1, "logs": []}, + ): receiver_ip_id = "0xA34611b0E11Bba2b11c69864f7D36aC83D862A9c" payer_ip_id = "0x9C098DF37b2324aaC8792dDc7BcEF7Bb0057A9C7" ERC20 = "0xB132A6B7AE652c974EE1557A3521D53d18F6739f" amount = 1 - response = royalty_client.pay_royalty_on_behalf(receiver_ip_id, payer_ip_id, ERC20, amount, tx_options={'wait_for_transaction': True}) + response = royalty_client.pay_royalty_on_behalf( + receiver_ip_id, + payer_ip_id, + ERC20, + amount, + tx_options={"wait_for_transaction": True}, + ) assert response is not None - assert 'tx_hash' in response - assert response['tx_hash'] == 'badf64f2c220e27407c4d2ccbc772fb72c7dc590ac25000dc316e4dc519fbfa2' + assert "tx_hash" in response + assert ( + response["tx_hash"] + == "badf64f2c220e27407c4d2ccbc772fb72c7dc590ac25000dc316e4dc519fbfa2" + ) From 7b089ba4fc799a58a6c6b41f9662ac07b3a986c0 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 30 Jul 2025 15:43:17 +0800 Subject: [PATCH 2/4] Add pre-commit --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..87f41bc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black From 8c7e05b7a88dacf6e88d3761a2799b87b9f622e9 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 30 Jul 2025 15:51:49 +0800 Subject: [PATCH 3/4] Add pre-commitdff --- .pre-commit-config.yaml | 13 ++++++++++--- .../CoreMetadataViewModule_client.py | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87f41bc..3bd9e35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,12 @@ repos: -- repo: https://github.com/psf/black - rev: 22.10.0 + - repo: https://github.com/psf/black + rev: 25.1.0 hooks: - - id: black + - id: black + language_version: python3 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: check-merge-conflict diff --git a/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py b/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py index e7f8ebd..2ac514e 100644 --- a/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py +++ b/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py @@ -14,6 +14,7 @@ def __init__(self, web3: Web3): ) with open(config_path, "r") as config_file: config = json.load(config_file) + contract_address = None for contract in config["contracts"]: if contract["contract_name"] == "CoreMetadataViewModule": From 13f2df5e6ba4fea4c643b3c7b4976c5b9032be13 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Wed, 30 Jul 2025 16:41:01 +0800 Subject: [PATCH 4/4] docs: add instructions for initializing pre-commit hooks in README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7eef81c..2650272 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ aeneid_chain_id = 1315 story_client = StoryClient(web3, account, aeneid_chain_id) ``` +## Init pre-commit hooks + +``` +pre-commit install +``` + ## Running test cases - Integration Tests