From 4c7c3c696db0c02b8607b65fed1c226fdf643be5 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 5 Aug 2025 15:11:05 +0800 Subject: [PATCH 01/16] Add mypy library --- MANIFEST.in | 1 + mypy.ini | 24 ++++++++++++++++++++++++ setup.py | 1 + src/story_protocol_python_sdk/py.typed | 0 4 files changed, 26 insertions(+) create mode 100644 mypy.ini create mode 100644 src/story_protocol_python_sdk/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index 2ace150..6704cb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ include requirements.txt recursive-include src *.py *.json recursive-include src/story_protocol_python_sdk/abi *.json recursive-include src/story_protocol_python_sdk/scripts *.json +include src/story_protocol_python_sdk/py.typed diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..a80ed31 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,24 @@ +[mypy] +python_version = 3.10 +warn_return_any = True +warn_unused_configs = True +disallow_untyped_defs = False +disallow_incomplete_defs = False +check_untyped_defs = True +disallow_untyped_decorators = False +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_no_return = True +warn_unreachable = True +strict_equality = True +exclude = story_protocol_python_sdk/abi|story_protocol_python_sdk/scripts +# Due to previous code having any return types, we need to disable this error code +disable_error_code = no-any-return + +# Ignore missing imports for external libraries +[mypy-web3.*] +ignore_missing_imports = True + +[mypy-ens.*] +ignore_missing_imports = True diff --git a/setup.py b/setup.py index eabcc7d..e562df3 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ package_dir={"": "src"}, install_requires=["web3>=7.0.0", "pytest", "python-dotenv", "base58"], include_package_data=True, # Ensure package data is included + package_data={"story_protocol_python_sdk": ["py.typed"]}, url="https://github.com/storyprotocol/python-sdk", license="MIT", author="Andrew Chung", diff --git a/src/story_protocol_python_sdk/py.typed b/src/story_protocol_python_sdk/py.typed new file mode 100644 index 0000000..e69de29 From f5811d9312432f14408095c526eeef4d28d24665 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 5 Aug 2025 15:17:28 +0800 Subject: [PATCH 02/16] Fix type errors --- .../CoreMetadataViewModule_client.py | 1 - .../abi/DisputeModule/DisputeModule_client.py | 12 + .../abi/jsons/DisputeModule.json | 1583 +++++++---------- .../resources/Dispute.py | 12 +- .../resources/Group.py | 24 +- .../resources/IPAccount.py | 21 +- .../resources/IPAsset.py | 42 +- .../resources/License.py | 57 +- .../resources/NFTClient.py | 15 +- .../resources/Permission.py | 9 +- .../resources/Royalty.py | 12 +- .../resources/WIP.py | 12 +- .../scripts/config.json | 490 +++-- src/story_protocol_python_sdk/story_client.py | 18 +- .../utils/constants.py | 6 +- .../utils/license_terms.py | 3 +- src/story_protocol_python_sdk/utils/oov3.py | 4 +- src/story_protocol_python_sdk/utils/sign.py | 8 +- .../utils/transaction_utils.py | 6 +- tests/demo/demo.py | 39 +- .../test_integration_ip_account.py | 9 +- .../integration/test_integration_ip_asset.py | 8 +- tests/integration/test_integration_license.py | 2 + .../test_integration_nft_client.py | 63 - tests/integration/test_integration_royalty.py | 2 + tests/integration/test_integration_wip.py | 32 +- tests/unit/resources/test_royalty.py | 5 +- 27 files changed, 1090 insertions(+), 1405 deletions(-) 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 8c14566..c0faf6f 100644 --- a/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py +++ b/src/story_protocol_python_sdk/abi/CoreMetadataViewModule/CoreMetadataViewModule_client.py @@ -15,7 +15,6 @@ 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": 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 2b8ae2f..58b9476 100644 --- a/src/story_protocol_python_sdk/abi/DisputeModule/DisputeModule_client.py +++ b/src/story_protocol_python_sdk/abi/DisputeModule/DisputeModule_client.py @@ -59,5 +59,17 @@ def build_resolveDispute_transaction(self, disputeId, data, tx_params): disputeId, data ).build_transaction(tx_params) + def tagIfRelatedIpInfringed(self, ipIdToTag, infringerDisputeId): + return self.contract.functions.tagIfRelatedIpInfringed( + ipIdToTag, infringerDisputeId + ).transact() + + def build_tagIfRelatedIpInfringed_transaction( + self, ipIdToTag, infringerDisputeId, tx_params + ): + return self.contract.functions.tagIfRelatedIpInfringed( + ipIdToTag, infringerDisputeId + ).build_transaction(tx_params) + def isWhitelistedDisputeTag(self, tag): return self.contract.functions.isWhitelistedDisputeTag(tag).call() diff --git a/src/story_protocol_python_sdk/abi/jsons/DisputeModule.json b/src/story_protocol_python_sdk/abi/jsons/DisputeModule.json index 1c58791..5053eb4 100644 --- a/src/story_protocol_python_sdk/abi/jsons/DisputeModule.json +++ b/src/story_protocol_python_sdk/abi/jsons/DisputeModule.json @@ -1,1286 +1,981 @@ [ { - "type": "constructor", "inputs": [ { + "internalType": "address", "name": "accessController", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "address", "name": "ipAssetRegistry", - "type": "address", - "internalType": "address" + "type": "address" }, { + "internalType": "address", "name": "licenseRegistry", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "ACCESS_CONTROLLER", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IAccessController" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "IN_DISPUTE", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "IP_ASSET_REGISTRY", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract IIPAssetRegistry" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "LICENSE_REGISTRY", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract ILicenseRegistry" - } + "type": "address" + }, + { "internalType": "address", "name": "ipGraphAcl", "type": "address" } ], - "stateMutability": "view" + "stateMutability": "nonpayable", + "type": "constructor" }, { - "type": "function", - "name": "UPGRADE_INTERFACE_VERSION", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } + "inputs": [ + { "internalType": "address", "name": "ipAccount", "type": "address" } ], - "stateMutability": "view" + "name": "AccessControlled__NotIpAccount", + "type": "error" }, + { "inputs": [], "name": "AccessControlled__ZeroAddress", "type": "error" }, { - "type": "function", - "name": "__ProtocolPausable_init", "inputs": [ - { - "name": "accessManager", - "type": "address", - "internalType": "address" - } + { "internalType": "address", "name": "authority", "type": "address" } ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "AccessManagedInvalidAuthority", + "type": "error" }, { - "type": "function", - "name": "arbitrationPolicies", "inputs": [ - { - "name": "ipId", - "type": "address", - "internalType": "address" - } + { "internalType": "address", "name": "caller", "type": "address" }, + { "internalType": "uint32", "name": "delay", "type": "uint32" } ], - "outputs": [ - { - "name": "policy", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" + "name": "AccessManagedRequiredDelay", + "type": "error" }, { - "type": "function", - "name": "arbitrationPolicyCooldown", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } + "inputs": [ + { "internalType": "address", "name": "caller", "type": "address" } ], - "stateMutability": "view" + "name": "AccessManagedUnauthorized", + "type": "error" }, { - "type": "function", - "name": "arbitrationRelayer", "inputs": [ - { - "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } + { "internalType": "address", "name": "target", "type": "address" } ], - "stateMutability": "view" + "name": "AddressEmptyCode", + "type": "error" }, { - "type": "function", - "name": "authority", "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" + "name": "DisputeModule__CannotBlacklistBaseArbitrationPolicy", + "type": "error" }, { - "type": "function", - "name": "baseArbitrationPolicy", "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "cancelDispute", - "inputs": [ - { - "name": "disputeId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "DisputeModule__DisputeAlreadyPropagated", + "type": "error" }, { - "type": "function", - "name": "disputeCounter", "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" + "name": "DisputeModule__DisputeWithoutInfringementTag", + "type": "error" }, { - "type": "function", - "name": "disputes", - "inputs": [ - { - "name": "disputeId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "targetIpId", - "type": "address", - "internalType": "address" - }, - { - "name": "disputeInitiator", - "type": "address", - "internalType": "address" - }, - { - "name": "disputeTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" - }, - { - "name": "disputeEvidenceHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "targetTag", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "currentTag", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "parentDisputeId", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DisputeModule__EvidenceHashAlreadyUsed", + "type": "error" }, + { "inputs": [], "name": "DisputeModule__NotAbleToResolve", "type": "error" }, { - "type": "function", - "name": "initialize", - "inputs": [ - { - "name": "accessManager", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" + "inputs": [], + "name": "DisputeModule__NotAllowedToWhitelist", + "type": "error" }, { - "type": "function", - "name": "isConsumingScheduledOp", "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "stateMutability": "view" + "name": "DisputeModule__NotArbitrationRelayer", + "type": "error" }, { - "type": "function", - "name": "isIpTagged", - "inputs": [ - { - "name": "ipId", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DisputeModule__NotDerivativeOrGroupIp", + "type": "error" }, { - "type": "function", - "name": "isWhitelistedArbitrationPolicy", - "inputs": [ - { - "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "allowed", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DisputeModule__NotDisputeInitiator", + "type": "error" }, + { "inputs": [], "name": "DisputeModule__NotInDisputeState", "type": "error" }, + { "inputs": [], "name": "DisputeModule__NotRegisteredIpId", "type": "error" }, { - "type": "function", - "name": "isWhitelistedDisputeTag", - "inputs": [ - { - "name": "tag", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [ - { - "name": "allowed", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DisputeModule__NotWhitelistedArbitrationPolicy", + "type": "error" }, { - "type": "function", - "name": "multicall", - "inputs": [ - { - "name": "data", - "type": "bytes[]", - "internalType": "bytes[]" - } - ], - "outputs": [ - { - "name": "results", - "type": "bytes[]", - "internalType": "bytes[]" - } - ], - "stateMutability": "nonpayable" + "inputs": [], + "name": "DisputeModule__NotWhitelistedDisputeTag", + "type": "error" }, { - "type": "function", - "name": "name", "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" + "name": "DisputeModule__RelatedDisputeNotResolved", + "type": "error" }, { - "type": "function", - "name": "nextArbitrationPolicies", - "inputs": [ - { - "name": "ipId", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "policy", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DisputeModule__ZeroAccessController", + "type": "error" }, + { "inputs": [], "name": "DisputeModule__ZeroAccessManager", "type": "error" }, { - "type": "function", - "name": "nextArbitrationUpdateTimestamps", - "inputs": [ - { - "name": "ipId", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "timestamp", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" + "inputs": [], + "name": "DisputeModule__ZeroArbitrationPolicy", + "type": "error" }, { - "type": "function", - "name": "pause", "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" + "name": "DisputeModule__ZeroArbitrationPolicyCooldown", + "type": "error" }, { - "type": "function", - "name": "paused", "inputs": [], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" + "name": "DisputeModule__ZeroDisputeEvidenceHash", + "type": "error" }, + { "inputs": [], "name": "DisputeModule__ZeroDisputeTag", "type": "error" }, { - "type": "function", - "name": "proxiableUUID", "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" + "name": "DisputeModule__ZeroIPAssetRegistry", + "type": "error" }, + { "inputs": [], "name": "DisputeModule__ZeroIPGraphACL", "type": "error" }, { - "type": "function", - "name": "raiseDispute", - "inputs": [ - { - "name": "targetIpId", - "type": "address", - "internalType": "address" - }, - { - "name": "disputeEvidenceHash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "targetTag", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" + "inputs": [], + "name": "DisputeModule__ZeroLicenseRegistry", + "type": "error" }, { - "type": "function", - "name": "resolveDispute", "inputs": [ - { - "name": "disputeId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } + { "internalType": "address", "name": "implementation", "type": "address" } ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "ERC1967InvalidImplementation", + "type": "error" }, + { "inputs": [], "name": "ERC1967NonPayable", "type": "error" }, + { "inputs": [], "name": "EnforcedPause", "type": "error" }, + { "inputs": [], "name": "ExpectedPause", "type": "error" }, + { "inputs": [], "name": "FailedCall", "type": "error" }, + { "inputs": [], "name": "InvalidInitialization", "type": "error" }, + { "inputs": [], "name": "NotInitializing", "type": "error" }, + { "inputs": [], "name": "ReentrancyGuardReentrantCall", "type": "error" }, + { "inputs": [], "name": "UUPSUnauthorizedCallContext", "type": "error" }, { - "type": "function", - "name": "setArbitrationPolicy", "inputs": [ - { - "name": "ipId", - "type": "address", - "internalType": "address" - }, - { - "name": "nextArbitrationPolicy", - "type": "address", - "internalType": "address" - } + { "internalType": "bytes32", "name": "slot", "type": "bytes32" } ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" }, { - "type": "function", - "name": "setArbitrationPolicyCooldown", + "anonymous": false, "inputs": [ { + "indexed": false, + "internalType": "uint256", "name": "cooldown", - "type": "uint256", - "internalType": "uint256" + "type": "uint256" } ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setArbitrationRelayer", - "inputs": [ - { - "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" - }, - { - "name": "arbPolicyRelayer", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setAuthority", - "inputs": [ - { - "name": "newAuthority", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setBaseArbitrationPolicy", - "inputs": [ - { - "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setDisputeJudgement", - "inputs": [ - { - "name": "disputeId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "decision", - "type": "bool", - "internalType": "bool" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "supportsInterface", - "inputs": [ - { - "name": "interfaceId", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "tagDerivativeIfParentInfringed", - "inputs": [ - { - "name": "parentIpId", - "type": "address", - "internalType": "address" - }, - { - "name": "derivativeIpId", - "type": "address", - "internalType": "address" - }, - { - "name": "parentDisputeId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "unpause", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" + "name": "ArbitrationPolicyCooldownUpdated", + "type": "event" }, { - "type": "function", - "name": "updateActiveArbitrationPolicy", + "anonymous": false, "inputs": [ { + "indexed": false, + "internalType": "address", "name": "ipId", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ + "type": "address" + }, { + "indexed": false, + "internalType": "address", "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "upgradeToAndCall", - "inputs": [ - { - "name": "newImplementation", - "type": "address", - "internalType": "address" + "type": "address" }, { - "name": "data", - "type": "bytes", - "internalType": "bytes" + "indexed": false, + "internalType": "uint256", + "name": "nextArbitrationUpdateTimestamp", + "type": "uint256" } ], - "outputs": [], - "stateMutability": "payable" + "name": "ArbitrationPolicySet", + "type": "event" }, { - "type": "function", - "name": "whitelistArbitrationPolicy", + "anonymous": false, "inputs": [ { + "indexed": false, + "internalType": "address", "name": "arbitrationPolicy", - "type": "address", - "internalType": "address" + "type": "address" }, { + "indexed": false, + "internalType": "bool", "name": "allowed", - "type": "bool", - "internalType": "bool" + "type": "bool" } ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "ArbitrationPolicyWhitelistUpdated", + "type": "event" }, { - "type": "function", - "name": "whitelistDisputeTag", + "anonymous": false, "inputs": [ { - "name": "tag", - "type": "bytes32", - "internalType": "bytes32" + "indexed": false, + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" }, { - "name": "allowed", - "type": "bool", - "internalType": "bool" + "indexed": false, + "internalType": "address", + "name": "arbitrationRelayer", + "type": "address" } ], - "outputs": [], - "stateMutability": "nonpayable" + "name": "ArbitrationRelayerUpdated", + "type": "event" }, { - "type": "event", - "name": "ArbitrationPolicyCooldownUpdated", + "anonymous": false, "inputs": [ { - "name": "cooldown", - "type": "uint256", "indexed": false, - "internalType": "uint256" + "internalType": "address", + "name": "authority", + "type": "address" } ], - "anonymous": false + "name": "AuthorityUpdated", + "type": "event" }, { - "type": "event", - "name": "ArbitrationPolicySet", + "anonymous": false, "inputs": [ { - "name": "ipId", - "type": "address", "indexed": false, - "internalType": "address" - }, - { + "internalType": "address", "name": "arbitrationPolicy", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "nextArbitrationUpdateTimestamp", - "type": "uint256", - "indexed": false, - "internalType": "uint256" + "type": "address" } ], - "anonymous": false + "name": "DefaultArbitrationPolicyUpdated", + "type": "event" }, { - "type": "event", - "name": "ArbitrationPolicyWhitelistUpdated", + "anonymous": false, "inputs": [ { - "name": "arbitrationPolicy", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "uint256", + "name": "disputeId", + "type": "uint256" }, { - "name": "allowed", - "type": "bool", "indexed": false, - "internalType": "bool" + "internalType": "bytes", + "name": "data", + "type": "bytes" } ], - "anonymous": false + "name": "DisputeCancelled", + "type": "event" }, { - "type": "event", - "name": "ArbitrationRelayerUpdated", + "anonymous": false, "inputs": [ { - "name": "arbitrationPolicy", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "uint256", + "name": "disputeId", + "type": "uint256" }, { - "name": "arbitrationRelayer", - "type": "address", "indexed": false, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AuthorityUpdated", - "inputs": [ + "internalType": "bool", + "name": "decision", + "type": "bool" + }, { - "name": "authority", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "bytes", + "name": "data", + "type": "bytes" } ], - "anonymous": false + "name": "DisputeJudgementSet", + "type": "event" }, { - "type": "event", - "name": "DefaultArbitrationPolicyUpdated", + "anonymous": false, "inputs": [ { - "name": "arbitrationPolicy", - "type": "address", "indexed": false, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "DerivativeTaggedOnParentInfringement", - "inputs": [ + "internalType": "uint256", + "name": "disputeId", + "type": "uint256" + }, { - "name": "parentIpId", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "address", + "name": "targetIpId", + "type": "address" }, { - "name": "derivativeIpId", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "address", + "name": "disputeInitiator", + "type": "address" }, { - "name": "parentDisputeId", - "type": "uint256", "indexed": false, - "internalType": "uint256" + "internalType": "uint256", + "name": "disputeTimestamp", + "type": "uint256" }, { - "name": "tag", - "type": "bytes32", "indexed": false, - "internalType": "bytes32" + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" }, { - "name": "disputeTimestamp", - "type": "uint256", "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "DisputeCancelled", - "inputs": [ + "internalType": "bytes32", + "name": "disputeEvidenceHash", + "type": "bytes32" + }, { - "name": "disputeId", - "type": "uint256", "indexed": false, - "internalType": "uint256" + "internalType": "bytes32", + "name": "targetTag", + "type": "bytes32" }, { - "name": "data", - "type": "bytes", "indexed": false, - "internalType": "bytes" + "internalType": "bytes", + "name": "data", + "type": "bytes" } ], - "anonymous": false + "name": "DisputeRaised", + "type": "event" }, { - "type": "event", - "name": "DisputeJudgementSet", + "anonymous": false, "inputs": [ { - "name": "disputeId", - "type": "uint256", "indexed": false, - "internalType": "uint256" + "internalType": "uint256", + "name": "disputeId", + "type": "uint256" }, { - "name": "decision", - "type": "bool", "indexed": false, - "internalType": "bool" - }, - { + "internalType": "bytes", "name": "data", - "type": "bytes", - "indexed": false, - "internalType": "bytes" + "type": "bytes" } ], - "anonymous": false + "name": "DisputeResolved", + "type": "event" }, { - "type": "event", - "name": "DisputeRaised", + "anonymous": false, "inputs": [ { - "name": "disputeId", - "type": "uint256", "indexed": false, - "internalType": "uint256" - }, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { - "name": "targetIpId", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "uint256", + "name": "disputeId", + "type": "uint256" }, { - "name": "disputeInitiator", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "address", + "name": "infringingIpId", + "type": "address" }, { - "name": "disputeTimestamp", - "type": "uint256", "indexed": false, - "internalType": "uint256" + "internalType": "address", + "name": "ipIdToTag", + "type": "address" }, { - "name": "arbitrationPolicy", - "type": "address", "indexed": false, - "internalType": "address" + "internalType": "uint256", + "name": "infringerDisputeId", + "type": "uint256" }, { - "name": "disputeEvidenceHash", - "type": "bytes32", "indexed": false, - "internalType": "bytes32" + "internalType": "bytes32", + "name": "tag", + "type": "bytes32" }, { - "name": "targetTag", - "type": "bytes32", "indexed": false, - "internalType": "bytes32" - }, + "internalType": "uint256", + "name": "disputeTimestamp", + "type": "uint256" + } + ], + "name": "IpTaggedOnRelatedIpInfringement", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { - "name": "data", - "type": "bytes", "indexed": false, - "internalType": "bytes" + "internalType": "address", + "name": "account", + "type": "address" } ], - "anonymous": false + "name": "Paused", + "type": "event" }, { - "type": "event", - "name": "DisputeResolved", + "anonymous": false, "inputs": [ { - "name": "disputeId", - "type": "uint256", "indexed": false, - "internalType": "uint256" + "internalType": "bytes32", + "name": "tag", + "type": "bytes32" }, { - "name": "data", - "type": "bytes", "indexed": false, - "internalType": "bytes" + "internalType": "bool", + "name": "allowed", + "type": "bool" } ], - "anonymous": false + "name": "TagWhitelistUpdated", + "type": "event" }, { - "type": "event", - "name": "Initialized", + "anonymous": false, "inputs": [ { - "name": "version", - "type": "uint64", "indexed": false, - "internalType": "uint64" + "internalType": "address", + "name": "account", + "type": "address" } ], - "anonymous": false + "name": "Unpaused", + "type": "event" }, { - "type": "event", - "name": "Paused", + "anonymous": false, "inputs": [ { - "name": "account", - "type": "address", - "indexed": false, - "internalType": "address" + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" } ], - "anonymous": false + "name": "Upgraded", + "type": "event" }, { - "type": "event", - "name": "TagWhitelistUpdated", - "inputs": [ - { - "name": "tag", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - }, + "inputs": [], + "name": "ACCESS_CONTROLLER", + "outputs": [ { - "name": "allowed", - "type": "bool", - "indexed": false, - "internalType": "bool" + "internalType": "contract IAccessController", + "name": "", + "type": "address" } ], - "anonymous": false + "stateMutability": "view", + "type": "function" }, { - "type": "event", - "name": "Unpaused", - "inputs": [ + "inputs": [], + "name": "GROUP_IP_ASSET_REGISTRY", + "outputs": [ { - "name": "account", - "type": "address", - "indexed": false, - "internalType": "address" + "internalType": "contract IGroupIPAssetRegistry", + "name": "", + "type": "address" } ], - "anonymous": false + "stateMutability": "view", + "type": "function" }, { - "type": "event", - "name": "Upgraded", - "inputs": [ + "inputs": [], + "name": "IN_DISPUTE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "IP_ASSET_REGISTRY", + "outputs": [ { - "name": "implementation", - "type": "address", - "indexed": true, - "internalType": "address" + "internalType": "contract IIPAssetRegistry", + "name": "", + "type": "address" } ], - "anonymous": false + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "AccessControlled__NotIpAccount", - "inputs": [ + "inputs": [], + "name": "IP_GRAPH_ACL", + "outputs": [ + { "internalType": "contract IPGraphACL", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "LICENSE_REGISTRY", + "outputs": [ { - "name": "ipAccount", - "type": "address", - "internalType": "address" + "internalType": "contract ILicenseRegistry", + "name": "", + "type": "address" } - ] + ], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "AccessControlled__ZeroAddress", - "inputs": [] + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "AccessManagedInvalidAuthority", "inputs": [ - { - "name": "authority", - "type": "address", - "internalType": "address" - } - ] + { "internalType": "address", "name": "accessManager", "type": "address" } + ], + "name": "__ProtocolPausable_init", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "AccessManagedRequiredDelay", "inputs": [ - { - "name": "caller", - "type": "address", - "internalType": "address" - }, - { - "name": "delay", - "type": "uint32", - "internalType": "uint32" - } - ] + { "internalType": "address", "name": "ipId", "type": "address" } + ], + "name": "arbitrationPolicies", + "outputs": [ + { "internalType": "address", "name": "policy", "type": "address" } + ], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "AccessManagedUnauthorized", - "inputs": [ - { - "name": "caller", - "type": "address", - "internalType": "address" - } - ] + "inputs": [], + "name": "arbitrationPolicyCooldown", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "AddressEmptyCode", "inputs": [ { - "name": "target", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" } - ] - }, - { - "type": "error", - "name": "DisputeModule__CannotBlacklistBaseArbitrationPolicy", - "inputs": [] + ], + "name": "arbitrationRelayer", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__DisputeAlreadyPropagated", - "inputs": [] + "inputs": [], + "name": "authority", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotAbleToResolve", - "inputs": [] + "inputs": [], + "name": "baseArbitrationPolicy", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotAllowedToWhitelist", - "inputs": [] + "inputs": [ + { "internalType": "uint256", "name": "disputeId", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "cancelDispute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotArbitrationRelayer", - "inputs": [] + "inputs": [], + "name": "disputeCounter", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotDerivative", - "inputs": [] + "inputs": [ + { "internalType": "uint256", "name": "disputeId", "type": "uint256" } + ], + "name": "disputes", + "outputs": [ + { "internalType": "address", "name": "targetIpId", "type": "address" }, + { + "internalType": "address", + "name": "disputeInitiator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "disputeTimestamp", + "type": "uint256" + }, + { + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "disputeEvidenceHash", + "type": "bytes32" + }, + { "internalType": "bytes32", "name": "targetTag", "type": "bytes32" }, + { "internalType": "bytes32", "name": "currentTag", "type": "bytes32" }, + { + "internalType": "uint256", + "name": "infringerDisputeId", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotDisputeInitiator", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "accessManager", "type": "address" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotInDisputeState", - "inputs": [] + "inputs": [], + "name": "isConsumingScheduledOp", + "outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotRegisteredIpId", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "ipId", "type": "address" } + ], + "name": "isIpTagged", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotWhitelistedArbitrationPolicy", - "inputs": [] + "inputs": [ + { + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" + } + ], + "name": "isWhitelistedArbitrationPolicy", + "outputs": [{ "internalType": "bool", "name": "allowed", "type": "bool" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__NotWhitelistedDisputeTag", - "inputs": [] + "inputs": [{ "internalType": "bytes32", "name": "tag", "type": "bytes32" }], + "name": "isWhitelistedDisputeTag", + "outputs": [{ "internalType": "bool", "name": "allowed", "type": "bool" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ParentDisputeNotResolved", - "inputs": [] + "inputs": [ + { "internalType": "bytes[]", "name": "data", "type": "bytes[]" } + ], + "name": "multicall", + "outputs": [ + { "internalType": "bytes[]", "name": "results", "type": "bytes[]" } + ], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ParentIpIdMismatch", - "inputs": [] + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ParentNotTagged", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "ipId", "type": "address" } + ], + "name": "nextArbitrationPolicies", + "outputs": [ + { "internalType": "address", "name": "policy", "type": "address" } + ], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroAccessController", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "ipId", "type": "address" } + ], + "name": "nextArbitrationUpdateTimestamps", + "outputs": [ + { "internalType": "uint256", "name": "timestamp", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroAccessManager", - "inputs": [] + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroArbitrationPolicy", - "inputs": [] + "inputs": [], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroArbitrationPolicyCooldown", - "inputs": [] + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroDisputeEvidenceHash", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "targetIpId", "type": "address" }, + { + "internalType": "bytes32", + "name": "disputeEvidenceHash", + "type": "bytes32" + }, + { "internalType": "bytes32", "name": "targetTag", "type": "bytes32" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "raiseDispute", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroDisputeTag", - "inputs": [] + "inputs": [ + { "internalType": "uint256", "name": "disputeId", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "resolveDispute", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroIPAssetRegistry", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "ipId", "type": "address" }, + { + "internalType": "address", + "name": "nextArbitrationPolicy", + "type": "address" + } + ], + "name": "setArbitrationPolicy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "DisputeModule__ZeroLicenseRegistry", - "inputs": [] + "inputs": [ + { "internalType": "uint256", "name": "cooldown", "type": "uint256" } + ], + "name": "setArbitrationPolicyCooldown", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "ERC1967InvalidImplementation", "inputs": [ { - "name": "implementation", - "type": "address", - "internalType": "address" + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" + }, + { + "internalType": "address", + "name": "arbPolicyRelayer", + "type": "address" } - ] + ], + "name": "setArbitrationRelayer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "ERC1967NonPayable", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "newAuthority", "type": "address" } + ], + "name": "setAuthority", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "EnforcedPause", - "inputs": [] + "inputs": [ + { + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" + } + ], + "name": "setBaseArbitrationPolicy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "ExpectedPause", - "inputs": [] + "inputs": [ + { "internalType": "uint256", "name": "disputeId", "type": "uint256" }, + { "internalType": "bool", "name": "decision", "type": "bool" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "setDisputeJudgement", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "FailedInnerCall", - "inputs": [] + "inputs": [ + { "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" } + ], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" }, { - "type": "error", - "name": "InvalidInitialization", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "ipIdToTag", "type": "address" }, + { + "internalType": "uint256", + "name": "infringerDisputeId", + "type": "uint256" + } + ], + "name": "tagIfRelatedIpInfringed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "NotInitializing", - "inputs": [] + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "ReentrancyGuardReentrantCall", - "inputs": [] + "inputs": [ + { "internalType": "address", "name": "ipId", "type": "address" } + ], + "name": "updateActiveArbitrationPolicy", + "outputs": [ + { + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" }, { - "type": "error", - "name": "UUPSUnauthorizedCallContext", - "inputs": [] + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" }, { - "type": "error", - "name": "UUPSUnsupportedProxiableUUID", "inputs": [ { - "name": "slot", - "type": "bytes32", - "internalType": "bytes32" - } - ] + "internalType": "address", + "name": "arbitrationPolicy", + "type": "address" + }, + { "internalType": "bool", "name": "allowed", "type": "bool" } + ], + "name": "whitelistArbitrationPolicy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "tag", "type": "bytes32" }, + { "internalType": "bool", "name": "allowed", "type": "bool" } + ], + "name": "whitelistDisputeTag", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] diff --git a/src/story_protocol_python_sdk/resources/Dispute.py b/src/story_protocol_python_sdk/resources/Dispute.py index 0fc7355..6859e80 100644 --- a/src/story_protocol_python_sdk/resources/Dispute.py +++ b/src/story_protocol_python_sdk/resources/Dispute.py @@ -55,7 +55,7 @@ def raise_dispute( cid: str, liveness: int, bond: int, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Raises a dispute on a given IP ID. @@ -132,7 +132,7 @@ def raise_dispute( raise ValueError(f"Failed to raise dispute: {str(e)}") def cancel_dispute( - self, dispute_id: int, data: str = "0x", tx_options: dict = None + self, dispute_id: int, data: str = "0x", tx_options: dict | None = None ) -> dict: """ Cancels an ongoing dispute. @@ -158,7 +158,7 @@ def cancel_dispute( raise ValueError(f"Failed to cancel dispute: {str(e)}") def resolve_dispute( - self, dispute_id: int, data: str, tx_options: dict = None + self, dispute_id: int, data: str, tx_options: dict | None = None ) -> dict: """ Resolves a dispute after it has been judged. @@ -184,7 +184,7 @@ def resolve_dispute( raise ValueError(f"Failed to resolve dispute: {str(e)}") def tag_if_related_ip_infringed( - self, infringement_tags: list, tx_options: dict = None + self, infringement_tags: list, tx_options: dict | None = None ) -> list: """ Tags a derivative if a parent has been tagged with an infringement tag. @@ -217,7 +217,7 @@ def tag_if_related_ip_infringed( except Exception as e: raise ValueError(f"Failed to tag related IP infringed: {str(e)}") - def _parse_tx_dispute_raised_event(self, tx_receipt: dict) -> int: + def _parse_tx_dispute_raised_event(self, tx_receipt: dict) -> int | None: """ Parse the DisputeRaised event from a transaction receipt. @@ -240,7 +240,7 @@ def dispute_assertion( assertion_id: str, counter_evidence_cid: str, ip_id: str, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Counters a dispute that was raised by another party on an IP using counter evidence. diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index dba882e..9776d19 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -57,7 +57,7 @@ def __init__(self, web3: Web3, account, chain_id: int): self.license_terms_util = LicenseTerms(web3) self.sign_util = Sign(web3, self.chain_id, self.account) - def register_group(self, group_pool: str, tx_options: dict = None) -> dict: + def register_group(self, group_pool: str, tx_options: dict | None = None) -> dict: """ Registers a Group IPA. @@ -85,7 +85,7 @@ def register_group(self, group_pool: str, tx_options: dict = None) -> dict: 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 = None ) -> dict: """ Register a group IP with a group reward pool and attach license terms to the group IP. @@ -124,11 +124,11 @@ def mint_and_register_ip_and_attach_license_and_add_to_group( spg_nft_contract: str, license_data: list, max_allowed_reward_share: int, - ip_metadata: dict = None, - recipient: str = None, + ip_metadata: dict | None = None, + recipient: str | None = None, allow_duplicates: bool = True, - deadline: int = None, - tx_options: dict = None, + deadline: int | None = None, + tx_options: dict | None = None, ) -> dict: """ Mint an NFT from a SPGNFT collection, register it with metadata as an IP, @@ -237,9 +237,9 @@ def register_ip_and_attach_license_and_add_to_group( token_id: int, license_data: list, max_allowed_reward_share: int, - ip_metadata: dict = None, - deadline: int = None, - tx_options: dict = None, + ip_metadata: dict | None = None, + deadline: int | None = None, + tx_options: dict | None = None, ) -> dict: """ Register an NFT as IP with metadata, attach license terms to the registered IP, @@ -380,7 +380,7 @@ 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 = None, ) -> dict: """ Register a group IP with a group reward pool, attach license terms to the group IP, @@ -446,7 +446,7 @@ def collect_and_distribute_group_royalties( group_ip_id: str, currency_tokens: list, member_ip_ids: list, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Collect royalties for the entire group and distribute the rewards to each member IP's royalty vault. @@ -581,7 +581,7 @@ def _get_license_data(self, license_data: list) -> list: return result - def _get_ip_metadata(self, ip_metadata: dict = None) -> dict: + def _get_ip_metadata(self, ip_metadata: dict | None = None) -> dict: """ Process IP metadata into the format expected by the contracts. diff --git a/src/story_protocol_python_sdk/resources/IPAccount.py b/src/story_protocol_python_sdk/resources/IPAccount.py index facab3c..996f85b 100644 --- a/src/story_protocol_python_sdk/resources/IPAccount.py +++ b/src/story_protocol_python_sdk/resources/IPAccount.py @@ -74,7 +74,12 @@ def _validate_transaction_params(self, ip_id: str, to: str): 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 + self, + to: str, + value: int, + ip_id: str, + data: str, + tx_options: dict | None = None, ) -> dict: """Execute a transaction from the IP Account. @@ -109,9 +114,9 @@ def execute_with_sig( data: str, signer: str, deadline: int, - signature: str, + signature: bytes, value: int = 0, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """Execute a signed transaction from the IP Account. @@ -187,7 +192,11 @@ def owner(self, ip_id: str) -> str: raise e def set_ip_metadata( - self, ip_id: str, metadata_uri: str, metadata_hash: str, tx_options: dict = None + self, + ip_id: str, + metadata_uri: str, + metadata_hash: str, + tx_options: dict | None = None, ) -> dict: """Sets the metadataURI for an IP asset. @@ -219,7 +228,9 @@ def set_ip_metadata( except Exception as e: raise e - def transfer_erc20(self, ip_id: str, tokens: list, tx_options: dict = None) -> dict: + def transfer_erc20( + self, ip_id: str, tokens: list, tx_options: dict | None = None + ) -> dict: """Transfers ERC20 tokens from the IP Account to the target address. :param ip_id str: The IP ID to transfer tokens from. diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 11eb3ca..11928a6 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -77,7 +77,7 @@ def mint( metadata_uri: str, metadata_hash: bytes, allow_duplicates: bool = False, - tx_options: dict = None, + tx_options: dict | None = None, ): spg_nft_client = SPGNFTImplClient(self.web3, contract_address=nft_contract) @@ -110,9 +110,9 @@ def register( self, nft_contract: str, token_id: int, - ip_metadata: dict = None, - deadline: int = None, - tx_options: dict = None, + ip_metadata: dict | None = None, + deadline: int | None = None, + tx_options: dict | None = None, ) -> dict: """ Register an NFT as IP, creating a corresponding IP record. @@ -132,7 +132,7 @@ def register( if self._is_registered(ip_id): return {"tx_hash": None, "ip_id": ip_id} - req_object = { + req_object: dict = { "tokenId": token_id, "nftContract": self.web3.to_checksum_address(nft_contract), "ipMetadata": { @@ -222,8 +222,8 @@ def register_derivative( max_minting_fee: int = 0, max_rts: int = 0, max_revenue_share: int = 0, - license_template: str = None, - tx_options: dict = None, + license_template: str | None = None, + tx_options: dict | None = None, ) -> dict: """ Registers a derivative directly with parent IP's license terms, without needing license tokens, @@ -287,7 +287,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 = None, ) -> dict: """ Registers a derivative with license tokens. The derivative IP is registered with license tokens @@ -338,10 +338,10 @@ def mint_and_register_ip_asset_with_pil_terms( self, spg_nft_contract: str, terms: list, - ip_metadata: dict = None, - recipient: str = None, + ip_metadata: dict | None = None, + recipient: str | None = None, allow_duplicates: bool = False, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Mint an NFT from a collection and register it as an IP. @@ -502,10 +502,10 @@ def mint_and_register_ip_asset_with_pil_terms( def mint_and_register_ip( self, spg_nft_contract: str, - recipient: str = None, - ip_metadata: dict = None, + recipient: str | None = None, + ip_metadata: dict | None = None, allow_duplicates: bool = True, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Mint an NFT from a SPGNFT collection and register it with metadata as an IP. @@ -573,9 +573,9 @@ def register_ip_and_attach_pil_terms( nft_contract: str, token_id: int, license_terms_data: dict, - ip_metadata: dict = None, - deadline: int = None, - tx_options: dict = None, + ip_metadata: dict | None = None, + deadline: int | None = None, + tx_options: dict | None = None, ) -> dict: """ Register a given NFT as an IP and attach Programmable IP License Terms. @@ -1030,7 +1030,7 @@ def _is_registered(self, ip_id: str) -> bool: """ return self.ip_asset_registry_client.isRegistered(ip_id) - def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> int: + def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> dict: """ Parse the IPRegistered event from a transaction receipt. @@ -1049,9 +1049,9 @@ def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> int: "ip_id": self.web3.to_checksum_address(ip_id), "token_id": token_id, } - return None + raise ValueError("IPRegistered event not found in transaction receipt.") - def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int: + def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int | None: """ Parse the LicenseTermsAttached event from a transaction receipt. @@ -1069,7 +1069,7 @@ def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int: return license_terms_id return None - def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list: + def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list | None: """ Parse the LicenseTermsAttached events from a transaction receipt. diff --git a/src/story_protocol_python_sdk/resources/License.py b/src/story_protocol_python_sdk/resources/License.py index 2381680..3700c08 100644 --- a/src/story_protocol_python_sdk/resources/License.py +++ b/src/story_protocol_python_sdk/resources/License.py @@ -1,5 +1,6 @@ # src/story_protcol_python_sdk/resources/License.py +from ens.ens import HexStr from web3 import Web3 from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( @@ -62,7 +63,7 @@ def register_pil_terms( commercial_use: bool, commercial_attribution: bool, commercializer_checker: str, - commercializer_checker_data: str, + commercializer_checker_data: HexStr, commercial_rev_share: int, commercial_rev_ceiling: int, derivatives_allowed: bool, @@ -72,7 +73,7 @@ def register_pil_terms( derivative_rev_ceiling: int, currency: str, uri: str, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Registers new license terms and returns the ID of the newly registered license terms. @@ -161,7 +162,9 @@ def register_pil_terms( except Exception as e: raise e - def register_non_com_social_remixing_pil(self, tx_options: dict = None) -> dict: + def register_non_com_social_remixing_pil( + self, tx_options: dict | None = None + ) -> dict: """ Convenient function to register a PIL non-commercial social remix license to the registry. @@ -197,9 +200,9 @@ def register_commercial_use_pil( self, default_minting_fee: int, currency: str, - royalty_policy: str = None, - tx_options: dict = None, - ) -> dict: + royalty_policy: str | None = None, + tx_options: dict | None = None, + ) -> dict | None: """ Convenient function to register a PIL commercial use license to the registry. @@ -248,8 +251,8 @@ def register_commercial_remix_pil( currency: str, commercial_rev_share: int, royalty_policy: str, - tx_options: dict = None, - ) -> dict: + tx_options: dict | None = None, + ) -> dict | None: """ Convenient function to register a PIL commercial remix license to the registry. @@ -294,7 +297,7 @@ def register_commercial_remix_pil( except Exception as e: raise e - def _parse_tx_license_terms_registered_event(self, tx_receipt: dict) -> int: + def _parse_tx_license_terms_registered_event(self, tx_receipt: dict) -> int | None: """ Parse the LicenseTermsRegistered event from a transaction receipt. @@ -316,7 +319,7 @@ def attach_license_terms( ip_id: str, license_template: str, license_terms_id: int, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Attaches license terms to an IP. @@ -375,7 +378,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 = None, ) -> dict: """ Mints license tokens for the license terms attached to an IP. @@ -441,7 +444,7 @@ def mint_license_tokens( except Exception as e: raise e - def _parse_tx_license_tokens_minted_event(self, tx_receipt: dict) -> list: + def _parse_tx_license_tokens_minted_event(self, tx_receipt: dict) -> list | None: """ Parse the LicenseTokenMinted event from a transaction receipt. @@ -479,9 +482,9 @@ def predict_minting_license_fee( licensor_ip_id: str, license_terms_id: int, amount: int, - license_template: str = None, - receiver: str = None, - tx_options: dict = None, + license_template: str | None = None, + receiver: str | None = None, + tx_options: dict | None = None, ) -> dict: """ Pre-compute the minting license fee for the given IP and license terms. @@ -505,16 +508,17 @@ def predict_minting_license_fee( if not self.license_template_client.exists(license_terms_id): raise ValueError(f"License terms id {license_terms_id} does not exist.") - # Set defaults if not provided - if not receiver: - receiver = self.account.address - if not license_template: - license_template = self.license_template_client.contract.address - - # Convert addresses to checksum format licensor_ip_id = self.web3.to_checksum_address(licensor_ip_id) - license_template = self.web3.to_checksum_address(license_template) - receiver = self.web3.to_checksum_address(receiver) + license_template = ( + self.web3.to_checksum_address(license_template) + if license_template + else self.license_template_client.contract.address + ) + receiver = ( + self.web3.to_checksum_address(receiver) + if receiver + else self.account.address + ) response = self.licensing_module_client.predictMintingLicenseFee( licensor_ip_id, @@ -535,8 +539,8 @@ def set_licensing_config( ip_id: str, license_terms_id: int, licensing_config: dict, - license_template: str = None, - tx_options: dict = None, + license_template: str | None = None, + tx_options: dict | None = 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. @@ -646,4 +650,3 @@ def set_licensing_config( except Exception as e: raise ValueError(f"Failed to set licensing config: {str(e)}") - raise ValueError(f"Failed to set licensing config: {str(e)}") diff --git a/src/story_protocol_python_sdk/resources/NFTClient.py b/src/story_protocol_python_sdk/resources/NFTClient.py index 41a7217..d6c72cb 100644 --- a/src/story_protocol_python_sdk/resources/NFTClient.py +++ b/src/story_protocol_python_sdk/resources/NFTClient.py @@ -1,5 +1,6 @@ # src/story_protcol_python_sdk/resources/NFTClient.py +from eth_typing.evm import ChecksumAddress from web3 import Web3 from story_protocol_python_sdk.abi.RegistrationWorkflows.RegistrationWorkflows_client import ( @@ -35,11 +36,11 @@ def create_nft_collection( 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, + max_supply: int | None = None, + mint_fee: int | None = None, + mint_fee_token: str | None = None, + owner: str | None = None, + tx_options: dict | None = None, ) -> dict: """ Creates a new SPG NFT Collection. @@ -99,7 +100,9 @@ def create_nft_collection( except Exception as e: raise e - def _parse_tx_collection_created_event(self, tx_receipt: dict) -> int: + def _parse_tx_collection_created_event( + self, tx_receipt: dict + ) -> ChecksumAddress | None: """ Parse the CollectionCreated event from a transaction receipt. diff --git a/src/story_protocol_python_sdk/resources/Permission.py b/src/story_protocol_python_sdk/resources/Permission.py index 3bcc992..845cdc9 100644 --- a/src/story_protocol_python_sdk/resources/Permission.py +++ b/src/story_protocol_python_sdk/resources/Permission.py @@ -44,7 +44,7 @@ def set_permission( to: str, permission: int, func: str = DEFAULT_FUNCTION_SELECTOR, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Sets the permission for a specific function call. @@ -92,7 +92,7 @@ def set_permission( 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 + self, ip_id: str, signer: str, permission: int, tx_options: dict | None = None ) -> dict: """ Sets permission to a signer for all functions across all modules. @@ -139,8 +139,8 @@ def create_set_permission_signature( to: str, permission: int, func: str = DEFAULT_FUNCTION_SELECTOR, - deadline: int = None, - tx_options: dict = None, + deadline: int | None = None, + tx_options: dict | None = None, ) -> dict: """ Specific permission overrides wildcard permission with signature. @@ -180,7 +180,6 @@ def create_set_permission_signature( # Get state and calculate deadline state = ip_account_client.state() - self.web3.eth.get_block("latest").timestamp calculated_deadline = self.sign_util.get_deadline(deadline) # Get permission signature diff --git a/src/story_protocol_python_sdk/resources/Royalty.py b/src/story_protocol_python_sdk/resources/Royalty.py index 100965f..8e86506 100644 --- a/src/story_protocol_python_sdk/resources/Royalty.py +++ b/src/story_protocol_python_sdk/resources/Royalty.py @@ -95,7 +95,7 @@ def pay_royalty_on_behalf( payer_ip_id: str, token: str, amount: int, - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Allows the function caller to pay royalties to the receiver IP asset on behalf of the payer IP asset. @@ -147,8 +147,8 @@ def claim_all_revenue( child_ip_ids: list, royalty_policies: list, currency_tokens: list, - claim_options: dict = None, - tx_options: dict = None, + claim_options: dict | None = None, + tx_options: dict | None = None, ) -> dict: """ Claims all revenue from the child IPs of an ancestor IP, then optionally transfers and unwraps tokens. @@ -231,7 +231,7 @@ def transfer_to_vault( ancestor_ip_id: str, token: str, royalty_policy: str = "LAP", - tx_options: dict = None, + tx_options: dict | None = None, ) -> dict: """ Transfers to vault an amount of revenue tokens claimable via a royalty policy. @@ -246,7 +246,9 @@ def transfer_to_vault( try: if not self.web3.is_address(token): raise ValueError(f'Token address "{token}" is invalid.') - + royalty_policy_client: ( + RoyaltyPolicyLAPClient | RoyaltyPolicyLRPClient | None + ) = None # Determine which royalty policy to use if royalty_policy == "LAP": royalty_policy_client = self.royalty_policy_lap_client diff --git a/src/story_protocol_python_sdk/resources/WIP.py b/src/story_protocol_python_sdk/resources/WIP.py index f6d627b..37d7c2f 100644 --- a/src/story_protocol_python_sdk/resources/WIP.py +++ b/src/story_protocol_python_sdk/resources/WIP.py @@ -22,7 +22,7 @@ def __init__(self, web3: Web3, account, chain_id: int): self.wip_client = WIPClient(web3) - def deposit(self, amount: int, tx_options: dict = None) -> dict: + def deposit(self, amount: int, tx_options: dict | None = None) -> dict: """ Wraps the selected amount of IP to WIP. The WIP will be deposited to the wallet that transferred the IP. @@ -57,7 +57,7 @@ def deposit(self, amount: int, tx_options: dict = None) -> dict: except Exception as e: raise ValueError(f"Failed to deposit IP for WIP: {str(e)}") - def withdraw(self, amount: int, tx_options: dict = None) -> dict: + def withdraw(self, amount: int, tx_options: dict | None = None) -> dict: """ Unwraps the selected amount of WIP to IP. @@ -82,7 +82,9 @@ def withdraw(self, amount: int, tx_options: dict = None) -> dict: except Exception as e: raise ValueError(f"Failed to withdraw WIP: {str(e)}") - def approve(self, spender: str, amount: int, tx_options: dict = None) -> dict: + def approve( + self, spender: str, amount: int, tx_options: dict | None = None + ) -> dict: """ Approve a spender to use the wallet's WIP balance. @@ -131,7 +133,7 @@ def balance_of(self, address: str) -> int: except Exception as e: raise ValueError(f"Failed to get WIP balance: {str(e)}") - def transfer(self, to: str, amount: int, tx_options: dict = None) -> dict: + def transfer(self, to: str, amount: int, tx_options: dict | None = None) -> dict: """ Transfers `amount` of WIP to a recipient `to`. @@ -164,7 +166,7 @@ def transfer(self, to: str, amount: int, tx_options: dict = None) -> dict: raise ValueError(f"Failed to transfer WIP: {str(e)}") def transfer_from( - self, from_address: str, to: str, amount: int, tx_options: dict = None + self, from_address: str, to: str, amount: int, tx_options: dict | None = None ) -> dict: """ Transfers `amount` of WIP from `from_address` to a recipient `to`. diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index ed90911..8f580e4 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -1,258 +1,236 @@ { - "contracts": [ - { - "contract_name": "AccessController", - "contract_address": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", - "functions": [ - "PermissionSet", - "setPermission", - "setAllPermissions", - "setBatchPermissions", - "setTransientPermission", - "setTransientBatchPermissions" - ] - }, - { - "contract_name": "DisputeModule", - "contract_address": "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5", - "functions": [ - "DisputeCancelled", - "DisputeRaised", - "DisputeResolved", - "cancelDispute", - "raiseDispute", - "resolveDispute", - "isWhitelistedDisputeTag" - ] - }, - { - "contract_name": "IPAccountImpl", - "contract_address": "0xc93d49fEdED1A2fbE3B54223Df65f4edB3845eb0", - "functions": [ - "execute", - "executeBatch", - "executeWithSig", - "state", - "token", - "owner" - ] - }, - { - "contract_name": "IPAssetRegistry", - "contract_address": "0x77319B4031e6eF1250907aa00018B8B1c67a244b", - "functions": [ - "IPRegistered", - "ipId", - "isRegistered", - "register", - "IPAccountRegistered" - ] - }, - { - "contract_name": "IpRoyaltyVaultImpl", - "contract_address": "0x63cC7611316880213f3A4Ba9bD72b0EaA2010298", - "functions": [ - "claimableRevenue", - "ipId", - "RoyaltyTokensCollected", - "RevenueTokenClaimed", - "balanceOf" - ] - }, - { - "contract_name": "PILicenseTemplate", - "contract_address": "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", - "functions": [ - "getLicenseTermsId", - "registerLicenseTerms", - "LicenseTermsRegistered", - "getLicenseTerms", - "exists" - ] - }, - { - "contract_name": "LicensingModule", - "contract_address": "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f", - "functions": [ - "attachLicenseTerms", - "mintLicenseTokens", - "LicenseTokensMinted", - "registerDerivativeWithLicenseTokens", - "registerDerivative", - "getLicenseTerms", - "LicenseTermsAttached", - "predictMintingLicenseFee", - "setLicensingConfig" - ] - }, - { - "contract_name": "ModuleRegistry", - "contract_address": "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5", - "functions": [ - "isRegistered", - "getDefaultLicenseTerms" - ] - }, - { - "contract_name": "RoyaltyModule", - "contract_address": "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086", - "functions": [ - "payRoyaltyOnBehalf", - "isWhitelistedRoyaltyPolicy", - "isWhitelistedRoyaltyToken", - "ipRoyaltyVaults", - "IpRoyaltyVaultDeployed" - ] - }, - { - "contract_name": "RoyaltyPolicyLAP", - "contract_address": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", - "functions": [ - "onRoyaltyPayment", - "getRoyaltyData", - "transferToVault" - ] - }, - { - "contract_name": "LicenseToken", - "contract_address": "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", - "functions": [ - "ownerOf" - ] - }, - { - "contract_name": "GroupingWorkflows", - "contract_address": "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd", - "functions": [ - "mintAndRegisterIpAndAttachLicenseAndAddToGroup", - "registerIpAndAttachLicenseAndAddToGroup", - "registerGroupAndAttachLicense", - "registerGroupAndAttachLicenseAndAddIps", - "collectRoyaltiesAndClaimReward", - "CollectedRoyaltiesToGroupPool", - "RoyaltyPaid" - ] - }, - { - "contract_name": "RegistrationWorkflows", - "contract_address": "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424", - "functions": [ - "createCollection", - "mintAndRegisterIp", - "registerIp", - "CollectionCreated", - "multicall" - ] - }, - { - "contract_name": "LicenseAttachmentWorkflows", - "contract_address": "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8", - "functions": [ - "registerPILTermsAndAttach", - "registerIpAndAttachPILTerms", - "mintAndRegisterIpAndAttachPILTerms", - "multicall" - ] - }, - { - "contract_name": "RoyaltyWorkflows", - "contract_address": "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890", - "functions": [ - "transferToVaultAndSnapshotAndClaimByTokenBatch", - "transferToVaultAndSnapshotAndClaimBySnapshotBatch", - "snapshotAndClaimByTokenBatch", - "snapshotAndClaimBySnapshotBatch", - "claimAllRevenue" - ] - }, - { - "contract_name": "RoyaltyTokenDistributionWorkflows", - "contract_address": "0xa38f42B8d33809917f23997B8423054aAB97322C", - "functions": [ - "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", - "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", - "registerIpAndAttachPILTermsAndDeployRoyaltyVault", - "distributeRoyaltyTokens", - "registerIpAndMakeDerivativeAndDeployRoyaltyVault" - ] - }, - { - "contract_name": "CoreMetadataModule", - "contract_address": "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", - "functions": [] - }, - { - "contract_name": "CoreMetadataViewModule", - "contract_address": "0xB3F88038A983CeA5753E11D144228Ebb5eACdE20", - "functions": [] - }, - { - "contract_name": "GroupingModule", - "contract_address": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", - "functions": [ - "registerGroup", - "addIp", - "IPGroupRegistered" - ] - }, - { - "contract_name": "LicenseRegistry", - "contract_address": "0x529a750E02d8E2f15649c13D69a465286a780e24", - "functions": [ - "exists", - "hasIpAttachedLicenseTerms", - "getRoyaltyPercent" - ] - }, - { - "contract_name": "RoyaltyPolicyLRP", - "contract_address": "0x9156e603C949481883B1d3355c6f1132D191fC41", - "functions": ["transferToVault"] - }, - { - "contract_name": "ArbitrationPolicyUMA", - "contract_address": "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936", - "functions": [ - "minLiveness", - "maxLiveness", - "maxBonds", - "disputeIdToAssertionId", - "oov3" - ] - }, - { - "contract_name": "MockERC20", - "contract_address": "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", - "functions": [ - "transfer", - "balanceOf" - ] - }, - { - "contract_name": "WIP", - "contract_address": "0x1514000000000000000000000000000000000000", - "functions": [ - "deposit", - "withdraw", - "approve", - "balanceOf", - "transfer", - "transferFrom", - "allowance" - ] - }, - { - "contract_name": "SPGNFTImpl", - "contract_address": "0xc09e3788Fdfbd3dd8CDaa2aa481B52CcFAb74a42", - "functions": [ - "mintFeeToken", - "mintFee" - ] - }, - { - "contract_name": "DerivativeWorkflows", - "contract_address": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", - "functions": [] - } - ] + "contracts": [ + { + "contract_name": "AccessController", + "contract_address": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", + "functions": [ + "PermissionSet", + "setPermission", + "setAllPermissions", + "setBatchPermissions", + "setTransientPermission", + "setTransientBatchPermissions" + ] + }, + { + "contract_name": "DisputeModule", + "contract_address": "0x9b7A9c70AFF961C799110954fc06F3093aeb94C5", + "functions": [ + "DisputeCancelled", + "DisputeRaised", + "DisputeResolved", + "cancelDispute", + "raiseDispute", + "resolveDispute", + "isWhitelistedDisputeTag", + "tagIfRelatedIpInfringed" + ] + }, + { + "contract_name": "IPAccountImpl", + "contract_address": "0xc93d49fEdED1A2fbE3B54223Df65f4edB3845eb0", + "functions": [ + "execute", + "executeBatch", + "executeWithSig", + "state", + "token", + "owner" + ] + }, + { + "contract_name": "IPAssetRegistry", + "contract_address": "0x77319B4031e6eF1250907aa00018B8B1c67a244b", + "functions": [ + "IPRegistered", + "ipId", + "isRegistered", + "register", + "IPAccountRegistered" + ] + }, + { + "contract_name": "IpRoyaltyVaultImpl", + "contract_address": "0x63cC7611316880213f3A4Ba9bD72b0EaA2010298", + "functions": [ + "claimableRevenue", + "ipId", + "RoyaltyTokensCollected", + "RevenueTokenClaimed", + "balanceOf" + ] + }, + { + "contract_name": "PILicenseTemplate", + "contract_address": "0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316", + "functions": [ + "getLicenseTermsId", + "registerLicenseTerms", + "LicenseTermsRegistered", + "getLicenseTerms", + "exists" + ] + }, + { + "contract_name": "LicensingModule", + "contract_address": "0x04fbd8a2e56dd85CFD5500A4A4DfA955B9f1dE6f", + "functions": [ + "attachLicenseTerms", + "mintLicenseTokens", + "LicenseTokensMinted", + "registerDerivativeWithLicenseTokens", + "registerDerivative", + "getLicenseTerms", + "LicenseTermsAttached", + "predictMintingLicenseFee", + "setLicensingConfig" + ] + }, + { + "contract_name": "ModuleRegistry", + "contract_address": "0x022DBAAeA5D8fB31a0Ad793335e39Ced5D631fa5", + "functions": ["isRegistered", "getDefaultLicenseTerms"] + }, + { + "contract_name": "RoyaltyModule", + "contract_address": "0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086", + "functions": [ + "payRoyaltyOnBehalf", + "isWhitelistedRoyaltyPolicy", + "isWhitelistedRoyaltyToken", + "ipRoyaltyVaults", + "IpRoyaltyVaultDeployed" + ] + }, + { + "contract_name": "RoyaltyPolicyLAP", + "contract_address": "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E", + "functions": ["onRoyaltyPayment", "getRoyaltyData", "transferToVault"] + }, + { + "contract_name": "LicenseToken", + "contract_address": "0xFe3838BFb30B34170F00030B52eA4893d8aAC6bC", + "functions": ["ownerOf"] + }, + { + "contract_name": "GroupingWorkflows", + "contract_address": "0xD7c0beb3aa4DCD4723465f1ecAd045676c24CDCd", + "functions": [ + "mintAndRegisterIpAndAttachLicenseAndAddToGroup", + "registerIpAndAttachLicenseAndAddToGroup", + "registerGroupAndAttachLicense", + "registerGroupAndAttachLicenseAndAddIps", + "collectRoyaltiesAndClaimReward", + "CollectedRoyaltiesToGroupPool", + "RoyaltyPaid" + ] + }, + { + "contract_name": "RegistrationWorkflows", + "contract_address": "0xbe39E1C756e921BD25DF86e7AAa31106d1eb0424", + "functions": [ + "createCollection", + "mintAndRegisterIp", + "registerIp", + "CollectionCreated", + "multicall" + ] + }, + { + "contract_name": "LicenseAttachmentWorkflows", + "contract_address": "0xcC2E862bCee5B6036Db0de6E06Ae87e524a79fd8", + "functions": [ + "registerPILTermsAndAttach", + "registerIpAndAttachPILTerms", + "mintAndRegisterIpAndAttachPILTerms", + "multicall" + ] + }, + { + "contract_name": "RoyaltyWorkflows", + "contract_address": "0x9515faE61E0c0447C6AC6dEe5628A2097aFE1890", + "functions": [ + "transferToVaultAndSnapshotAndClaimByTokenBatch", + "transferToVaultAndSnapshotAndClaimBySnapshotBatch", + "snapshotAndClaimByTokenBatch", + "snapshotAndClaimBySnapshotBatch", + "claimAllRevenue" + ] + }, + { + "contract_name": "RoyaltyTokenDistributionWorkflows", + "contract_address": "0xa38f42B8d33809917f23997B8423054aAB97322C", + "functions": [ + "mintAndRegisterIpAndAttachPILTermsAndDistributeRoyaltyTokens", + "mintAndRegisterIpAndMakeDerivativeAndDistributeRoyaltyTokens", + "registerIpAndAttachPILTermsAndDeployRoyaltyVault", + "distributeRoyaltyTokens", + "registerIpAndMakeDerivativeAndDeployRoyaltyVault" + ] + }, + { + "contract_name": "CoreMetadataModule", + "contract_address": "0x6E81a25C99C6e8430aeC7353325EB138aFE5DC16", + "functions": [] + }, + { + "contract_name": "CoreMetadataViewModule", + "contract_address": "0xB3F88038A983CeA5753E11D144228Ebb5eACdE20", + "functions": [] + }, + { + "contract_name": "GroupingModule", + "contract_address": "0x69D3a7aa9edb72Bc226E745A7cCdd50D947b69Ac", + "functions": ["registerGroup", "addIp", "IPGroupRegistered"] + }, + { + "contract_name": "LicenseRegistry", + "contract_address": "0x529a750E02d8E2f15649c13D69a465286a780e24", + "functions": ["exists", "hasIpAttachedLicenseTerms", "getRoyaltyPercent"] + }, + { + "contract_name": "RoyaltyPolicyLRP", + "contract_address": "0x9156e603C949481883B1d3355c6f1132D191fC41", + "functions": ["transferToVault"] + }, + { + "contract_name": "ArbitrationPolicyUMA", + "contract_address": "0xfFD98c3877B8789124f02C7E8239A4b0Ef11E936", + "functions": [ + "minLiveness", + "maxLiveness", + "maxBonds", + "disputeIdToAssertionId", + "oov3" + ] + }, + { + "contract_name": "MockERC20", + "contract_address": "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", + "functions": ["transfer", "balanceOf"] + }, + { + "contract_name": "WIP", + "contract_address": "0x1514000000000000000000000000000000000000", + "functions": [ + "deposit", + "withdraw", + "approve", + "balanceOf", + "transfer", + "transferFrom", + "allowance" + ] + }, + { + "contract_name": "SPGNFTImpl", + "contract_address": "0xc09e3788Fdfbd3dd8CDaa2aa481B52CcFAb74a42", + "functions": ["mintFeeToken", "mintFee"] + }, + { + "contract_name": "DerivativeWorkflows", + "contract_address": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", + "functions": [] + } + ] } diff --git a/src/story_protocol_python_sdk/story_client.py b/src/story_protocol_python_sdk/story_client.py index 652ce1f..51ea6b5 100644 --- a/src/story_protocol_python_sdk/story_client.py +++ b/src/story_protocol_python_sdk/story_client.py @@ -48,15 +48,15 @@ def __init__(self, web3, account, chain_id: int): self.account = account self.chain_id = chain_id - self._ip_asset = None - self._license = None - self._royalty = None - self._ip_account = None - self._permission = None - self._nft_client = None - self._dispute = None - self._wip = None - self._group = None + self._ip_asset: IPAsset | None = None + self._license: License | None = None + self._royalty: Royalty | None = None + self._ip_account: IPAccount | None = None + self._permission: Permission | None = None + self._nft_client: NFTClient | None = None + self._dispute: Dispute | None = None + self._wip: WIP | None = None + self._group: Group | None = None @property def IPAsset(self) -> IPAsset: diff --git a/src/story_protocol_python_sdk/utils/constants.py b/src/story_protocol_python_sdk/utils/constants.py index 8d17752..e16eb8d 100644 --- a/src/story_protocol_python_sdk/utils/constants.py +++ b/src/story_protocol_python_sdk/utils/constants.py @@ -1,5 +1,9 @@ +from eth_typing import HexStr + ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" -ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000" +ZERO_HASH: HexStr = HexStr( + "0x0000000000000000000000000000000000000000000000000000000000000000" +) ROYALTY_POLICY = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" ZERO_FUNC = "0x00000000" DEFAULT_FUNCTION_SELECTOR = "0x00000000" diff --git a/src/story_protocol_python_sdk/utils/license_terms.py b/src/story_protocol_python_sdk/utils/license_terms.py index 5597e55..ad05e43 100644 --- a/src/story_protocol_python_sdk/utils/license_terms.py +++ b/src/story_protocol_python_sdk/utils/license_terms.py @@ -1,5 +1,6 @@ # src/story_protocol_python_sdk/utils/license_terms.py +from ens.async_ens import HexStr from web3 import Web3 from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( @@ -134,7 +135,7 @@ def validate_license_terms(self, params): commercializer_checker_data = params.get( "commercializer_checker_data", ZERO_ADDRESS ) - if isinstance(commercializer_checker_data, str): + if isinstance(commercializer_checker_data, HexStr): params["commercializer_checker_data"] = Web3.to_bytes( hexstr=commercializer_checker_data ) diff --git a/src/story_protocol_python_sdk/utils/oov3.py b/src/story_protocol_python_sdk/utils/oov3.py index ee5b3b7..81dae77 100644 --- a/src/story_protocol_python_sdk/utils/oov3.py +++ b/src/story_protocol_python_sdk/utils/oov3.py @@ -40,7 +40,9 @@ def get_assertion_bond( :return int: The bond amount. """ try: - oov3_contract_address = get_oov3_contract(arbitration_policy_uma_client) + oov3_contract_address = Web3.to_checksum_address( + get_oov3_contract(arbitration_policy_uma_client) + ) oov3_contract = web3.eth.contract( address=oov3_contract_address, abi=ASSERTION_ABI diff --git a/src/story_protocol_python_sdk/utils/sign.py b/src/story_protocol_python_sdk/utils/sign.py index 0fe266c..2e63bd6 100644 --- a/src/story_protocol_python_sdk/utils/sign.py +++ b/src/story_protocol_python_sdk/utils/sign.py @@ -26,7 +26,7 @@ def __init__(self, web3: Web3, chain_id: int, account): def get_signature( self, - state: str, + state: bytes, to: str, encode_data: bytes, verifying_contract: str, @@ -98,7 +98,7 @@ def get_signature( except Exception as e: raise e - def get_deadline(self, deadline: int = None) -> int: + def get_deadline(self, deadline: int | None = None) -> int: """ Calculate the deadline for a transaction. @@ -119,8 +119,8 @@ def get_permission_signature( ip_id: str, deadline: int, permissions: list, - permission_func: str = None, - state: str = None, + state: bytes, + permission_func: str | None = None, ) -> dict: """ Get the signature for setting permissions. diff --git a/src/story_protocol_python_sdk/utils/transaction_utils.py b/src/story_protocol_python_sdk/utils/transaction_utils.py index 8f58ef3..3c14c6a 100644 --- a/src/story_protocol_python_sdk/utils/transaction_utils.py +++ b/src/story_protocol_python_sdk/utils/transaction_utils.py @@ -6,7 +6,11 @@ def build_and_send_transaction( - web3: Web3, account, client_function, *client_args, tx_options: dict = None + web3: Web3, + account, + client_function, + *client_args, + tx_options: dict | None = None, ) -> dict: """ Builds and sends a transaction using the provided client function and arguments. diff --git a/tests/demo/demo.py b/tests/demo/demo.py index e126c1c..38a6b55 100644 --- a/tests/demo/demo.py +++ b/tests/demo/demo.py @@ -1,6 +1,11 @@ import os -from demo_utils import ( +from dotenv import load_dotenv +from web3 import Web3 + +from story_protocol_python_sdk import StoryClient + +from .demo_utils import ( PIL_LICENSE_TEMPLATE, ROYALTY_POLICY, MockERC20, @@ -8,10 +13,6 @@ get_token_id, mint_tokens, ) -from dotenv import load_dotenv -from web3 import Web3 - -from story_protocol_python_sdk import StoryClient def main(): @@ -42,14 +43,18 @@ def main(): registered_ip_asset_response = story_client.IPAsset.register( nft_contract=MockERC721, token_id=token_id ) + if registered_ip_asset_response is None: + raise ValueError("Failed to register IP asset") 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( + register_pil_terms_response = story_client.License.register_commercial_use_pil( default_minting_fee=1, currency=MockERC20, royalty_policy=ROYALTY_POLICY ) + if register_pil_terms_response is None: + raise ValueError("Failed to register PIL terms") 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']}" @@ -61,11 +66,13 @@ def main(): # 4. Attach License Terms to IP try: - attach_license_terms_response = story_client.License.attachLicenseTerms( + attach_license_terms_response = story_client.License.attach_license_terms( ip_id=registered_ip_asset_response["ipId"], license_template=PIL_LICENSE_TEMPLATE, license_terms_id=register_pil_terms_response["licenseTermsId"], ) + if attach_license_terms_response is None: + raise ValueError("Failed to attach license terms") print( f"Attached License Terms to IP at transaction hash {attach_license_terms_response['txHash']}" ) @@ -78,7 +85,7 @@ def main(): mint_tokens(MockERC20, web3, account, account.address, 10000) # 5. Mint License - mint_license_response = story_client.License.mintLicenseTokens( + mint_license_response = story_client.License.mint_license_tokens( licensor_ip_id=registered_ip_asset_response["ipId"], license_template=PIL_LICENSE_TEMPLATE, license_terms_id=register_pil_terms_response["licenseTermsId"], @@ -87,6 +94,8 @@ def main(): max_minting_fee=1, max_revenue_share=0, ) + if mint_license_response is None: + raise ValueError("Failed to mint license tokens") print( f"License Token minted at transaction hash {mint_license_response['txHash']}, License Token IDs: {mint_license_response['licenseTokenIds']}" ) @@ -98,15 +107,21 @@ def main(): registered_ip_asset_derivative_response = story_client.IPAsset.register( nft_contract=MockERC721, token_id=derivative_token_id ) + if registered_ip_asset_derivative_response is None: + raise ValueError("Failed to register derivative IP asset") 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, + link_derivative_response = ( + story_client.IPAsset.register_derivative_with_license_tokens( + child_ip_id=registered_ip_asset_derivative_response["ipId"], + license_token_ids=mint_license_response["licenseTokenIds"], + max_rts=5 * 10**6, + ) ) + if link_derivative_response is None: + raise ValueError("Failed to link derivative IP asset") print( f"Derivative IPA linked to parent at transaction hash {link_derivative_response['txHash']}" ) diff --git a/tests/integration/test_integration_ip_account.py b/tests/integration/test_integration_ip_account.py index 4dca5c9..6f6f879 100644 --- a/tests/integration/test_integration_ip_account.py +++ b/tests/integration/test_integration_ip_account.py @@ -4,7 +4,11 @@ from eth_abi.abi import encode from eth_account import Account from eth_account.messages import encode_typed_data -from setup_for_integration import ( +from web3 import Web3 + +from story_protocol_python_sdk.story_client import StoryClient + +from .setup_for_integration import ( MockERC20, MockERC721, account, @@ -14,9 +18,6 @@ private_key, web3, ) -from web3 import Web3 - -from story_protocol_python_sdk.story_client import StoryClient class TestBasicIPAccountOperations: diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index ff9a5a0..462c555 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -357,8 +357,8 @@ def test_register_ip_and_attach_pil_terms( nft_contract=nft_collection, token_id=token_id, deadline=1000, - license_terms_data=[ - { + license_terms_data={ + 0: { "terms": { "transferable": True, "royalty_policy": ZERO_ADDRESS, @@ -389,7 +389,7 @@ def test_register_ip_and_attach_pil_terms( "expect_group_reward_pool": ZERO_ADDRESS, }, }, - { + 1: { "terms": { "transferable": True, "royalty_policy": ROYALTY_POLICY, @@ -420,7 +420,7 @@ def test_register_ip_and_attach_pil_terms( "expect_group_reward_pool": ZERO_ADDRESS, }, }, - ], + }, ) assert isinstance(result["tx_hash"], str) and result["tx_hash"] diff --git a/tests/integration/test_integration_license.py b/tests/integration/test_integration_license.py index bcc5be9..f0cdc2a 100644 --- a/tests/integration/test_integration_license.py +++ b/tests/integration/test_integration_license.py @@ -302,6 +302,8 @@ def setup_license_terms(story_client: StoryClient, ip_id): commercial_rev_share=100, royalty_policy=ROYALTY_POLICY, ) + if response is None: + raise ValueError("Failed to register license terms") license_id = response["license_terms_id"] # Attach the license terms diff --git a/tests/integration/test_integration_nft_client.py b/tests/integration/test_integration_nft_client.py index e68b1c0..00e56a3 100644 --- a/tests/integration/test_integration_nft_client.py +++ b/tests/integration/test_integration_nft_client.py @@ -148,69 +148,6 @@ def test_invalid_mint_fee_values(self, story_client: StoryClient): or "invalid" in str(e).lower() ) - def test_parameter_omission(self, story_client: StoryClient): - """Test omitting required parameters""" - - with pytest.raises(TypeError): - story_client.NFTClient.create_nft_collection( - # name is omitted - symbol="TEST", - is_public_minting=True, - mint_open=True, - contract_uri="test-uri", - mint_fee_recipient=story_client.account.address, - ) - - with pytest.raises(TypeError): - story_client.NFTClient.create_nft_collection( - name="test-collection", - # symbol is omitted - is_public_minting=True, - mint_open=True, - contract_uri="test-uri", - mint_fee_recipient=story_client.account.address, - ) - - with pytest.raises(TypeError): - story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - # is_public_minting is omitted - mint_open=True, - contract_uri="test-uri", - mint_fee_recipient=story_client.account.address, - ) - - with pytest.raises(TypeError): - story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - is_public_minting=True, - # mint_open is omitted - contract_uri="test-uri", - mint_fee_recipient=story_client.account.address, - ) - - with pytest.raises(TypeError): - story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - is_public_minting=True, - mint_open=True, - # contract_uri is omitted - mint_fee_recipient=story_client.account.address, - ) - - with pytest.raises(TypeError): - story_client.NFTClient.create_nft_collection( - name="test-collection", - symbol="TEST", - is_public_minting=True, - mint_open=True, - contract_uri="test-uri", - # mint_fee_recipient is omitted - ) - def test_authorization_errors(self, story_client: StoryClient): """Test unauthorized operations""" diff --git a/tests/integration/test_integration_royalty.py b/tests/integration/test_integration_royalty.py index 262f8ec..7d9c1ca 100644 --- a/tests/integration/test_integration_royalty.py +++ b/tests/integration/test_integration_royalty.py @@ -49,6 +49,8 @@ def setup_ips_and_licenses(self, story_client: StoryClient): commercial_rev_share=10, royalty_policy=ROYALTY_POLICY, ) + if license_terms_response is None: + raise ValueError("Failed to register license terms") license_terms_id = license_terms_response["license_terms_id"] story_client.License.attach_license_terms( diff --git a/tests/integration/test_integration_wip.py b/tests/integration/test_integration_wip.py index b6d12b8..f059b44 100644 --- a/tests/integration/test_integration_wip.py +++ b/tests/integration/test_integration_wip.py @@ -1,7 +1,17 @@ +from eth_typing import Hash32 + from story_protocol_python_sdk.story_client import StoryClient from .setup_for_integration import wallet_address, wallet_address_2, web3 +# Type assertions to ensure wallet addresses are strings +assert wallet_address is not None, "wallet_address is required" +assert wallet_address_2 is not None, "wallet_address_2 is required" + +# Type cast to satisfy mypy +wallet_address_str: str = wallet_address +wallet_address_2_str: str = wallet_address_2 + class TestWIPDeposit: def test_deposit(self, story_client: StoryClient): @@ -10,7 +20,7 @@ def test_deposit(self, story_client: StoryClient): # Get balances before deposit balance_before = story_client.get_wallet_balance() - wip_before = story_client.WIP.balance_of(wallet_address) + wip_before = story_client.WIP.balance_of(wallet_address_str) # Deposit IP to WIP response = story_client.WIP.deposit(amount=ip_amt) @@ -20,14 +30,14 @@ def test_deposit(self, story_client: StoryClient): # Get balances after deposit balance_after = story_client.get_wallet_balance() - wip_after = story_client.WIP.balance_of(wallet_address) + wip_after = story_client.WIP.balance_of(wallet_address_str) # 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 + Hash32(bytes.fromhex(response["tx_hash"][2:])), timeout=300 ) gas_cost = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] @@ -41,12 +51,12 @@ def test_transfer(self, story_client: StoryClient): 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) + sender_wip_before = story_client.WIP.balance_of(wallet_address_str) + receiver_wip_before = story_client.WIP.balance_of(wallet_address_2_str) # Transfer WIP to wallet_address_2 response = story_client.WIP.transfer( - to=wallet_address_2, + to=wallet_address_2_str, amount=transfer_amount, tx_options={"waitForTransaction": True}, ) @@ -55,8 +65,8 @@ def test_transfer(self, story_client: StoryClient): 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) + sender_wip_after = story_client.WIP.balance_of(wallet_address_str) + receiver_wip_after = story_client.WIP.balance_of(wallet_address_2_str) # Verify sender's WIP balance decreased by transfer amount assert sender_wip_after == sender_wip_before - transfer_amount @@ -72,7 +82,7 @@ def test_withdraw(self, story_client: StoryClient): """Test withdrawing WIP to IP""" # Get balances before withdrawal balance_before = story_client.get_wallet_balance() - wip_before = story_client.WIP.balance_of(wallet_address) + wip_before = story_client.WIP.balance_of(wallet_address_str) # Withdraw all WIP response = story_client.WIP.withdraw( @@ -83,7 +93,7 @@ def test_withdraw(self, story_client: StoryClient): assert isinstance(response["tx_hash"], str) # Get balances after withdrawal - wip_after = story_client.WIP.balance_of(wallet_address) + wip_after = story_client.WIP.balance_of(wallet_address_str) balance_after = story_client.get_wallet_balance() # Verify WIP balance is now zero @@ -91,7 +101,7 @@ def test_withdraw(self, story_client: StoryClient): # Calculate gas cost tx_receipt = web3.eth.wait_for_transaction_receipt( - response["tx_hash"], timeout=300 + Hash32(bytes.fromhex(response["tx_hash"][2:])), timeout=300 ) gas_cost = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] diff --git a/tests/unit/resources/test_royalty.py b/tests/unit/resources/test_royalty.py index 246db0d..47c620b 100644 --- a/tests/unit/resources/test_royalty.py +++ b/tests/unit/resources/test_royalty.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest +from ens.ens import HexStr from web3 import Web3 from story_protocol_python_sdk.resources.Royalty import Royalty @@ -103,7 +104,9 @@ def test_pay_royalty_on_behalf_success(royalty_client): with patch( "web3.eth.Eth.send_raw_transaction", return_value=Web3.to_bytes( - hexstr="0xbadf64f2c220e27407c4d2ccbc772fb72c7dc590ac25000dc316e4dc519fbfa2" + hexstr=HexStr( + "0xbadf64f2c220e27407c4d2ccbc772fb72c7dc590ac25000dc316e4dc519fbfa2" + ) ), ): with patch( From 5a7e99716affce9c670cb9808b5757d17ffc25a5 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 5 Aug 2025 15:17:37 +0800 Subject: [PATCH 03/16] Add type checking instructions to DEVELOPMENT.md with mypy command --- DEVELOPMENT.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f61ce87..e50bcdd 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -106,6 +106,12 @@ The project uses several tools to maintain code quality: All these tools are automatically run as pre-commit hooks when you commit code. +### Type Checking + +```bash +mypy . +``` + ### Manual Pre-commit Checks To manually run all pre-commit checks: From a2f784a0ce98b1a6e73e53daf2c59309ec73fb1b Mon Sep 17 00:00:00 2001 From: Bonnie Date: Tue, 5 Aug 2025 17:43:13 +0800 Subject: [PATCH 04/16] Fix integration tests --- .../resources/IPAsset.py | 4 +- .../utils/license_terms.py | 4 +- .../integration/test_integration_ip_asset.py | 8 ++-- tests/integration/test_integration_wip.py | 4 +- tests/unit/resources/test_license.py | 44 +++++++++---------- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 11928a6..d6e6709 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -572,7 +572,7 @@ def register_ip_and_attach_pil_terms( self, nft_contract: str, token_id: int, - license_terms_data: dict, + license_terms_data: list, ip_metadata: dict | None = None, deadline: int | None = None, tx_options: dict | None = None, @@ -582,7 +582,7 @@ def register_ip_and_attach_pil_terms( :param nft_contract str: The address of the NFT collection. :param token_id int: The ID of the NFT. - :param license_terms_data dict: The PIL terms and licensing configuration data to be attached to the IP. + :param license_terms_data list: The PIL terms and licensing configuration data to be attached to the IP. :param terms dict: The PIL terms to be used for the licensing. :param transferable bool: Indicates whether the license is transferable or not. :param royalty_policy str: The address of the royalty policy contract which required to StoryProtocol in advance. diff --git a/src/story_protocol_python_sdk/utils/license_terms.py b/src/story_protocol_python_sdk/utils/license_terms.py index ad05e43..05cd049 100644 --- a/src/story_protocol_python_sdk/utils/license_terms.py +++ b/src/story_protocol_python_sdk/utils/license_terms.py @@ -135,9 +135,9 @@ def validate_license_terms(self, params): commercializer_checker_data = params.get( "commercializer_checker_data", ZERO_ADDRESS ) - if isinstance(commercializer_checker_data, HexStr): + if isinstance(commercializer_checker_data, str): params["commercializer_checker_data"] = Web3.to_bytes( - hexstr=commercializer_checker_data + hexstr=HexStr(commercializer_checker_data) ) params["expect_minimum_group_reward_share"] = int( diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 462c555..ff9a5a0 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -357,8 +357,8 @@ def test_register_ip_and_attach_pil_terms( nft_contract=nft_collection, token_id=token_id, deadline=1000, - license_terms_data={ - 0: { + license_terms_data=[ + { "terms": { "transferable": True, "royalty_policy": ZERO_ADDRESS, @@ -389,7 +389,7 @@ def test_register_ip_and_attach_pil_terms( "expect_group_reward_pool": ZERO_ADDRESS, }, }, - 1: { + { "terms": { "transferable": True, "royalty_policy": ROYALTY_POLICY, @@ -420,7 +420,7 @@ def test_register_ip_and_attach_pil_terms( "expect_group_reward_pool": ZERO_ADDRESS, }, }, - }, + ], ) assert isinstance(result["tx_hash"], str) and result["tx_hash"] diff --git a/tests/integration/test_integration_wip.py b/tests/integration/test_integration_wip.py index f059b44..066be08 100644 --- a/tests/integration/test_integration_wip.py +++ b/tests/integration/test_integration_wip.py @@ -37,7 +37,7 @@ def test_deposit(self, story_client: StoryClient): # Calculate gas cost tx_receipt = web3.eth.wait_for_transaction_receipt( - Hash32(bytes.fromhex(response["tx_hash"][2:])), timeout=300 + Hash32(bytes.fromhex(response["tx_hash"])), timeout=300 ) gas_cost = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] @@ -101,7 +101,7 @@ def test_withdraw(self, story_client: StoryClient): # Calculate gas cost tx_receipt = web3.eth.wait_for_transaction_receipt( - Hash32(bytes.fromhex(response["tx_hash"][2:])), timeout=300 + Hash32(bytes.fromhex(response["tx_hash"])), timeout=300 ) gas_cost = tx_receipt["gasUsed"] * tx_receipt["effectiveGasPrice"] diff --git a/tests/unit/resources/test_license.py b/tests/unit/resources/test_license.py index d2fdb50..5ce788e 100644 --- a/tests/unit/resources/test_license.py +++ b/tests/unit/resources/test_license.py @@ -78,7 +78,9 @@ def license_client(mock_web3, mock_account): class TestPILTermsRegistration: """Tests for PIL (Programmable IP License) terms registration.""" - def test_register_pil_terms_license_terms_id_registered(self, license_client): + def test_register_pil_terms_license_terms_id_registered( + self, license_client: License + ): with patch.object( license_client.license_template_client, "getLicenseTermsId", return_value=1 ), patch.object( @@ -91,27 +93,25 @@ def test_register_pil_terms_license_terms_id_registered(self, license_client): 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": "", - } - - response = license_client.register_pil_terms(**license_terms) + response = license_client.register_pil_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="", + ) assert response["license_terms_id"] == 1 assert "tx_hash" not in response From b2df1f5041572c9f1b2e684401c37dbb9fe55c14 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Thu, 7 Aug 2025 13:38:24 +0800 Subject: [PATCH 05/16] Add derivative_data class and unit tests --- src/story_protocol_python_sdk/types/common.py | 7 + .../utils/constants.py | 1 + .../utils/derivative_data.py | 107 +++++ .../utils/ip_metadata.py | 20 + .../utils/validation.py | 20 + tests/unit/fixtures/data.py | 1 + tests/unit/utils/test_derivative_data.py | 417 ++++++++++++++++++ 7 files changed, 573 insertions(+) create mode 100644 src/story_protocol_python_sdk/types/common.py create mode 100644 src/story_protocol_python_sdk/utils/derivative_data.py create mode 100644 src/story_protocol_python_sdk/utils/ip_metadata.py create mode 100644 tests/unit/utils/test_derivative_data.py diff --git a/src/story_protocol_python_sdk/types/common.py b/src/story_protocol_python_sdk/types/common.py new file mode 100644 index 0000000..7d9ea11 --- /dev/null +++ b/src/story_protocol_python_sdk/types/common.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class RevShareType(Enum): + COMMERCIAL_REVENUE_SHARE = "commercialRevShare" + MAX_REVENUE_SHARE = "maxRevenueShare" + MAX_ALLOWED_REWARD_SHARE = "maxAllowedRewardShare" diff --git a/src/story_protocol_python_sdk/utils/constants.py b/src/story_protocol_python_sdk/utils/constants.py index e16eb8d..d08ce4c 100644 --- a/src/story_protocol_python_sdk/utils/constants.py +++ b/src/story_protocol_python_sdk/utils/constants.py @@ -7,3 +7,4 @@ ROYALTY_POLICY = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" ZERO_FUNC = "0x00000000" DEFAULT_FUNCTION_SELECTOR = "0x00000000" +MAX_ROYALTY_TOKEN = 100000000 diff --git a/src/story_protocol_python_sdk/utils/derivative_data.py b/src/story_protocol_python_sdk/utils/derivative_data.py new file mode 100644 index 0000000..610369b --- /dev/null +++ b/src/story_protocol_python_sdk/utils/derivative_data.py @@ -0,0 +1,107 @@ +from dataclasses import dataclass, field +from typing import List, Optional + +from web3 import Web3 + +from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( + IPAssetRegistryClient, +) +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.types.common import RevShareType +from story_protocol_python_sdk.utils.constants import MAX_ROYALTY_TOKEN +from story_protocol_python_sdk.utils.validation import get_revenue_share + + +@dataclass +class DerivativeData: + """Validated derivative data for IP creation.""" + + web3: Web3 + parent_ip_ids: List[str] + license_terms_ids: List[int] + max_minting_fee: int | float = field(default=0) + max_rts: int | float = field(default=MAX_ROYALTY_TOKEN) + max_revenue_share: int | float = field(default=100) + license_template: Optional[str] = field(default=None) + + pi_license_template_client: PILicenseTemplateClient = field(init=False) + ip_asset_registry_client: IPAssetRegistryClient = field(init=False) + license_registry_client: LicenseRegistryClient = field(init=False) + + def __post_init__(self): + """Initialize clients and validate data after object creation.""" + + self.pi_license_template_client = PILicenseTemplateClient(self.web3) + self.ip_asset_registry_client = IPAssetRegistryClient(self.web3) + self.license_registry_client = LicenseRegistryClient(self.web3) + + if self.license_template is None: + self.license_template = self.pi_license_template_client.contract.address + self.max_revenue_share = get_revenue_share( + self.max_revenue_share, type=RevShareType.MAX_REVENUE_SHARE + ) + + self.validate_max_minting_fee() + self.validate_max_rts() + self.validate_parent_ip_ids_and_license_terms_ids() + + def validate_parent_ip_ids_and_license_terms_ids(self): + if len(self.parent_ip_ids) == 0: + raise ValueError("The parent IP IDs must be provided.") + + if len(self.license_terms_ids) == 0: + raise ValueError("The license terms IDs must be provided.") + + if len(self.parent_ip_ids) != len(self.license_terms_ids): + raise ValueError( + "The number of parent IP IDs must match the number of license terms IDs." + ) + + ip_asset_registry_client: IPAssetRegistryClient = self.ip_asset_registry_client + license_registry_client: LicenseRegistryClient = self.license_registry_client + + for parent_ip_id, license_terms_id in zip( + self.parent_ip_ids, self.license_terms_ids + ): + if not Web3.is_checksum_address(parent_ip_id): + raise ValueError("The parent IP ID must be a valid address.") + if not ip_asset_registry_client.isRegistered(parent_ip_id): + raise ValueError(f"The parent IP ID {parent_ip_id} must be registered.") + if not license_registry_client.hasIpAttachedLicenseTerms( + parent_ip_id, self.license_template, license_terms_id + ): + raise ValueError( + f"License terms id {license_terms_id} must be attached to the parent ipId {parent_ip_id} before registering derivative." + ) + royalty_percent = license_registry_client.getRoyaltyPercent( + parent_ip_id, self.license_template, license_terms_id + ) + if self.max_revenue_share != 0 and royalty_percent > self.max_revenue_share: + raise ValueError( + f"The royalty percent for the parent IP {parent_ip_id} is greater than the maximum revenue share {self.max_revenue_share}." + ) + + def validate_max_minting_fee(self): + if self.max_minting_fee < 0: + raise ValueError("The max minting fee must be greater than 0.") + + def validate_max_rts(self): + if self.max_rts < 0 or self.max_rts > MAX_ROYALTY_TOKEN: + raise ValueError( + f"The maxRts must be greater than 0 and less than {MAX_ROYALTY_TOKEN}." + ) + + def get_validated_data(self) -> dict: + return { + "parentIpIds": self.parent_ip_ids, + "licenseTermsIds": self.license_terms_ids, + "maxMintingFee": self.max_minting_fee, + "maxRts": self.max_rts, + "maxRevenueShare": self.max_revenue_share, + "licenseTemplate": self.license_template, + } diff --git a/src/story_protocol_python_sdk/utils/ip_metadata.py b/src/story_protocol_python_sdk/utils/ip_metadata.py new file mode 100644 index 0000000..8c1ba6b --- /dev/null +++ b/src/story_protocol_python_sdk/utils/ip_metadata.py @@ -0,0 +1,20 @@ +from typing import Optional + +from eth_typing import HexStr + +from story_protocol_python_sdk.types.common import IpMetadataForWorkflow +from story_protocol_python_sdk.utils.constants import ZERO_HASH + + +def get_ip_metadata_for_workflow( + ip_metadata_uri: Optional[str], + ip_metadata_hash: Optional[HexStr], + nft_metadata_uri: Optional[str], + nft_metadata_hash: Optional[HexStr], +) -> IpMetadataForWorkflow: + return { + "ip_metadata_uri": ip_metadata_uri or "", + "ip_metadata_hash": ip_metadata_hash or ZERO_HASH, + "nft_metadata_uri": nft_metadata_uri or "", + "nft_metadata_hash": nft_metadata_hash or ZERO_HASH, + } diff --git a/src/story_protocol_python_sdk/utils/validation.py b/src/story_protocol_python_sdk/utils/validation.py index fdaaee1..d2e8da4 100644 --- a/src/story_protocol_python_sdk/utils/validation.py +++ b/src/story_protocol_python_sdk/utils/validation.py @@ -1,5 +1,8 @@ from web3 import Web3 +from story_protocol_python_sdk.types.common import RevShareType +from story_protocol_python_sdk.utils.constants import MAX_ROYALTY_TOKEN + def validate_address(address: str) -> str: """ @@ -12,3 +15,20 @@ def validate_address(address: str) -> str: if not Web3.is_address(address): raise ValueError(f"Invalid address: {address}.") return address + + +def get_revenue_share( + revShare: int | float, + type: RevShareType = RevShareType.COMMERCIAL_REVENUE_SHARE, +) -> int | float: + """ + Convert revenue share percentage to token amount. + + :param revShare int: Revenue share percentage between 0-100 + :param type RevShareType: Type of revenue share + :return int: Revenue share token amount + """ + if revShare < 0 or revShare > 100: + raise ValueError(f"The {type.value} must be between 0 and 100.") + + return (revShare * MAX_ROYALTY_TOKEN) / 100 diff --git a/tests/unit/fixtures/data.py b/tests/unit/fixtures/data.py index df5ad3f..91040db 100644 --- a/tests/unit/fixtures/data.py +++ b/tests/unit/fixtures/data.py @@ -3,3 +3,4 @@ TX_HASH = "0x0c0cce07beb64ccfbdd59da111f23084ab7c9e96a951f7381af49e792d014c04" # STATE as bytes32 (32 bytes = 64 hex characters) STATE = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +IP_ID = "0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57" diff --git a/tests/unit/utils/test_derivative_data.py b/tests/unit/utils/test_derivative_data.py new file mode 100644 index 0000000..615b99a --- /dev/null +++ b/tests/unit/utils/test_derivative_data.py @@ -0,0 +1,417 @@ +from unittest.mock import MagicMock, patch + +import pytest +from _pytest.raises import raises +from web3 import Web3 + +from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( + IPAssetRegistryClient, +) +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.utils.constants import MAX_ROYALTY_TOKEN +from story_protocol_python_sdk.utils.derivative_data import DerivativeData +from tests.unit.fixtures.data import ADDRESS, IP_ID +from tests.unit.fixtures.web3 import mock_web3 + + +@pytest.fixture +def mock_is_checksum_address(): + """Fixture to mock Web3.is_checksum_address""" + + def _mock_is_checksum_address(is_checksum_address=True): + return patch.object( + Web3, "is_checksum_address", return_value=is_checksum_address + ) + + return _mock_is_checksum_address + + +@pytest.fixture +def mock_ip_asset_registry_client(): + """Fixture to mock IPAssetRegistryClient""" + + def _mock_ip_registered(is_registered=True): + return patch.object( + IPAssetRegistryClient, + "__new__", + return_value=MagicMock(isRegistered=MagicMock(return_value=is_registered)), + ) + + return _mock_ip_registered + + +@pytest.fixture +def mock_license_registry_client(): + """Fixture to mock LicenseRegistryClient""" + + def _mock_license_registry_client( + has_ip_attached_license_terms=True, get_royalty_percent=10 + ): + return patch.object( + LicenseRegistryClient, + "__new__", + return_value=MagicMock( + hasIpAttachedLicenseTerms=MagicMock( + return_value=has_ip_attached_license_terms + ), + getRoyaltyPercent=MagicMock(return_value=get_royalty_percent), + ), + ) + + return _mock_license_registry_client + + +@pytest.fixture +def mock_pi_license_template_client(): + """Fixture to mock PILicenseTemplateClient""" + + def _mock_pi_license_template_client(): + mock_instance = MagicMock() + mock_instance.contract = MagicMock() + mock_instance.contract.address = ADDRESS + return patch.object( + PILicenseTemplateClient, + "__new__", + return_value=mock_instance, + ) + + return _mock_pi_license_template_client + + +class TestValidateParentIpIdsAndLicenseTermsIds: + def test_validate_parent_ip_ids_is_empty(self): + with raises(ValueError, match="The parent IP IDs must be provided."): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_license_terms_ids_is_empty(self): + with raises(ValueError, match="The license terms IDs must be provided."): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[ADDRESS], + license_terms_ids=[], + max_minting_fee=10, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_parent_ip_ids_and_license_terms_ids_are_not_equal(self): + with raises( + ValueError, + match="The number of parent IP IDs must match the number of license terms IDs.", + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[ADDRESS], + license_terms_ids=[2, 3], + max_minting_fee=10, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_parent_ip_ids_is_not_valid_address( + self, mock_is_checksum_address + ): + with mock_is_checksum_address(is_checksum_address=False): + with raises(ValueError, match="The parent IP ID must be a valid address."): + DerivativeData( + web3=mock_web3, + parent_ip_ids=["0x1234567890123456789012345678901234567890"], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_parent_ip_ids_is_not_registered( + self, mock_ip_asset_registry_client, mock_is_checksum_address + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client( + is_registered=False + ): + with raises( + ValueError, + match="The parent IP ID 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 must be registered.", + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_license_terms_not_attached( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client( + is_registered=True + ), mock_license_registry_client(has_ip_attached_license_terms=False): + with raises( + ValueError, + match="License terms id 2 must be attached to the parent ipId 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 before registering derivative.", + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_royalty_percent_exceeds_max_revenue_share( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client( + is_registered=True + ), mock_license_registry_client( + has_ip_attached_license_terms=True, get_royalty_percent=1500000000000 + ): + with raises( + ValueError, + match="The royalty percent for the parent IP 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 is greater than the maximum revenue share 110000.0", + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=0.11, + license_template="0x1234567890123456789012345678901234567890", + ) + + def test_validate_royalty_percent_is_less_than_max_revenue_share( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): + derivative_data = DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + license_template="0x1234567890123456789012345678901234567890", + ) + assert derivative_data.max_revenue_share == MAX_ROYALTY_TOKEN + + +class TestValidateMaxMintingFee: + def test_validate_max_minting_fee_is_less_than_0( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): + with raises( + ValueError, match="The max minting fee must be greater than 0." + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=-1, + max_rts=10, + max_revenue_share=100, + license_template="0x1234567890123456789012345678901234567890", + ) + + +class TestValidateMaxRts: + def test_validate_max_rts_is_less_than_0( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): + with raises( + ValueError, + match="The maxRts must be greater than 0 and less than 100000000.", + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_rts=-1, + ) + + def test_validate_max_rts_is_greater_than_100_000_000( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + ): + with mock_ip_asset_registry_client(), mock_license_registry_client(): + with raises( + ValueError, + match="The maxRts must be greater than 0 and less than 100000000.", + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_rts=1000000000001, + ) + + def test_validate_max_rts_default_value_is_max_rts( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): + derivative_data = DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ) + assert derivative_data.max_rts == MAX_ROYALTY_TOKEN + + +class TestValidateMaxRevenueShare: + def test_validate_max_revenue_share_is_less_than_0( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + ): + with mock_ip_asset_registry_client(), mock_license_registry_client(): + with raises( + ValueError, match="The maxRevenueShare must be between 0 and 100." + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=-1, + ) + + def test_validate_max_revenue_share_is_greater_than_100( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): + with raises( + ValueError, match="The maxRevenueShare must be between 0 and 100." + ): + DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=101, + ) + + def test_validate_max_revenue_share_default_value_is_100( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): + derivative_data = DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ) + assert derivative_data.max_revenue_share == MAX_ROYALTY_TOKEN + + +class TestValidateLicenseTemplate: + def test_validate_license_template_default_value_is_pi_license_template( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_pi_license_template_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): + derivative_data = DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ) + assert derivative_data.license_template == ADDRESS + + +class TestGetValidatedData: + def test_get_validated_data_with_default_values( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_pi_license_template_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): + derivative_data = DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ) + assert derivative_data.get_validated_data() == { + "parentIpIds": [IP_ID], + "licenseTermsIds": [2], + "maxMintingFee": 0, + "maxRts": MAX_ROYALTY_TOKEN, + "maxRevenueShare": MAX_ROYALTY_TOKEN, + "licenseTemplate": ADDRESS, + } + + def test_get_validated_data_with_custom_values( + self, + mock_ip_asset_registry_client, + mock_license_registry_client, + mock_pi_license_template_client, + mock_is_checksum_address, + ): + with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): + derivative_data = DerivativeData( + web3=mock_web3, + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=10, + license_template="0x1234567890123456789012345678901234567890", + ) + assert derivative_data.get_validated_data() == { + "parentIpIds": [IP_ID], + "licenseTermsIds": [2], + "maxMintingFee": 10, + "maxRts": 10, + "maxRevenueShare": 10000000.0, + "licenseTemplate": "0x1234567890123456789012345678901234567890", + } From 9c89bb4b4abc948da3682a82c7e654b4716aab47 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 8 Aug 2025 10:41:04 +0800 Subject: [PATCH 06/16] Fix the register_derivative_ip method --- .../AccessController_client.py | 8 +- .../DerivativeWorkflows_client.py | 20 + .../abi/jsons/AccessController.json | 484 +++--------------- .../resources/IPAsset.py | 231 ++++----- .../scripts/config.json | 6 +- src/story_protocol_python_sdk/types/common.py | 14 + .../utils/derivative_data.py | 15 +- .../utils/function_signature.py | 57 +++ .../utils/ip_metadata.py | 26 +- src/story_protocol_python_sdk/utils/sign.py | 6 +- .../utils/validation.py | 6 +- .../integration/test_integration_ip_asset.py | 83 ++- tests/unit/utils/test_derivative_data.py | 14 +- 13 files changed, 368 insertions(+), 602 deletions(-) create mode 100644 src/story_protocol_python_sdk/utils/function_signature.py 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 0b6118f..30259b1 100644 --- a/src/story_protocol_python_sdk/abi/AccessController/AccessController_client.py +++ b/src/story_protocol_python_sdk/abi/AccessController/AccessController_client.py @@ -48,13 +48,13 @@ def build_setAllPermissions_transaction( ipAccount, signer, permission ).build_transaction(tx_params) - def setTransientBatchPermissions(self, permissions): - return self.contract.functions.setTransientBatchPermissions( + def setBatchTransientPermissions(self, permissions): + return self.contract.functions.setBatchTransientPermissions( permissions ).transact() - def build_setTransientBatchPermissions_transaction(self, permissions, tx_params): - return self.contract.functions.setTransientBatchPermissions( + def build_setBatchTransientPermissions_transaction(self, permissions, tx_params): + return self.contract.functions.setBatchTransientPermissions( permissions ).build_transaction(tx_params) 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 b1162b6..8cc2331 100644 --- a/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py +++ b/src/story_protocol_python_sdk/abi/DerivativeWorkflows/DerivativeWorkflows_client.py @@ -35,3 +35,23 @@ def __init__(self, web3: Web3): 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 registerIpAndMakeDerivative( + self, nftContract, tokenId, derivData, ipMetadata, sigMetadataAndRegister + ): + return self.contract.functions.registerIpAndMakeDerivative( + nftContract, tokenId, derivData, ipMetadata, sigMetadataAndRegister + ).transact() + + def build_registerIpAndMakeDerivative_transaction( + self, + nftContract, + tokenId, + derivData, + ipMetadata, + sigMetadataAndRegister, + tx_params, + ): + return self.contract.functions.registerIpAndMakeDerivative( + nftContract, tokenId, derivData, ipMetadata, sigMetadataAndRegister + ).build_transaction(tx_params) diff --git a/src/story_protocol_python_sdk/abi/jsons/AccessController.json b/src/story_protocol_python_sdk/abi/jsons/AccessController.json index 85f0230..9956dcd 100644 --- a/src/story_protocol_python_sdk/abi/jsons/AccessController.json +++ b/src/story_protocol_python_sdk/abi/jsons/AccessController.json @@ -6,27 +6,15 @@ "name": "ipAccountRegistry", "type": "address" }, - { - "internalType": "address", - "name": "moduleRegistry", - "type": "address" - } + { "internalType": "address", "name": "moduleRegistry", "type": "address" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [ - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - } + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" } ], "name": "AccessController__BothCallerAndRecipientAreNotRegisteredModule", "type": "error" @@ -38,11 +26,7 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - } + { "internalType": "address", "name": "ipAccount", "type": "address" } ], "name": "AccessController__IPAccountIsNotValid", "type": "error" @@ -54,42 +38,18 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "owner", - "type": "address" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "owner", "type": "address" } ], "name": "AccessController__OwnerIsIPAccount", "type": "error" }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" } ], "name": "AccessController__PermissionDenied", "type": "error" @@ -126,106 +86,50 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "authority", - "type": "address" - } + { "internalType": "address", "name": "authority", "type": "address" } ], "name": "AccessManagedInvalidAuthority", "type": "error" }, { "inputs": [ - { - "internalType": "address", - "name": "caller", - "type": "address" - }, - { - "internalType": "uint32", - "name": "delay", - "type": "uint32" - } + { "internalType": "address", "name": "caller", "type": "address" }, + { "internalType": "uint32", "name": "delay", "type": "uint32" } ], "name": "AccessManagedRequiredDelay", "type": "error" }, { "inputs": [ - { - "internalType": "address", - "name": "caller", - "type": "address" - } + { "internalType": "address", "name": "caller", "type": "address" } ], "name": "AccessManagedUnauthorized", "type": "error" }, { "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - } + { "internalType": "address", "name": "target", "type": "address" } ], "name": "AddressEmptyCode", "type": "error" }, { "inputs": [ - { - "internalType": "address", - "name": "implementation", - "type": "address" - } + { "internalType": "address", "name": "implementation", "type": "address" } ], "name": "ERC1967InvalidImplementation", "type": "error" }, - { - "inputs": [], - "name": "ERC1967NonPayable", - "type": "error" - }, - { - "inputs": [], - "name": "EnforcedPause", - "type": "error" - }, - { - "inputs": [], - "name": "ExpectedPause", - "type": "error" - }, - { - "inputs": [], - "name": "FailedCall", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidInitialization", - "type": "error" - }, - { - "inputs": [], - "name": "NotInitializing", - "type": "error" - }, - { - "inputs": [], - "name": "UUPSUnauthorizedCallContext", - "type": "error" - }, + { "inputs": [], "name": "ERC1967NonPayable", "type": "error" }, + { "inputs": [], "name": "EnforcedPause", "type": "error" }, + { "inputs": [], "name": "ExpectedPause", "type": "error" }, + { "inputs": [], "name": "FailedCall", "type": "error" }, + { "inputs": [], "name": "InvalidInitialization", "type": "error" }, + { "inputs": [], "name": "NotInitializing", "type": "error" }, + { "inputs": [], "name": "UUPSUnauthorizedCallContext", "type": "error" }, { "inputs": [ - { - "internalType": "bytes32", - "name": "slot", - "type": "bytes32" - } + { "internalType": "bytes32", "name": "slot", "type": "bytes32" } ], "name": "UUPSUnsupportedProxiableUUID", "type": "error" @@ -410,23 +314,13 @@ { "inputs": [], "name": "UPGRADE_INTERFACE_VERSION", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "accessManager", - "type": "address" - } + { "internalType": "address", "name": "accessManager", "type": "address" } ], "name": "__ProtocolPausable_init", "outputs": [], @@ -436,38 +330,16 @@ { "inputs": [], "name": "authority", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" } ], "name": "checkPermission", "outputs": [], @@ -476,113 +348,43 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" } ], "name": "getPermanentPermission", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" } ], "name": "getPermission", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" } ], "name": "getTransientPermission", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "accessManager", - "type": "address" - } + { "internalType": "address", "name": "accessManager", "type": "address" } ], "name": "initialize", "outputs": [], @@ -592,13 +394,7 @@ { "inputs": [], "name": "isConsumingScheduledOp", - "outputs": [ - { - "internalType": "bytes4", - "name": "", - "type": "bytes4" - } - ], + "outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }], "stateMutability": "view", "type": "function" }, @@ -612,46 +408,22 @@ { "inputs": [], "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "proxiableUUID", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], "stateMutability": "view", "type": "function" }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "uint8", - "name": "permission", - "type": "uint8" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "uint8", "name": "permission", "type": "uint8" } ], "name": "setAllPermissions", "outputs": [], @@ -660,21 +432,9 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "uint8", - "name": "permission", - "type": "uint8" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "uint8", "name": "permission", "type": "uint8" } ], "name": "setAllTransientPermissions", "outputs": [], @@ -683,11 +443,7 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "newAuthority", - "type": "address" - } + { "internalType": "address", "name": "newAuthority", "type": "address" } ], "name": "setAuthority", "outputs": [], @@ -698,31 +454,11 @@ "inputs": [ { "components": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - }, - { - "internalType": "uint8", - "name": "permission", - "type": "uint8" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" }, + { "internalType": "uint8", "name": "permission", "type": "uint8" } ], "internalType": "struct AccessPermission.Permission[]", "name": "permissions", @@ -738,31 +474,11 @@ "inputs": [ { "components": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - }, - { - "internalType": "uint8", - "name": "permission", - "type": "uint8" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" }, + { "internalType": "uint8", "name": "permission", "type": "uint8" } ], "internalType": "struct AccessPermission.Permission[]", "name": "permissions", @@ -776,31 +492,11 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - }, - { - "internalType": "uint8", - "name": "permission", - "type": "uint8" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" }, + { "internalType": "uint8", "name": "permission", "type": "uint8" } ], "name": "setPermission", "outputs": [], @@ -809,31 +505,11 @@ }, { "inputs": [ - { - "internalType": "address", - "name": "ipAccount", - "type": "address" - }, - { - "internalType": "address", - "name": "signer", - "type": "address" - }, - { - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "func", - "type": "bytes4" - }, - { - "internalType": "uint8", - "name": "permission", - "type": "uint8" - } + { "internalType": "address", "name": "ipAccount", "type": "address" }, + { "internalType": "address", "name": "signer", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "bytes4", "name": "func", "type": "bytes4" }, + { "internalType": "uint8", "name": "permission", "type": "uint8" } ], "name": "setTransientPermission", "outputs": [], @@ -854,11 +530,7 @@ "name": "newImplementation", "type": "address" }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } + { "internalType": "bytes", "name": "data", "type": "bytes" } ], "name": "upgradeToAndCall", "outputs": [], diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index d6e6709..618d3cc 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -33,7 +33,11 @@ RegistrationWorkflowsClient, ) from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient +from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH +from story_protocol_python_sdk.utils.derivative_data import DerivativeData +from story_protocol_python_sdk.utils.function_signature import get_function_signature +from story_protocol_python_sdk.utils.ip_metadata import get_ip_metadata from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -764,143 +768,96 @@ def register_ip_and_attach_pil_terms( except Exception as e: raise e - # def register_derivative_ip( - # self, - # nft_contract: str, - # token_id: int, - # deriv_data: dict, - # metadata: dict = None, - # deadline: int = None, - # tx_options: dict = None - # ) -> dict: - # """ - # 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. - # :param parentIpIds list: The parent IP IDs. - # :param licenseTemplate str: License template address to be used. - # :param licenseTermsIds list: The license terms IDs. - # :param metadata dict: [Optional] Desired IP metadata. - # :param metadataURI str: [Optional] Metadata URI for the IP. - # :param metadataHash str: [Optional] Metadata hash for the IP. - # :param nftMetadataHash str: [Optional] NFT metadata hash. - # :param deadline int: [Optional] Signature deadline in milliseconds. - # :param tx_options dict: [Optional] Transaction options. - # :return dict: Dictionary with the tx hash and IP ID. - # """ - # try: - # ip_id = self._get_ip_id(nft_contract, token_id) - # if self._is_registered(ip_id): - # raise ValueError( - # f"The NFT with id {token_id} is already registered as IP." - # ) - - # 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'] - # ): - # if not self.license_registry_client.hasIpAttachedLicenseTerms( - # parent_ip_id, - # self.pi_license_template_client.contract.address, - # license_terms_id - # ): - # raise ValueError( - # f"License terms id {license_terms_id} must be attached to " - # 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, - # self.licensing_module_client.contract.address, - # calculated_deadline, - # "registerDerivative(address,address[],uint256[],address,bytes)", - # 2 - # ) - - # req_object = { - # 'nftContract': nft_contract, - # 'tokenId': token_id, - # 'derivData': { - # 'parentIpIds': [ - # self.web3.to_checksum_address(id) - # for id in deriv_data['parentIpIds'] - # ], - # 'licenseTermsIds': deriv_data['licenseTermsIds'], - # 'licenseTemplate': self.pi_license_template_client.contract.address, - # 'royaltyContext': ZERO_ADDRESS, - # }, - # 'sigRegister': { - # 'signer': self.web3.to_checksum_address(self.account.address), - # 'deadline': calculated_deadline, - # 'signature': sig_register_signature, - # }, - # 'metadata': { - # 'metadataURI': "", - # 'metadataHash': ZERO_HASH, - # 'nftMetadataHash': ZERO_HASH, - # }, - # 'sigMetadata': { - # 'signer': ZERO_ADDRESS, - # 'deadline': 0, - # '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, - # calculated_deadline, - # "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, - # self.derivative_workflows_client.build_registerIpAndMakeDerivative_transaction, # noqa: E501 - # req_object['nftContract'], - # req_object['tokenId'], - # req_object['derivData'], - # req_object['metadata'], - # req_object['sigMetadata'], - # 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 register_derivative_ip( + self, + nft_contract: str, + token_id: int, + deriv_data: dict, + metadata: dict | None = None, + deadline: int | None = None, + tx_options: dict | None = None, + ) -> dict: + """ + 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. + :param parent_ip_ids list[str]: The parent IP IDs (Address[]). + :param license_terms_ids list[int]: The IDs of the license terms that the parent IP supports (bigint[] | string[] | number[]). + :param max_minting_fee int: [Optional] The maximum minting fee that the caller is willing to pay. If set to 0 then no limit. Defaults to 0. + :param max_rts int: [Optional] The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). Defaults to 100,000,000. + :param max_revenue_share int: [Optional] The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100 (where 100% represents 100,000,000). Defaults to 100. + :param license_template str: [Optional] The address of the license template. Defaults to the License Template address if not provided. See https://docs.story.foundation/docs/programmable-ip-license for more information. + :param metadata dict: [Optional] Desired IP metadata. + :param ip_metadata_uri str: [Optional] The URI of the metadata for the IP. Defaults to "". + :param ip_metadata_hash str: [Optional] The hash of the metadata for the IP. Defaults to zero hash. + :param nft_metadata_uri str: [Optional] The URI of the metadata for the NFT. Defaults to "". + :param nft_metadata_hash str: [Optional] The hash of the metadata for the NFT. Defaults to zero hash. + :param deadline int: [Optional] Signature deadline in milliseconds. + :param tx_options dict: [Optional] Transaction options. + :return dict: Dictionary with the tx hash and IP ID. + """ + try: + ip_id = self._get_ip_id(nft_contract, token_id) + if self._is_registered(ip_id): + raise ValueError( + f"The NFT with id {token_id} is already registered as IP." + ) + validated_deriv_data = DerivativeData( + web3=self.web3, **deriv_data + ).get_validated_data() + calculated_deadline = self.sign_util.get_deadline(deadline=deadline) + sig_register_signature = self.sign_util.get_permission_signature( + ip_id=ip_id, + deadline=calculated_deadline, + state=Web3.to_bytes(0), + permissions=[ + { + "ipId": ip_id, + "signer": self.derivative_workflows_client.contract.address, + "to": self.core_metadata_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": get_function_signature( + self.core_metadata_module_client.contract.abi, + "setAll", + ), + }, + { + "ipId": ip_id, + "signer": self.derivative_workflows_client.contract.address, + "to": self.licensing_module_client.contract.address, + "permission": AccessPermission.ALLOW, + "func": get_function_signature( + self.licensing_module_client.contract.abi, + "registerDerivative", + ), + }, + ], + ) + response = build_and_send_transaction( + self.web3, + self.account, + self.derivative_workflows_client.build_registerIpAndMakeDerivative_transaction, + nft_contract, + token_id, + validated_deriv_data, + get_ip_metadata(metadata), + { + "signer": self.account.address, + "deadline": calculated_deadline, + "signature": sig_register_signature["signature"], + }, + 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): """ diff --git a/src/story_protocol_python_sdk/scripts/config.json b/src/story_protocol_python_sdk/scripts/config.json index 8f580e4..bdc4395 100644 --- a/src/story_protocol_python_sdk/scripts/config.json +++ b/src/story_protocol_python_sdk/scripts/config.json @@ -5,11 +5,9 @@ "contract_address": "0xcCF37d0a503Ee1D4C11208672e622ed3DFB2275a", "functions": [ "PermissionSet", - "setPermission", "setAllPermissions", - "setBatchPermissions", "setTransientPermission", - "setTransientBatchPermissions" + "setBatchTransientPermissions" ] }, { @@ -230,7 +228,7 @@ { "contract_name": "DerivativeWorkflows", "contract_address": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f", - "functions": [] + "functions": ["registerIpAndMakeDerivative"] } ] } diff --git a/src/story_protocol_python_sdk/types/common.py b/src/story_protocol_python_sdk/types/common.py index 7d9ea11..314e219 100644 --- a/src/story_protocol_python_sdk/types/common.py +++ b/src/story_protocol_python_sdk/types/common.py @@ -5,3 +5,17 @@ class RevShareType(Enum): COMMERCIAL_REVENUE_SHARE = "commercialRevShare" MAX_REVENUE_SHARE = "maxRevenueShare" MAX_ALLOWED_REWARD_SHARE = "maxAllowedRewardShare" + + +class AccessPermission(Enum): + """ + Permission level + """ + + # ABSTAIN means having not enough information to make decision at + # current level, deferred decision to up. + ABSTAIN = 0 + # ALLOW means the permission is granted to transaction signer to call the function. + ALLOW = 1 + # DENY means the permission is denied to transaction signer to call the function. + DENY = 2 diff --git a/src/story_protocol_python_sdk/utils/derivative_data.py b/src/story_protocol_python_sdk/utils/derivative_data.py index 610369b..15a539a 100644 --- a/src/story_protocol_python_sdk/utils/derivative_data.py +++ b/src/story_protocol_python_sdk/utils/derivative_data.py @@ -13,7 +13,7 @@ PILicenseTemplateClient, ) from story_protocol_python_sdk.types.common import RevShareType -from story_protocol_python_sdk.utils.constants import MAX_ROYALTY_TOKEN +from story_protocol_python_sdk.utils.constants import MAX_ROYALTY_TOKEN, ZERO_ADDRESS from story_protocol_python_sdk.utils.validation import get_revenue_share @@ -26,7 +26,7 @@ class DerivativeData: license_terms_ids: List[int] max_minting_fee: int | float = field(default=0) max_rts: int | float = field(default=MAX_ROYALTY_TOKEN) - max_revenue_share: int | float = field(default=100) + max_revenue_share: int = field(default=100) license_template: Optional[str] = field(default=None) pi_license_template_client: PILicenseTemplateClient = field(init=False) @@ -64,7 +64,7 @@ def validate_parent_ip_ids_and_license_terms_ids(self): ip_asset_registry_client: IPAssetRegistryClient = self.ip_asset_registry_client license_registry_client: LicenseRegistryClient = self.license_registry_client - + total_royalty_percent = 0 for parent_ip_id, license_terms_id in zip( self.parent_ip_ids, self.license_terms_ids ): @@ -81,9 +81,13 @@ def validate_parent_ip_ids_and_license_terms_ids(self): royalty_percent = license_registry_client.getRoyaltyPercent( parent_ip_id, self.license_template, license_terms_id ) - if self.max_revenue_share != 0 and royalty_percent > self.max_revenue_share: + total_royalty_percent += royalty_percent + if ( + self.max_revenue_share != 0 + and total_royalty_percent > self.max_revenue_share + ): raise ValueError( - f"The royalty percent for the parent IP {parent_ip_id} is greater than the maximum revenue share {self.max_revenue_share}." + f"The total royalty percent for the parent IP {parent_ip_id} is greater than the maximum revenue share {self.max_revenue_share}." ) def validate_max_minting_fee(self): @@ -104,4 +108,5 @@ def get_validated_data(self) -> dict: "maxRts": self.max_rts, "maxRevenueShare": self.max_revenue_share, "licenseTemplate": self.license_template, + "royaltyContext": ZERO_ADDRESS, } diff --git a/src/story_protocol_python_sdk/utils/function_signature.py b/src/story_protocol_python_sdk/utils/function_signature.py new file mode 100644 index 0000000..8524cce --- /dev/null +++ b/src/story_protocol_python_sdk/utils/function_signature.py @@ -0,0 +1,57 @@ +from typing import Any, Dict, List + + +def get_function_signature( + abi: List[Dict[str, Any]], + method_name: str, +) -> str: + """ + Gets the function signature from an ABI for a given method name. + + Args: + abi: The contract ABI as a list of dictionaries + method_name: The name of the method to get the signature for + + Returns: + The function signature in standard format (e.g. "methodName(uint256,address)") + """ + + # Filter functions by name and type + functions = [ + item + for item in abi + if item.get("type") == "function" and item.get("name") == method_name + ] + + if len(functions) == 0: + raise ValueError(f"Method {method_name} not found in ABI.") + + # Get the target function + func = functions[0] + + def get_type_string(input_param: Dict[str, Any]) -> str: + """ + Recursively get the type string for a parameter. + + Args: + input_param: The ABI parameter as a dictionary + + Returns: + The type string representation + """ + param_type = input_param["type"] + + if param_type.startswith("tuple"): + components = input_param.get("components", []) + if components: + component_types = ",".join(get_type_string(comp) for comp in components) + return f"({component_types})" + else: + return "()" # Empty tuple + return param_type + + # Build the function signature + inputs = ",".join( + get_type_string(input_param) for input_param in func.get("inputs", []) + ) + return f"{method_name}({inputs})" diff --git a/src/story_protocol_python_sdk/utils/ip_metadata.py b/src/story_protocol_python_sdk/utils/ip_metadata.py index 8c1ba6b..0a056a1 100644 --- a/src/story_protocol_python_sdk/utils/ip_metadata.py +++ b/src/story_protocol_python_sdk/utils/ip_metadata.py @@ -1,20 +1,16 @@ -from typing import Optional - -from eth_typing import HexStr - -from story_protocol_python_sdk.types.common import IpMetadataForWorkflow from story_protocol_python_sdk.utils.constants import ZERO_HASH -def get_ip_metadata_for_workflow( - ip_metadata_uri: Optional[str], - ip_metadata_hash: Optional[HexStr], - nft_metadata_uri: Optional[str], - nft_metadata_hash: Optional[HexStr], -) -> IpMetadataForWorkflow: +def get_ip_metadata( + metadata: dict | None = None, +) -> dict: return { - "ip_metadata_uri": ip_metadata_uri or "", - "ip_metadata_hash": ip_metadata_hash or ZERO_HASH, - "nft_metadata_uri": nft_metadata_uri or "", - "nft_metadata_hash": nft_metadata_hash or ZERO_HASH, + "ipMetadataURI": metadata.get("ip_metadata_uri", "") if metadata else "", + "ipMetadataHash": ( + metadata.get("ip_metadata_hash", ZERO_HASH) if metadata else ZERO_HASH + ), + "nftMetadataURI": metadata.get("nft_metadata_uri", "") if metadata else "", + "nftMetadataHash": ( + metadata.get("nft_metadata_hash", ZERO_HASH) if metadata else ZERO_HASH + ), } diff --git a/src/story_protocol_python_sdk/utils/sign.py b/src/story_protocol_python_sdk/utils/sign.py index 2e63bd6..448ecc3 100644 --- a/src/story_protocol_python_sdk/utils/sign.py +++ b/src/story_protocol_python_sdk/utils/sign.py @@ -91,7 +91,7 @@ def get_signature( signed_message = Account.sign_message(signable_message, self.account.key) return { - "signature": signed_message.signature.hex(), + "signature": "0x" + signed_message.signature.hex(), "nonce": expected_state, } @@ -159,7 +159,7 @@ def get_permission_signature( if permissions[0].get("func") else b"\x00\x00\x00\x00" ), - permissions[0]["permission"], + permissions[0]["permission"].value, ], ) else: @@ -175,7 +175,7 @@ def get_permission_signature( if p.get("func") else b"\x00\x00\x00\x00" ), - "permission": p["permission"], + "permission": p["permission"].value, } formatted_permissions.append(formatted_permission) diff --git a/src/story_protocol_python_sdk/utils/validation.py b/src/story_protocol_python_sdk/utils/validation.py index d2e8da4..5d0155b 100644 --- a/src/story_protocol_python_sdk/utils/validation.py +++ b/src/story_protocol_python_sdk/utils/validation.py @@ -18,9 +18,9 @@ def validate_address(address: str) -> str: def get_revenue_share( - revShare: int | float, + revShare: int, type: RevShareType = RevShareType.COMMERCIAL_REVENUE_SHARE, -) -> int | float: +) -> int: """ Convert revenue share percentage to token amount. @@ -31,4 +31,4 @@ def get_revenue_share( if revShare < 0 or revShare > 100: raise ValueError(f"The {type.value} must be between 0 and 100.") - return (revShare * MAX_ROYALTY_TOKEN) / 100 + return (revShare * MAX_ROYALTY_TOKEN) // 100 diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index ff9a5a0..86dc4b0 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -280,7 +280,7 @@ def parent_ip_and_license_terms(self, story_client: StoryClient, nft_collection) "commercial_attribution": False, "commercializer_checker": ZERO_ADDRESS, "commercializer_checker_data": ZERO_ADDRESS, - "commercial_rev_share": 90, + "commercial_rev_share": 50, "commercial_rev_ceiling": 0, "derivatives_allowed": True, "derivatives_attribution": True, @@ -327,25 +327,70 @@ def parent_ip_and_license_terms(self, story_client: StoryClient, nft_collection) # assert isinstance(response['ip_id'], str) # assert response['ip_id'] != '' - # def test_register_derivative_ip(self, story_client, parent_ip_id, license_terms_id): - # token_child_id = mint_by_spg(MockERC721, story_client.web3, story_client.account) - - # result = story_client.IPAsset.register_derivative_ip( - # nft_contract=MockERC721, - # token_id=token_child_id, - # deriv_data={ - # 'parentIpIds': [parent_ip_id], - # 'licenseTermsIds': [license_terms_id], - # 'maxMintingFee': 0, - # 'maxRts': 5 * 10**6, - # 'maxRevenueShare': 0 - # }, - # deadline=1000, - # tx_options={'waitForTransaction': True} - # ) + def test_register_derivative_ip( + self, story_client: StoryClient, parent_ip_and_license_terms, nft_collection + ): + token_child_id = mint_by_spg( + nft_collection, story_client.web3, story_client.account + ) + # Register another IP asset with PIL terms + second_ip_id_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": 50, + "commercial_rev_ceiling": 0, + "derivatives_allowed": True, + "derivatives_attribution": True, + "derivatives_approval": False, + "derivatives_reciprocal": True, + "derivative_rev_ceiling": 0, + "currency": MockERC20, + "uri": "", + }, + "licensing_config": { + "is_set": True, + "minting_fee": 0, + "hook_data": ZERO_ADDRESS, + "licensing_hook": ZERO_ADDRESS, + "commercial_rev_share": 0, + "disabled": False, + "expect_minimum_group_reward_share": 0, + "expect_group_reward_pool": ZERO_ADDRESS, + }, + } + ], + allow_duplicates=True, + ) + ) - # assert isinstance(result['tx_hash'], str) and result['tx_hash'] - # assert isinstance(result['ip_id'], str) and result['ip_id'] + result = story_client.IPAsset.register_derivative_ip( + nft_contract=nft_collection, + token_id=token_child_id, + deriv_data={ + "parent_ip_ids": [ + parent_ip_and_license_terms["parent_ip_id"], + second_ip_id_response["ip_id"], + ], + "license_terms_ids": [ + parent_ip_and_license_terms["license_terms_id"], + second_ip_id_response["license_terms_ids"][0], + ], + }, + deadline=1000, + ) + 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: StoryClient, nft_collection, parent_ip_and_license_terms diff --git a/tests/unit/utils/test_derivative_data.py b/tests/unit/utils/test_derivative_data.py index 615b99a..dbd82b6 100644 --- a/tests/unit/utils/test_derivative_data.py +++ b/tests/unit/utils/test_derivative_data.py @@ -19,7 +19,7 @@ from tests.unit.fixtures.web3 import mock_web3 -@pytest.fixture +@pytest.fixture(scope="module") def mock_is_checksum_address(): """Fixture to mock Web3.is_checksum_address""" @@ -31,7 +31,7 @@ def _mock_is_checksum_address(is_checksum_address=True): return _mock_is_checksum_address -@pytest.fixture +@pytest.fixture(scope="module") def mock_ip_asset_registry_client(): """Fixture to mock IPAssetRegistryClient""" @@ -45,7 +45,7 @@ def _mock_ip_registered(is_registered=True): return _mock_ip_registered -@pytest.fixture +@pytest.fixture(scope="module") def mock_license_registry_client(): """Fixture to mock LicenseRegistryClient""" @@ -66,7 +66,7 @@ def _mock_license_registry_client( return _mock_license_registry_client -@pytest.fixture +@pytest.fixture(scope="module") def mock_pi_license_template_client(): """Fixture to mock PILicenseTemplateClient""" @@ -194,7 +194,7 @@ def test_validate_royalty_percent_exceeds_max_revenue_share( ): with raises( ValueError, - match="The royalty percent for the parent IP 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 is greater than the maximum revenue share 110000.0", + match="The total royalty percent for the parent IP 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 is greater than the maximum revenue share 1000000", ): DerivativeData( web3=mock_web3, @@ -202,7 +202,7 @@ def test_validate_royalty_percent_exceeds_max_revenue_share( license_terms_ids=[2], max_minting_fee=10, max_rts=10, - max_revenue_share=0.11, + max_revenue_share=1, license_template="0x1234567890123456789012345678901234567890", ) @@ -388,6 +388,7 @@ def test_get_validated_data_with_default_values( "maxRts": MAX_ROYALTY_TOKEN, "maxRevenueShare": MAX_ROYALTY_TOKEN, "licenseTemplate": ADDRESS, + "royaltyContext": "0x0000000000000000000000000000000000000000", } def test_get_validated_data_with_custom_values( @@ -414,4 +415,5 @@ def test_get_validated_data_with_custom_values( "maxRts": 10, "maxRevenueShare": 10000000.0, "licenseTemplate": "0x1234567890123456789012345678901234567890", + "royaltyContext": "0x0000000000000000000000000000000000000000", } From 52367bfe4bc45243a8e2c58a9187c7e0347488a8 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 8 Aug 2025 15:23:03 +0800 Subject: [PATCH 07/16] Add conftest.py for unit tests --- tests/unit/conftest.py | 114 ++++++++++++++++++++++++++++++++++++ tests/unit/fixtures/web3.py | 25 -------- 2 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 tests/unit/conftest.py delete mode 100644 tests/unit/fixtures/web3.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..afab7be --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,114 @@ +from unittest.mock import MagicMock, Mock, patch + +import pytest +from eth_account import Account +from web3 import Web3 + +from tests.unit.fixtures.data import ADDRESS, TX_HASH + + +@pytest.fixture(scope="package") +def mock_account(): + account = MagicMock() + account.address = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" + # Create a mock signed transaction object with raw_transaction attribute + mock_signed_txn = MagicMock() + mock_signed_txn.raw_transaction = b"raw_transaction_bytes" + + account.sign_transaction = MagicMock(return_value=mock_signed_txn) + account.sign_message = MagicMock(return_value=b"mock_signature") + return account + + +@pytest.fixture(scope="package") +def mock_web3(): + mock_web3 = Mock(spec=Web3) + mock_web3.to_checksum_address = MagicMock(return_value=ADDRESS) + mock_web3.to_bytes = MagicMock(return_value=b"mock_bytes") + + # Add eth attribute with contract method + mock_eth = Mock() + + # Create a function that returns a new mock contract each time + def create_mock_contract(*args, **kwargs): + """Create a new mock contract instance with address""" + mock_contract = Mock() + mock_contract.address = ADDRESS + mock_contract.encode_abi = MagicMock(return_value="0x00") + return mock_contract + + # Set up the contract method to return new mock contracts + mock_eth.contract = create_mock_contract + mock_web3.eth = mock_eth + mock_web3.eth.get_transaction_count = MagicMock(return_value=0) + mock_web3.eth.send_raw_transaction = MagicMock(return_value=TX_HASH) + mock_web3.eth.wait_for_transaction_receipt = MagicMock( + return_value={"status": 1, "logs": []} + ) + return mock_web3 + + +@pytest.fixture(scope="package") +def mock_is_checksum_address(): + def _mock(is_checksum_address: bool = True): + return patch.object( + Web3, "is_checksum_address", return_value=is_checksum_address + ) + + return _mock + + +@pytest.fixture(scope="package") +def mock_signature_related_methods(): + class SignatureMockContext: + def __init__(self): + self.patches = [] + + def __enter__(self): + # Mock the IPAccountImplClient constructor and its contract.encode_abi method + mock_client = MagicMock() + mock_contract = MagicMock() + mock_contract.encode_abi = MagicMock(return_value=b"encoded_data") + mock_client.contract = mock_contract + + # Create all the patches + mock_web3_to_bytes = patch.object( + Web3, "to_bytes", return_value=b"mock_bytes" + ) + mock_account_sign_message = patch.object( + Account, + "sign_message", + return_value=MagicMock(signature=b"mock_signature"), + ) + + # Create a mock class that behaves like IPAccountImplClient + class MockIPAccountImplClient: + def __init__(self, web3, contract_address=None): + self.web3 = web3 + self.contract_address = contract_address + self.contract = mock_contract + + # Patch the class to return our mock instance + mock_ip_account_client = patch( + "story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client.IPAccountImplClient", + MockIPAccountImplClient, + ) + + # Apply all patches at once + mock_web3_to_bytes.start() + mock_account_sign_message.start() + mock_ip_account_client.start() + + # Store patches for cleanup + self.patches = [ + mock_web3_to_bytes, + mock_account_sign_message, + mock_ip_account_client, + ] + + def __exit__(self, exc_type, exc_val, exc_tb): + # Stop all patches in reverse order + for patch_obj in reversed(self.patches): + patch_obj.stop() + + return SignatureMockContext diff --git a/tests/unit/fixtures/web3.py b/tests/unit/fixtures/web3.py deleted file mode 100644 index 6817f8e..0000000 --- a/tests/unit/fixtures/web3.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest.mock import MagicMock, Mock - -from web3 import Web3 - -from tests.unit.fixtures.data import ADDRESS - -mock_web3 = Mock(spec=Web3) -mock_web3.to_checksum_address = MagicMock(return_value=ADDRESS) - -# Add eth attribute with contract method -mock_eth = Mock() - - -# Create a function that returns a new mock contract each time -def create_mock_contract(*args, **kwargs): - """Create a new mock contract instance with address""" - mock_contract = Mock() - mock_contract.address = ADDRESS - mock_contract.encode_abi = MagicMock(return_value="0x00") - return mock_contract - - -# Set up the contract method to return new mock contracts -mock_eth.contract = create_mock_contract -mock_web3.eth = mock_eth From b5830d79c1f5ef9cfd5f1aba9e0ae2546f8e35e4 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 8 Aug 2025 15:45:47 +0800 Subject: [PATCH 08/16] Refactor IPAsset permission handling and clean up test fixtures --- .../resources/IPAsset.py | 2 +- src/story_protocol_python_sdk/utils/sign.py | 1 - tests/unit/fixtures/data.py | 2 +- tests/unit/resources/test_ip_asset.py | 232 ++++++------------ tests/unit/resources/test_permission.py | 3 +- tests/unit/utils/test_derivative_data.py | 72 +++--- 6 files changed, 117 insertions(+), 195 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 618d3cc..3cad983 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -177,7 +177,7 @@ def register( "signer": self.registration_workflows_client.contract.address, "to": self.core_metadata_module_client.contract.address, "func": "setAll(address,string,bytes32,bytes32)", - "permission": 1, + "permission": AccessPermission.ALLOW, } ], ) diff --git a/src/story_protocol_python_sdk/utils/sign.py b/src/story_protocol_python_sdk/utils/sign.py index 448ecc3..e5be696 100644 --- a/src/story_protocol_python_sdk/utils/sign.py +++ b/src/story_protocol_python_sdk/utils/sign.py @@ -46,7 +46,6 @@ def get_signature( execute_data = self.ip_account_client.contract.encode_abi( abi_element_identifier="execute", args=[to, 0, encode_data] ) - # expected_state = nonce expected_state = Web3.keccak( encode( diff --git a/tests/unit/fixtures/data.py b/tests/unit/fixtures/data.py index 91040db..6191583 100644 --- a/tests/unit/fixtures/data.py +++ b/tests/unit/fixtures/data.py @@ -1,6 +1,6 @@ CHAIN_ID = 1315 ADDRESS = "0x1234567890123456789012345678901234567890" -TX_HASH = "0x0c0cce07beb64ccfbdd59da111f23084ab7c9e96a951f7381af49e792d014c04" +TX_HASH = b"tx_hash_bytes" # STATE as bytes32 (32 bytes = 64 hex characters) STATE = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" IP_ID = "0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57" diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 8fe3917..a4a2a39 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -1,179 +1,105 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch # noqa: F401 import pytest -from eth_utils import is_address, to_checksum_address -from web3 import Web3 from story_protocol_python_sdk.resources.IPAsset import IPAsset +from story_protocol_python_sdk.utils.constants import ZERO_HASH +from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH -ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000" -ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +@pytest.fixture(scope="module") +def ip_asset(mock_web3, mock_account): + return IPAsset(mock_web3, mock_account, CHAIN_ID) -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) +@pytest.fixture(scope="module") +def mock_get_ip_id(ip_asset): + def _mock(): + return patch.object( + ip_asset.ip_asset_registry_client, "ipId", return_value=IP_ID + ) - def is_connected(self): - return True + return _mock -@pytest.fixture -def mock_web3(): - return MockWeb3() +@pytest.fixture(scope="module") +def mock_is_registered(ip_asset): + def _mock(is_registered: bool = False): + return patch.object( + ip_asset.ip_asset_registry_client, + "isRegistered", + return_value=is_registered, + ) + return _mock -@pytest.fixture -def mock_account(): - account = MagicMock() - account.address = "0xF60cBF0Ea1A61567F1dDaf79A6219D20d189155c" - return account +@pytest.fixture(scope="module") +def mock_parse_ip_registered_event(ip_asset): + def _mock(): + return patch.object( + ip_asset, "_parse_tx_ip_registered_event", return_value={"ip_id": IP_ID} + ) -@pytest.fixture -def ip_asset(mock_web3, mock_account): - chain_id = 1516 - return IPAsset(mock_web3, mock_account, chain_id) + return _mock 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 pytest.raises(ValueError): + def test_register_invalid_deadline_type( + self, ip_asset, mock_get_ip_id, mock_is_registered + ): + with mock_get_ip_id(), mock_is_registered(): + with pytest.raises(ValueError, match="Invalid deadline value."): ip_asset.register( - nft_contract="0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c", + nft_contract=ADDRESS, token_id=3, deadline="error", ip_metadata={"ip_metadata_uri": "1", "ip_metadata_hash": ZERO_HASH}, ) - def test_register_already_registered(self, ip_asset): - token_contract = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" - 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 - ): - response = ip_asset.register(token_contract, token_id) - assert response["ip_id"] == ip_id + def test_register_already_registered( + self, ip_asset, mock_get_ip_id, mock_is_registered + ): + with mock_get_ip_id(), mock_is_registered(True): + response = ip_asset.register(ADDRESS, 3) + assert response["ip_id"] == IP_ID assert response["tx_hash"] is None - def test_register_successful(self, ip_asset): - token_contract = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" - token_id = 3 - ip_id = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" - tx_hash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997" - - class MockTxHash: - def hex(self): - return tx_hash - - mock_tx_hash = MockTxHash() - - mock_signed_txn = MagicMock() - 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} - ): - - result = ip_asset.register(token_contract, token_id) - assert result["tx_hash"] == tx_hash - assert result["ip_id"] == ip_id - - def test_register_with_metadata(self, ip_asset): - token_contract = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" - token_id = 3 - ip_id = "0x1daAE3197Bc469Cb97B917aa460a12dD95c6627c" - tx_hash = "0x129f7dd802200f096221dd89d5b086e4bd3ad6eafb378a0c75e3b04fc375f997" - - metadata = { - "ip_metadata_uri": "", - "ip_metadata_hash": ZERO_HASH, - "nft_metadata_uri": "", - "nft_metadata_hash": ZERO_HASH, - } - - calculated_deadline = 1000 - - class MockTxHash: - def hex(self): - return tx_hash - - mock_tx_hash = MockTxHash() - - mock_signed_txn = MagicMock() - 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}, - ): - - result = ip_asset.register( - nft_contract=token_contract, - token_id=token_id, - ip_metadata=metadata, - deadline=1000, - ) - - assert result["tx_hash"] == tx_hash - assert result["ip_id"] == ip_id + def test_register_successful( + self, + ip_asset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + ): + with mock_get_ip_id(), mock_is_registered(), mock_parse_ip_registered_event(): + + result = ip_asset.register(ADDRESS, 3) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID + + def test_register_with_metadata( + self, + ip_asset: IPAsset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + mock_signature_related_methods, + ): + + with mock_get_ip_id(), mock_is_registered(), mock_parse_ip_registered_event(): + with mock_signature_related_methods(): + result = ip_asset.register( + nft_contract=ADDRESS, + token_id=3, + ip_metadata={ + "ip_metadata_uri": "", + "ip_metadata_hash": ZERO_HASH, + "nft_metadata_uri": "", + "nft_metadata_hash": ZERO_HASH, + }, + deadline=1000, + ) + + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID diff --git a/tests/unit/resources/test_permission.py b/tests/unit/resources/test_permission.py index 9b0687b..87b74ba 100644 --- a/tests/unit/resources/test_permission.py +++ b/tests/unit/resources/test_permission.py @@ -4,11 +4,10 @@ from story_protocol_python_sdk.resources.Permission import Permission from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, STATE, TX_HASH -from tests.unit.fixtures.web3 import mock_web3 @pytest.fixture -def permission(): +def permission(mock_web3): return Permission(mock_web3, ADDRESS, CHAIN_ID) diff --git a/tests/unit/utils/test_derivative_data.py b/tests/unit/utils/test_derivative_data.py index dbd82b6..a3a41e6 100644 --- a/tests/unit/utils/test_derivative_data.py +++ b/tests/unit/utils/test_derivative_data.py @@ -2,7 +2,6 @@ import pytest from _pytest.raises import raises -from web3 import Web3 from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( IPAssetRegistryClient, @@ -16,19 +15,6 @@ from story_protocol_python_sdk.utils.constants import MAX_ROYALTY_TOKEN from story_protocol_python_sdk.utils.derivative_data import DerivativeData from tests.unit.fixtures.data import ADDRESS, IP_ID -from tests.unit.fixtures.web3 import mock_web3 - - -@pytest.fixture(scope="module") -def mock_is_checksum_address(): - """Fixture to mock Web3.is_checksum_address""" - - def _mock_is_checksum_address(is_checksum_address=True): - return patch.object( - Web3, "is_checksum_address", return_value=is_checksum_address - ) - - return _mock_is_checksum_address @pytest.fixture(scope="module") @@ -84,7 +70,7 @@ def _mock_pi_license_template_client(): class TestValidateParentIpIdsAndLicenseTermsIds: - def test_validate_parent_ip_ids_is_empty(self): + def test_validate_parent_ip_ids_is_empty(self, mock_web3): with raises(ValueError, match="The parent IP IDs must be provided."): DerivativeData( web3=mock_web3, @@ -96,7 +82,7 @@ def test_validate_parent_ip_ids_is_empty(self): license_template="0x1234567890123456789012345678901234567890", ) - def test_validate_license_terms_ids_is_empty(self): + def test_validate_license_terms_ids_is_empty(self, mock_web3): with raises(ValueError, match="The license terms IDs must be provided."): DerivativeData( web3=mock_web3, @@ -108,7 +94,9 @@ def test_validate_license_terms_ids_is_empty(self): license_template="0x1234567890123456789012345678901234567890", ) - def test_validate_parent_ip_ids_and_license_terms_ids_are_not_equal(self): + def test_validate_parent_ip_ids_and_license_terms_ids_are_not_equal( + self, mock_web3 + ): with raises( ValueError, match="The number of parent IP IDs must match the number of license terms IDs.", @@ -124,7 +112,7 @@ def test_validate_parent_ip_ids_and_license_terms_ids_are_not_equal(self): ) def test_validate_parent_ip_ids_is_not_valid_address( - self, mock_is_checksum_address + self, mock_web3, mock_is_checksum_address ): with mock_is_checksum_address(is_checksum_address=False): with raises(ValueError, match="The parent IP ID must be a valid address."): @@ -139,7 +127,10 @@ def test_validate_parent_ip_ids_is_not_valid_address( ) def test_validate_parent_ip_ids_is_not_registered( - self, mock_ip_asset_registry_client, mock_is_checksum_address + self, + mock_web3, + mock_is_checksum_address, + mock_ip_asset_registry_client, ): with mock_is_checksum_address(), mock_ip_asset_registry_client( is_registered=False @@ -160,9 +151,10 @@ def test_validate_parent_ip_ids_is_not_registered( def test_validate_license_terms_not_attached( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client( is_registered=True @@ -183,9 +175,10 @@ def test_validate_license_terms_not_attached( def test_validate_royalty_percent_exceeds_max_revenue_share( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client( is_registered=True @@ -208,9 +201,10 @@ def test_validate_royalty_percent_exceeds_max_revenue_share( def test_validate_royalty_percent_is_less_than_max_revenue_share( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): derivative_data = DerivativeData( @@ -227,9 +221,10 @@ def test_validate_royalty_percent_is_less_than_max_revenue_share( class TestValidateMaxMintingFee: def test_validate_max_minting_fee_is_less_than_0( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): with raises( @@ -249,9 +244,10 @@ def test_validate_max_minting_fee_is_less_than_0( class TestValidateMaxRts: def test_validate_max_rts_is_less_than_0( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): with raises( @@ -266,9 +262,7 @@ def test_validate_max_rts_is_less_than_0( ) def test_validate_max_rts_is_greater_than_100_000_000( - self, - mock_ip_asset_registry_client, - mock_license_registry_client, + self, mock_web3, mock_ip_asset_registry_client, mock_license_registry_client ): with mock_ip_asset_registry_client(), mock_license_registry_client(): with raises( @@ -284,9 +278,10 @@ def test_validate_max_rts_is_greater_than_100_000_000( def test_validate_max_rts_default_value_is_max_rts( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): derivative_data = DerivativeData( @@ -299,9 +294,7 @@ def test_validate_max_rts_default_value_is_max_rts( class TestValidateMaxRevenueShare: def test_validate_max_revenue_share_is_less_than_0( - self, - mock_ip_asset_registry_client, - mock_license_registry_client, + self, mock_web3, mock_ip_asset_registry_client, mock_license_registry_client ): with mock_ip_asset_registry_client(), mock_license_registry_client(): with raises( @@ -318,9 +311,10 @@ def test_validate_max_revenue_share_is_less_than_0( def test_validate_max_revenue_share_is_greater_than_100( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): with raises( @@ -337,9 +331,10 @@ def test_validate_max_revenue_share_is_greater_than_100( def test_validate_max_revenue_share_default_value_is_100( self, + mock_web3, + mock_is_checksum_address, mock_ip_asset_registry_client, mock_license_registry_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): derivative_data = DerivativeData( @@ -353,10 +348,11 @@ def test_validate_max_revenue_share_default_value_is_100( class TestValidateLicenseTemplate: def test_validate_license_template_default_value_is_pi_license_template( self, + mock_web3, + mock_is_checksum_address, + mock_pi_license_template_client, mock_ip_asset_registry_client, mock_license_registry_client, - mock_pi_license_template_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): derivative_data = DerivativeData( @@ -370,10 +366,11 @@ def test_validate_license_template_default_value_is_pi_license_template( class TestGetValidatedData: def test_get_validated_data_with_default_values( self, + mock_web3, + mock_is_checksum_address, + mock_pi_license_template_client, mock_ip_asset_registry_client, mock_license_registry_client, - mock_pi_license_template_client, - mock_is_checksum_address, ): with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): derivative_data = DerivativeData( @@ -393,6 +390,7 @@ def test_get_validated_data_with_default_values( def test_get_validated_data_with_custom_values( self, + mock_web3, mock_ip_asset_registry_client, mock_license_registry_client, mock_pi_license_template_client, From e5ffcc00b0f8d220ebab7ae5abe2a34143c0a8b9 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Fri, 8 Aug 2025 17:33:32 +0800 Subject: [PATCH 09/16] Add unit test for registerDerivativeIp method --- tests/unit/conftest.py | 38 ++++++++++++ tests/unit/fixtures/data.py | 2 +- tests/unit/resources/test_ip_asset.py | 78 ++++++++++++++++++++++-- tests/unit/utils/test_derivative_data.py | 6 +- 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index afab7be..08b7793 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -112,3 +112,41 @@ def __exit__(self, exc_type, exc_val, exc_tb): patch_obj.stop() return SignatureMockContext + + +@pytest.fixture(scope="package") +def mock_license_registry_client(): + """Fixture to mock LicenseRegistryClient for derivative data validation""" + + def _mock(): + # Create a mock that returns a proper value for getRoyaltyPercent + mock_client = MagicMock() + mock_client.hasIpAttachedLicenseTerms = MagicMock(return_value=True) + mock_client.getRoyaltyPercent = MagicMock(return_value=10) + + # Patch both IPAsset and derivative_data modules + patch1 = patch( + "story_protocol_python_sdk.resources.IPAsset.LicenseRegistryClient", + return_value=mock_client, + ) + patch2 = patch( + "story_protocol_python_sdk.utils.derivative_data.LicenseRegistryClient", + return_value=mock_client, + ) + + # Start both patches + patch1.start() + patch2.start() + + # Return a context manager that stops both patches + class MockContext: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + patch1.stop() + patch2.stop() + + return MockContext() + + return _mock diff --git a/tests/unit/fixtures/data.py b/tests/unit/fixtures/data.py index 6191583..42203f7 100644 --- a/tests/unit/fixtures/data.py +++ b/tests/unit/fixtures/data.py @@ -3,4 +3,4 @@ TX_HASH = b"tx_hash_bytes" # STATE as bytes32 (32 bytes = 64 hex characters) STATE = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" -IP_ID = "0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57" +IP_ID = "0xFEB4eE75600768635010D80D56a5711268D26DaB" diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index a4a2a39..2edade6 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch # noqa: F401 +from unittest.mock import patch import pytest @@ -7,12 +7,12 @@ from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH -@pytest.fixture(scope="module") +@pytest.fixture(scope="class") def ip_asset(mock_web3, mock_account): return IPAsset(mock_web3, mock_account, CHAIN_ID) -@pytest.fixture(scope="module") +@pytest.fixture(scope="class") def mock_get_ip_id(ip_asset): def _mock(): return patch.object( @@ -22,7 +22,7 @@ def _mock(): return _mock -@pytest.fixture(scope="module") +@pytest.fixture(scope="class") def mock_is_registered(ip_asset): def _mock(is_registered: bool = False): return patch.object( @@ -34,7 +34,7 @@ def _mock(is_registered: bool = False): return _mock -@pytest.fixture(scope="module") +@pytest.fixture(scope="class") def mock_parse_ip_registered_event(ip_asset): def _mock(): return patch.object( @@ -44,6 +44,17 @@ def _mock(): return _mock +@pytest.fixture(scope="class") +def mock_get_function_signature(): + def _mock(): + return patch( + "story_protocol_python_sdk.resources.IPAsset.get_function_signature", + return_value="setAll(address,string,bytes32,bytes32)", + ) + + return _mock + + class TestIPAssetRegister: def test_register_invalid_deadline_type( self, ip_asset, mock_get_ip_id, mock_is_registered @@ -103,3 +114,60 @@ def test_register_with_metadata( assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID + + +class TestRegisterDerivativeIp: + def test_ip_is_already_registered( + self, ip_asset, mock_get_ip_id, mock_is_registered + ): + with mock_get_ip_id(), mock_is_registered(True): + with pytest.raises( + ValueError, match="The NFT with id 3 is already registered as IP." + ): + ip_asset.register_derivative_ip( + nft_contract=ADDRESS, + token_id=3, + deriv_data={ + "max_minting_fee": 1000000000000000000, + "max_rts": 1000000000000000000, + "max_revenue_share": 1000000000000000000, + }, + ) + + def test_parent_ip_id_is_empty(self, ip_asset, mock_get_ip_id, mock_is_registered): + with mock_get_ip_id(), mock_is_registered(): + with pytest.raises(ValueError, match="The parent IP IDs must be provided."): + ip_asset.register_derivative_ip( + nft_contract=ADDRESS, + token_id=3, + deriv_data={ + "parent_ip_ids": [], + "license_terms_ids": [], + }, + ) + + def test_success( + self, + ip_asset, + mock_get_ip_id, + mock_is_registered, + mock_parse_ip_registered_event, + mock_signature_related_methods, + mock_get_function_signature, + mock_license_registry_client, + ): + with mock_get_ip_id(), mock_is_registered(), mock_parse_ip_registered_event(), mock_get_function_signature(), mock_license_registry_client(): + with mock_signature_related_methods(): + result = ip_asset.register_derivative_ip( + nft_contract=ADDRESS, + token_id=3, + deriv_data={ + "max_minting_fee": 10, + "max_rts": 100, + "max_revenue_share": 100, + "parent_ip_ids": [IP_ID, IP_ID], + "license_terms_ids": [1, 2], + }, + ) + assert result["tx_hash"] == TX_HASH.hex() + assert result["ip_id"] == IP_ID diff --git a/tests/unit/utils/test_derivative_data.py b/tests/unit/utils/test_derivative_data.py index a3a41e6..5c60cef 100644 --- a/tests/unit/utils/test_derivative_data.py +++ b/tests/unit/utils/test_derivative_data.py @@ -137,7 +137,7 @@ def test_validate_parent_ip_ids_is_not_registered( ): with raises( ValueError, - match="The parent IP ID 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 must be registered.", + match=f"The parent IP ID {IP_ID} must be registered.", ): DerivativeData( web3=mock_web3, @@ -161,7 +161,7 @@ def test_validate_license_terms_not_attached( ), mock_license_registry_client(has_ip_attached_license_terms=False): with raises( ValueError, - match="License terms id 2 must be attached to the parent ipId 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 before registering derivative.", + match=f"License terms id 2 must be attached to the parent ipId {IP_ID} before registering derivative.", ): DerivativeData( web3=mock_web3, @@ -187,7 +187,7 @@ def test_validate_royalty_percent_exceeds_max_revenue_share( ): with raises( ValueError, - match="The total royalty percent for the parent IP 0xaeF5999378C0Af338Db01f38F6Ac51E82E4E5a57 is greater than the maximum revenue share 1000000", + match=f"The total royalty percent for the parent IP {IP_ID} is greater than the maximum revenue share 1000000", ): DerivativeData( web3=mock_web3, From acb590876dae97bb07e5eef7524a2d9b7189dc44 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 09:57:24 +0800 Subject: [PATCH 10/16] Update __init__.py to include AccessPermission in __all__ exports --- src/story_protocol_python_sdk/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 8520c85..1db8b18 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -7,6 +7,7 @@ from .resources.Royalty import Royalty from .resources.WIP import WIP from .story_client import StoryClient +from .types.common import AccessPermission __all__ = [ "StoryClient", @@ -16,4 +17,5 @@ "IPAccount", "Dispute", "WIP", + "AccessPermission", ] From dfc12ed575a35df1970c01e7b9323fe7b8284270 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 11:01:06 +0800 Subject: [PATCH 11/16] Refactor permission handling to use AccessPermission enum for clarity and type safety --- .../resources/IPAsset.py | 6 +- .../resources/Permission.py | 23 ++++--- .../test_integration_permission.py | 63 +++++++++---------- tests/unit/resources/test_permission.py | 31 ++++++--- 4 files changed, 68 insertions(+), 55 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 3cad983..0806b74 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -694,21 +694,21 @@ def register_ip_and_attach_pil_terms( "ipId": ip_id, "signer": self.license_attachment_workflows_client.contract.address, "to": self.core_metadata_module_client.contract.address, - "permission": 1, # ALLOW + "permission": AccessPermission.ALLOW, "func": "setAll(address,string,bytes32,bytes32)", }, { "ipId": ip_id, "signer": self.license_attachment_workflows_client.contract.address, "to": self.licensing_module_client.contract.address, - "permission": 1, # ALLOW + "permission": AccessPermission.ALLOW, "func": "attachLicenseTerms(address,address,uint256)", }, { "ipId": ip_id, "signer": self.license_attachment_workflows_client.contract.address, "to": self.licensing_module_client.contract.address, - "permission": 1, # ALLOW + "permission": AccessPermission.ALLOW, "func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))", }, ], diff --git a/src/story_protocol_python_sdk/resources/Permission.py b/src/story_protocol_python_sdk/resources/Permission.py index 845cdc9..10a0d26 100644 --- a/src/story_protocol_python_sdk/resources/Permission.py +++ b/src/story_protocol_python_sdk/resources/Permission.py @@ -13,6 +13,7 @@ IPAssetRegistryClient, ) from story_protocol_python_sdk.resources.IPAccount import IPAccount +from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.utils.constants import DEFAULT_FUNCTION_SELECTOR from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.validation import validate_address @@ -42,7 +43,7 @@ def set_permission( ip_id: str, signer: str, to: str, - permission: int, + permission: AccessPermission, func: str = DEFAULT_FUNCTION_SELECTOR, tx_options: dict | None = None, ) -> dict: @@ -56,7 +57,7 @@ def set_permission( :param ip_id str: The IP ID of the IP account that grants the permission for `signer`. :param signer str: The address that can call `to` on behalf of the `ip_id`. :param to str: The address that can be called by the `signer`. - :param permission int: The new permission level. + :param permission `AccessPermission`: The new permission level. :param func str: [Optional] The function selector string of `to` that can be called by the `signer` on behalf of the `ipAccount`. :param tx_options dict: [Optional] The transaction options. :return dict: A dictionary with the transaction hash and success status if waiting for transaction. @@ -74,7 +75,7 @@ def set_permission( 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, + permission.value, ], ) @@ -92,14 +93,18 @@ def set_permission( 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 = None + self, + ip_id: str, + signer: str, + permission: AccessPermission, + tx_options: dict | None = None, ) -> dict: """ Sets permission to a signer for all functions across all modules. :param ip_id str: The IP ID of the IP account that grants the permission. :param signer str: The address that will receive the permissions. - :param permission int: The new permission level. + :param permission `AccessPermission`: The new permission level. :param tx_options dict: [Optional] The transaction options. :return dict: A dictionary with the transaction hash and success status if waiting for transaction. """ @@ -113,7 +118,7 @@ def set_all_permissions( args=[ self.web3.to_checksum_address(ip_id), self.web3.to_checksum_address(signer), - permission, + permission.value, ], ) @@ -137,7 +142,7 @@ def create_set_permission_signature( ip_id: str, signer: str, to: str, - permission: int, + permission: AccessPermission, func: str = DEFAULT_FUNCTION_SELECTOR, deadline: int | None = None, tx_options: dict | None = None, @@ -148,7 +153,7 @@ def create_set_permission_signature( :param ip_id str: The IP ID of the IP account that grants the permission. :param signer str: The address that can call `to` on behalf of the `ip_id`. :param to str: The address that can be called by the `signer`. - :param permission int: The new permission level. + :param permission `AccessPermission`: The new permission level. :param func str: [Optional] The function selector string. :param deadline int: [Optional] The deadline for the signature validity. :param tx_options dict: [Optional] The transaction options. @@ -174,7 +179,7 @@ def create_set_permission_signature( signer, to, Web3.keccak(text=func)[:4] if func else b"\x00\x00\x00\x00", - permission, + permission.value, ], ) diff --git a/tests/integration/test_integration_permission.py b/tests/integration/test_integration_permission.py index ae0e06e..389bbfd 100644 --- a/tests/integration/test_integration_permission.py +++ b/tests/integration/test_integration_permission.py @@ -3,6 +3,7 @@ import pytest from story_protocol_python_sdk.story_client import StoryClient +from story_protocol_python_sdk.types.common import AccessPermission from .setup_for_integration import ( CORE_METADATA_MODULE, @@ -30,7 +31,7 @@ def test_set_permission(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=1, # ALLOW + permission=AccessPermission.ALLOW, func="function setAll(address,string,bytes32,bytes32)", ) @@ -42,7 +43,7 @@ def test_set_permission(self, story_client: StoryClient, ip_id): def test_set_all_permissions(self, story_client: StoryClient, 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=AccessPermission.ALLOW ) assert response is not None @@ -59,7 +60,7 @@ def test_create_set_permission_signature(self, story_client: StoryClient, ip_id) signer=account.address, to=CORE_METADATA_MODULE, func="setAll(address,string,bytes32,bytes32)", - permission=1, # ALLOW + permission=AccessPermission.ALLOW, deadline=deadline, ) @@ -77,7 +78,7 @@ def test_set_permission_invalid_ip(self, story_client: StoryClient): ip_id=unregistered_ip, signer=account.address, to=CORE_METADATA_MODULE, - permission=1, + permission=AccessPermission.ALLOW, ) assert f"IP id with {unregistered_ip} is not registered" in str(exc_info.value) @@ -91,7 +92,7 @@ def test_set_permission_invalid_addresses(self, story_client: StoryClient, ip_id ip_id=ip_id, signer=invalid_signer, to=CORE_METADATA_MODULE, - permission=1, # ALLOW + permission=AccessPermission.ALLOW, ) assert "invalid address" in str(exc_info.value).lower() @@ -103,7 +104,7 @@ def test_set_permission_invalid_addresses(self, story_client: StoryClient, ip_id ip_id=ip_id, signer=account.address, to=invalid_to, - permission=1, # ALLOW + permission=AccessPermission.ALLOW, ) assert "invalid address" in str(exc_info.value).lower() @@ -114,7 +115,7 @@ def test_set_permission_invalid_addresses(self, story_client: StoryClient, ip_id ip_id=ip_id, signer=lowercase_address, to=CORE_METADATA_MODULE, - permission=1, + permission=AccessPermission.ALLOW, ) assert "tx_hash" in response except Exception as e: @@ -124,15 +125,11 @@ def test_set_permission_invalid_addresses(self, story_client: StoryClient, ip_id def test_different_permission_levels(self, story_client: StoryClient, 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, + permission=AccessPermission.DENY, func="function setAll(address,string,bytes32,bytes32)", ) @@ -145,7 +142,7 @@ def test_different_permission_levels(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, func="function setAll(address,string,bytes32,bytes32)", ) @@ -156,7 +153,7 @@ def test_different_permission_levels(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ABSTAIN, + permission=AccessPermission.DENY, func="function setAll(address,string,bytes32,bytes32)", ) @@ -164,14 +161,14 @@ def test_different_permission_levels(self, story_client: StoryClient, ip_id): 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=AccessPermission.ABSTAIN ) assert response is not None 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=AccessPermission.DENY ) assert response is not None @@ -179,13 +176,11 @@ def test_different_permission_levels(self, story_client: StoryClient, ip_id): def test_different_function_selectors(self, story_client: StoryClient, ip_id): """Test setting permissions with different function selectors.""" - ALLOW = 1 - response = story_client.Permission.set_permission( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=1, + permission=AccessPermission.ALLOW, # No func parameter provided - should use default ) @@ -198,7 +193,7 @@ def test_different_function_selectors(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, func="setAll(address,string,bytes32,bytes32)", ) @@ -209,7 +204,7 @@ def test_different_function_selectors(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, func="setName(address,string)", ) @@ -220,7 +215,7 @@ def test_different_function_selectors(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, func="setDescription(address,string)", ) @@ -232,7 +227,7 @@ def test_different_function_selectors(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, # No func parameter provided deadline=deadline, ) @@ -244,12 +239,8 @@ def test_permission_hierarchies_and_overrides( self, story_client: StoryClient, 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=AccessPermission.ABSTAIN ) assert response is not None @@ -260,7 +251,7 @@ def test_permission_hierarchies_and_overrides( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, func=specific_func, ) @@ -270,7 +261,9 @@ def test_permission_hierarchies_and_overrides( 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=AccessPermission.ALLOW, ) assert response is not None @@ -280,7 +273,7 @@ def test_permission_hierarchies_and_overrides( ip_id=ip_id, signer=alternate_signer.address, to=CORE_METADATA_MODULE, - permission=DISALLOW, + permission=AccessPermission.ABSTAIN, func=specific_func, ) @@ -293,7 +286,7 @@ def test_permission_hierarchies_and_overrides( ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=ALLOW, + permission=AccessPermission.ALLOW, func="setDescription(address,string)", deadline=deadline, ) @@ -302,14 +295,16 @@ def test_permission_hierarchies_and_overrides( 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=AccessPermission.DENY ) assert response is not None 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=AccessPermission.DENY, ) assert response is not None diff --git a/tests/unit/resources/test_permission.py b/tests/unit/resources/test_permission.py index 87b74ba..2b7250e 100644 --- a/tests/unit/resources/test_permission.py +++ b/tests/unit/resources/test_permission.py @@ -3,6 +3,7 @@ import pytest from story_protocol_python_sdk.resources.Permission import Permission +from story_protocol_python_sdk.types.common import AccessPermission from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, STATE, TX_HASH @@ -20,18 +21,24 @@ def test_unregistered_ip_account(self, permission: Permission): Exception, match="IP id with 0x1234567890123456789012345678901234567890 is not registered.", ): - permission.set_permission(ADDRESS, ADDRESS, ADDRESS, 1) + permission.set_permission( + ADDRESS, ADDRESS, ADDRESS, AccessPermission.ALLOW + ) def test_invalid_signer_address(self, permission: Permission): with patch.object( permission.ip_asset_registry_client, "isRegistered", return_value=True ): with pytest.raises(Exception, match="Invalid address: 0xInvalidAddress."): - permission.set_permission(ADDRESS, "0xInvalidAddress", ADDRESS, 1) + permission.set_permission( + ADDRESS, "0xInvalidAddress", ADDRESS, AccessPermission.ALLOW + ) def test_invalid_to_address(self, permission: Permission): with pytest.raises(Exception, match="Invalid address: 0xInvalidAddress."): - permission.set_permission(ADDRESS, ADDRESS, "0xInvalidAddress", 1) + permission.set_permission( + ADDRESS, ADDRESS, "0xInvalidAddress", AccessPermission.ALLOW + ) def test_successful_transaction(self, permission: Permission): with patch.object( @@ -39,7 +46,9 @@ def test_successful_transaction(self, permission: Permission): ), patch.object( permission.ip_account, "execute", return_value={"tx_hash": TX_HASH} ): - response = permission.set_permission(ADDRESS, ADDRESS, ADDRESS, 1) + response = permission.set_permission( + ADDRESS, ADDRESS, ADDRESS, AccessPermission.ALLOW + ) assert response["tx_hash"] == TX_HASH def test_transaction_request_fails(self, permission: Permission): @@ -51,7 +60,9 @@ def test_transaction_request_fails(self, permission: Permission): side_effect=Exception("Transaction failed"), ): with pytest.raises(Exception, match="Transaction failed"): - permission.set_permission(ADDRESS, ADDRESS, ADDRESS, 1) + permission.set_permission( + ADDRESS, ADDRESS, ADDRESS, AccessPermission.ALLOW + ) class TestSetAllPermissions: @@ -61,7 +72,9 @@ def test_successful_transaction(self, permission: Permission): ), patch.object( permission.ip_account, "execute", return_value={"tx_hash": TX_HASH} ): - response = permission.set_all_permissions(ADDRESS, ADDRESS, 1) + response = permission.set_all_permissions( + ADDRESS, ADDRESS, AccessPermission.ALLOW + ) assert response["tx_hash"] == TX_HASH def test_transaction_request_fails(self, permission: Permission): @@ -73,7 +86,7 @@ def test_transaction_request_fails(self, permission: Permission): side_effect=Exception("Transaction failed"), ): with pytest.raises(Exception, match="Transaction failed"): - permission.set_all_permissions(ADDRESS, ADDRESS, 1) + permission.set_all_permissions(ADDRESS, ADDRESS, AccessPermission.ALLOW) class TestCreateSetPermissionSignature: @@ -81,7 +94,7 @@ class TestCreateSetPermissionSignature: def test_invalid_deadline(self, permission: Permission): with pytest.raises(Exception, match="Invalid deadline value."): permission.create_set_permission_signature( - ADDRESS, ADDRESS, ADDRESS, 1, deadline=-1 + ADDRESS, ADDRESS, ADDRESS, AccessPermission.ALLOW, deadline=-1 ) def test_successful_signature(self, permission: Permission): @@ -103,6 +116,6 @@ def test_successful_signature(self, permission: Permission): ), ): response = permission.create_set_permission_signature( - ADDRESS, ADDRESS, ADDRESS, 1 + ADDRESS, ADDRESS, ADDRESS, AccessPermission.ALLOW ) assert response["tx_hash"] == TX_HASH From fd55c5049b2bfd2e3c187a3bf2d8009a29b1d0d2 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 11:28:36 +0800 Subject: [PATCH 12/16] Enhance derivative data handling by introducing DerivativeDataInput class and updating IPAsset methods to utilize it for improved type safety and clarity --- .../resources/IPAsset.py | 19 ++-- .../utils/constants.py | 3 +- .../utils/derivative_data.py | 57 ++++++++++- .../integration/test_integration_ip_asset.py | 9 +- tests/unit/utils/test_derivative_data.py | 99 +++++++++++-------- 5 files changed, 128 insertions(+), 59 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 0806b74..bc07219 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -35,7 +35,10 @@ from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient from story_protocol_python_sdk.types.common import AccessPermission from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH -from story_protocol_python_sdk.utils.derivative_data import DerivativeData +from story_protocol_python_sdk.utils.derivative_data import ( + DerivativeData, + DerivativeDataInput, +) from story_protocol_python_sdk.utils.function_signature import get_function_signature from story_protocol_python_sdk.utils.ip_metadata import get_ip_metadata from story_protocol_python_sdk.utils.license_terms import LicenseTerms @@ -772,7 +775,7 @@ def register_derivative_ip( self, nft_contract: str, token_id: int, - deriv_data: dict, + deriv_data: DerivativeDataInput, metadata: dict | None = None, deadline: int | None = None, tx_options: dict | None = None, @@ -783,13 +786,7 @@ def register_derivative_ip( :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. - :param parent_ip_ids list[str]: The parent IP IDs (Address[]). - :param license_terms_ids list[int]: The IDs of the license terms that the parent IP supports (bigint[] | string[] | number[]). - :param max_minting_fee int: [Optional] The maximum minting fee that the caller is willing to pay. If set to 0 then no limit. Defaults to 0. - :param max_rts int: [Optional] The maximum number of royalty tokens that can be distributed to the external royalty policies (max: 100,000,000). Defaults to 100,000,000. - :param max_revenue_share int: [Optional] The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100 (where 100% represents 100,000,000). Defaults to 100. - :param license_template str: [Optional] The address of the license template. Defaults to the License Template address if not provided. See https://docs.story.foundation/docs/programmable-ip-license for more information. + :param deriv_data DerivativeDataInput: The derivative data for registerDerivative. :param metadata dict: [Optional] Desired IP metadata. :param ip_metadata_uri str: [Optional] The URI of the metadata for the IP. Defaults to "". :param ip_metadata_hash str: [Optional] The hash of the metadata for the IP. Defaults to zero hash. @@ -805,8 +802,8 @@ def register_derivative_ip( raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) - validated_deriv_data = DerivativeData( - web3=self.web3, **deriv_data + validated_deriv_data = DerivativeData.from_input( + web3=self.web3, input_data=deriv_data ).get_validated_data() calculated_deadline = self.sign_util.get_deadline(deadline=deadline) sig_register_signature = self.sign_util.get_permission_signature( diff --git a/src/story_protocol_python_sdk/utils/constants.py b/src/story_protocol_python_sdk/utils/constants.py index d08ce4c..b30f276 100644 --- a/src/story_protocol_python_sdk/utils/constants.py +++ b/src/story_protocol_python_sdk/utils/constants.py @@ -4,7 +4,8 @@ ZERO_HASH: HexStr = HexStr( "0x0000000000000000000000000000000000000000000000000000000000000000" ) -ROYALTY_POLICY = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" ZERO_FUNC = "0x00000000" DEFAULT_FUNCTION_SELECTOR = "0x00000000" MAX_ROYALTY_TOKEN = 100000000 +ROYALTY_POLICY_LAP_ADDRESS = "0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" +ROYALTY_POLICY_LRP_ADDRESS = "0x9156E603C949481883B1D3355C6F1132D191FC41" diff --git a/src/story_protocol_python_sdk/utils/derivative_data.py b/src/story_protocol_python_sdk/utils/derivative_data.py index 15a539a..7ed643f 100644 --- a/src/story_protocol_python_sdk/utils/derivative_data.py +++ b/src/story_protocol_python_sdk/utils/derivative_data.py @@ -1,6 +1,7 @@ from dataclasses import dataclass, field from typing import List, Optional +from ens.ens import Address from web3 import Web3 from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import ( @@ -17,6 +18,30 @@ from story_protocol_python_sdk.utils.validation import get_revenue_share +@dataclass +class DerivativeDataInput: + """ + Input data structure for creating derivative IP assets. + + This type defines the data that users need to provide when creating derivative works. + + Attributes: + parent_ip_ids: List of parent IP asset addresses that this derivative is based on. + license_terms_ids: List of license terms IDs corresponding to each parent IP. + max_minting_fee: [Optional] The maximum minting fee that the caller is willing to pay. if set to 0 then no limit. (default: 0). + max_rts: [Optional] The maximum number of royalty tokens that can be distributed to the external royalty policies. (max: 100,000,000) (default: 100,000,000). + max_revenue_share: [Optional] The maximum revenue share percentage allowed for minting the License Tokens. Must be between 0 and 100 (where 100% represents 100_000_000) (default: 100). + license_template: [Optional] The address of the license template. Defaults to [License Template](https://docs.story.foundation/docs/programmable-ip-license) address if not provided + """ + + parent_ip_ids: List[Address] + license_terms_ids: List[int] + max_minting_fee: int | float = field(default=0) + max_rts: int | float = field(default=MAX_ROYALTY_TOKEN) + max_revenue_share: int = field(default=100) + license_template: Optional[Address] = field(default=None) + + @dataclass class DerivativeData: """Validated derivative data for IP creation.""" @@ -24,15 +49,39 @@ class DerivativeData: web3: Web3 parent_ip_ids: List[str] license_terms_ids: List[int] - max_minting_fee: int | float = field(default=0) - max_rts: int | float = field(default=MAX_ROYALTY_TOKEN) - max_revenue_share: int = field(default=100) - license_template: Optional[str] = field(default=None) + max_minting_fee: int | float + max_rts: int | float + max_revenue_share: int + license_template: Optional[str] pi_license_template_client: PILicenseTemplateClient = field(init=False) ip_asset_registry_client: IPAssetRegistryClient = field(init=False) license_registry_client: LicenseRegistryClient = field(init=False) + @classmethod + def from_input( + cls, web3: Web3, input_data: DerivativeDataInput + ) -> "DerivativeData": + """ + Create a DerivativeData instance from DerivativeDataInput. + + Args: + web3: Web3 instance for blockchain interaction + input_data: User-provided derivative data + + Returns: + DerivativeData instance with validated data + """ + return cls( + web3=web3, + parent_ip_ids=input_data.parent_ip_ids, + license_terms_ids=input_data.license_terms_ids, + max_minting_fee=input_data.max_minting_fee, + max_rts=input_data.max_rts, + max_revenue_share=input_data.max_revenue_share, + license_template=input_data.license_template, + ) + def __post_init__(self): """Initialize clients and validate data after object creation.""" diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 86dc4b0..0870191 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -3,6 +3,7 @@ import pytest from story_protocol_python_sdk.story_client import StoryClient +from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput from .setup_for_integration import ( PIL_LICENSE_TEMPLATE, @@ -377,16 +378,16 @@ def test_register_derivative_ip( result = story_client.IPAsset.register_derivative_ip( nft_contract=nft_collection, token_id=token_child_id, - deriv_data={ - "parent_ip_ids": [ + deriv_data=DerivativeDataInput( + parent_ip_ids=[ parent_ip_and_license_terms["parent_ip_id"], second_ip_id_response["ip_id"], ], - "license_terms_ids": [ + license_terms_ids=[ parent_ip_and_license_terms["license_terms_id"], second_ip_id_response["license_terms_ids"][0], ], - }, + ), deadline=1000, ) assert isinstance(result["tx_hash"], str) and result["tx_hash"] diff --git a/tests/unit/utils/test_derivative_data.py b/tests/unit/utils/test_derivative_data.py index 5c60cef..e90b469 100644 --- a/tests/unit/utils/test_derivative_data.py +++ b/tests/unit/utils/test_derivative_data.py @@ -13,7 +13,10 @@ PILicenseTemplateClient, ) from story_protocol_python_sdk.utils.constants import MAX_ROYALTY_TOKEN -from story_protocol_python_sdk.utils.derivative_data import DerivativeData +from story_protocol_python_sdk.utils.derivative_data import ( + DerivativeData, + DerivativeDataInput, +) from tests.unit.fixtures.data import ADDRESS, IP_ID @@ -207,13 +210,15 @@ def test_validate_royalty_percent_is_less_than_max_revenue_share( mock_license_registry_client, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): - derivative_data = DerivativeData( + derivative_data = DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], - max_minting_fee=10, - max_rts=10, - license_template="0x1234567890123456789012345678901234567890", + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + license_template="0x1234567890123456789012345678901234567890", + ), ) assert derivative_data.max_revenue_share == MAX_ROYALTY_TOKEN @@ -254,11 +259,13 @@ def test_validate_max_rts_is_less_than_0( ValueError, match="The maxRts must be greater than 0 and less than 100000000.", ): - DerivativeData( + DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], - max_rts=-1, + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_rts=-1, + ), ) def test_validate_max_rts_is_greater_than_100_000_000( @@ -269,11 +276,13 @@ def test_validate_max_rts_is_greater_than_100_000_000( ValueError, match="The maxRts must be greater than 0 and less than 100000000.", ): - DerivativeData( + DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], - max_rts=1000000000001, + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_rts=1000000000001, + ), ) def test_validate_max_rts_default_value_is_max_rts( @@ -284,10 +293,12 @@ def test_validate_max_rts_default_value_is_max_rts( mock_license_registry_client, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): - derivative_data = DerivativeData( + derivative_data = DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ), ) assert derivative_data.max_rts == MAX_ROYALTY_TOKEN @@ -300,13 +311,15 @@ def test_validate_max_revenue_share_is_less_than_0( with raises( ValueError, match="The maxRevenueShare must be between 0 and 100." ): - DerivativeData( + DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], - max_minting_fee=10, - max_rts=10, - max_revenue_share=-1, + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=-1, + ), ) def test_validate_max_revenue_share_is_greater_than_100( @@ -320,13 +333,15 @@ def test_validate_max_revenue_share_is_greater_than_100( with raises( ValueError, match="The maxRevenueShare must be between 0 and 100." ): - DerivativeData( + DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], - max_minting_fee=10, - max_rts=10, - max_revenue_share=101, + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + max_minting_fee=10, + max_rts=10, + max_revenue_share=101, + ), ) def test_validate_max_revenue_share_default_value_is_100( @@ -337,10 +352,12 @@ def test_validate_max_revenue_share_default_value_is_100( mock_license_registry_client, ): with mock_is_checksum_address(), mock_ip_asset_registry_client(), mock_license_registry_client(): - derivative_data = DerivativeData( + derivative_data = DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ), ) assert derivative_data.max_revenue_share == MAX_ROYALTY_TOKEN @@ -355,10 +372,12 @@ def test_validate_license_template_default_value_is_pi_license_template( mock_license_registry_client, ): with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): - derivative_data = DerivativeData( + derivative_data = DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ), ) assert derivative_data.license_template == ADDRESS @@ -373,10 +392,12 @@ def test_get_validated_data_with_default_values( mock_license_registry_client, ): with mock_is_checksum_address(), mock_pi_license_template_client(), mock_ip_asset_registry_client(), mock_license_registry_client(): - derivative_data = DerivativeData( + derivative_data = DerivativeData.from_input( web3=mock_web3, - parent_ip_ids=[IP_ID], - license_terms_ids=[2], + input_data=DerivativeDataInput( + parent_ip_ids=[IP_ID], + license_terms_ids=[2], + ), ) assert derivative_data.get_validated_data() == { "parentIpIds": [IP_ID], From 1b2f42b14ce14d60985f6c7c58d3a6fdd6fd09e5 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 11:28:49 +0800 Subject: [PATCH 13/16] Update __init__.py to include new constants and DerivativeDataInput in __all__ exports for improved module structure --- src/story_protocol_python_sdk/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index 1db8b18..d097328 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -8,6 +8,16 @@ from .resources.WIP import WIP from .story_client import StoryClient from .types.common import AccessPermission +from .utils.constants import ( + DEFAULT_FUNCTION_SELECTOR, + MAX_ROYALTY_TOKEN, + ROYALTY_POLICY_LAP_ADDRESS, + ROYALTY_POLICY_LRP_ADDRESS, + ZERO_ADDRESS, + ZERO_FUNC, + ZERO_HASH, +) +from .utils.derivative_data import DerivativeDataInput __all__ = [ "StoryClient", @@ -18,4 +28,13 @@ "Dispute", "WIP", "AccessPermission", + "DerivativeDataInput", + # Constants + "ZERO_ADDRESS", + "ZERO_HASH", + "ROYALTY_POLICY_LAP_ADDRESS", + "ROYALTY_POLICY_LRP_ADDRESS", + "ZERO_FUNC", + "DEFAULT_FUNCTION_SELECTOR", + "MAX_ROYALTY_TOKEN", ] From a6e5ce572ed690da26d70780179c014bc0765a43 Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 14:21:40 +0800 Subject: [PATCH 14/16] Refactor LicenseTerms to use ROYALTY_POLICY_LAP_ADDRESS and update permission handling to use AccessPermission.ABSTAIN; enhance test cases to utilize DerivativeDataInput for improved clarity and type safety --- .../utils/license_terms.py | 7 ++++-- .../test_integration_permission.py | 2 +- tests/unit/resources/test_ip_asset.py | 23 ++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/story_protocol_python_sdk/utils/license_terms.py b/src/story_protocol_python_sdk/utils/license_terms.py index 05cd049..f660f1a 100644 --- a/src/story_protocol_python_sdk/utils/license_terms.py +++ b/src/story_protocol_python_sdk/utils/license_terms.py @@ -6,7 +6,10 @@ from story_protocol_python_sdk.abi.RoyaltyModule.RoyaltyModule_client import ( RoyaltyModuleClient, ) -from story_protocol_python_sdk.utils.constants import ROYALTY_POLICY, ZERO_ADDRESS +from story_protocol_python_sdk.utils.constants import ( + ROYALTY_POLICY_LAP_ADDRESS, + ZERO_ADDRESS, +) class LicenseTerms: @@ -51,7 +54,7 @@ def get_license_term_by_type(self, type, term=None): ) if term["royaltyPolicyAddress"] is None: - term["royaltyPolicyAddress"] = ROYALTY_POLICY + term["royaltyPolicyAddress"] = ROYALTY_POLICY_LAP_ADDRESS license_terms.update( { diff --git a/tests/integration/test_integration_permission.py b/tests/integration/test_integration_permission.py index 389bbfd..2f8c729 100644 --- a/tests/integration/test_integration_permission.py +++ b/tests/integration/test_integration_permission.py @@ -129,7 +129,7 @@ def test_different_permission_levels(self, story_client: StoryClient, ip_id): ip_id=ip_id, signer=account.address, to=CORE_METADATA_MODULE, - permission=AccessPermission.DENY, + permission=AccessPermission.ABSTAIN, func="function setAll(address,string,bytes32,bytes32)", ) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index 2edade6..45c7e02 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -4,6 +4,7 @@ from story_protocol_python_sdk.resources.IPAsset import IPAsset from story_protocol_python_sdk.utils.constants import ZERO_HASH +from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput from tests.unit.fixtures.data import ADDRESS, CHAIN_ID, IP_ID, TX_HASH @@ -140,10 +141,10 @@ def test_parent_ip_id_is_empty(self, ip_asset, mock_get_ip_id, mock_is_registere ip_asset.register_derivative_ip( nft_contract=ADDRESS, token_id=3, - deriv_data={ - "parent_ip_ids": [], - "license_terms_ids": [], - }, + deriv_data=DerivativeDataInput( + parent_ip_ids=[], + license_terms_ids=[], + ), ) def test_success( @@ -161,13 +162,13 @@ def test_success( result = ip_asset.register_derivative_ip( nft_contract=ADDRESS, token_id=3, - deriv_data={ - "max_minting_fee": 10, - "max_rts": 100, - "max_revenue_share": 100, - "parent_ip_ids": [IP_ID, IP_ID], - "license_terms_ids": [1, 2], - }, + deriv_data=DerivativeDataInput( + parent_ip_ids=[IP_ID, IP_ID], + license_terms_ids=[1, 2], + max_minting_fee=10, + max_rts=100, + max_revenue_share=100, + ), ) assert result["tx_hash"] == TX_HASH.hex() assert result["ip_id"] == IP_ID From 55b6b27899bb1faa1ac7d7d74ae7e749aa8eec8b Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 14:55:01 +0800 Subject: [PATCH 15/16] Add IPMetadataInput and IPMetadata classes for structured IP metadata handling --- src/story_protocol_python_sdk/__init__.py | 2 + .../resources/IPAsset.py | 12 +-- .../utils/ip_metadata.py | 89 ++++++++++++++++--- .../integration/test_integration_ip_asset.py | 7 ++ tests/unit/utils/test_ip_metadata.py | 41 +++++++++ 5 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 tests/unit/utils/test_ip_metadata.py diff --git a/src/story_protocol_python_sdk/__init__.py b/src/story_protocol_python_sdk/__init__.py index d097328..590f0e4 100644 --- a/src/story_protocol_python_sdk/__init__.py +++ b/src/story_protocol_python_sdk/__init__.py @@ -18,6 +18,7 @@ ZERO_HASH, ) from .utils.derivative_data import DerivativeDataInput +from .utils.ip_metadata import IPMetadataInput __all__ = [ "StoryClient", @@ -29,6 +30,7 @@ "WIP", "AccessPermission", "DerivativeDataInput", + "IPMetadataInput", # Constants "ZERO_ADDRESS", "ZERO_HASH", diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index bc07219..b1a1632 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -40,7 +40,7 @@ DerivativeDataInput, ) from story_protocol_python_sdk.utils.function_signature import get_function_signature -from story_protocol_python_sdk.utils.ip_metadata import get_ip_metadata +from story_protocol_python_sdk.utils.ip_metadata import IPMetadata, IPMetadataInput from story_protocol_python_sdk.utils.license_terms import LicenseTerms from story_protocol_python_sdk.utils.sign import Sign from story_protocol_python_sdk.utils.transaction_utils import build_and_send_transaction @@ -776,7 +776,7 @@ def register_derivative_ip( nft_contract: str, token_id: int, deriv_data: DerivativeDataInput, - metadata: dict | None = None, + metadata: IPMetadataInput | None = None, deadline: int | None = None, tx_options: dict | None = None, ) -> dict: @@ -787,11 +787,7 @@ def register_derivative_ip( :param nft_contract str: The address of the NFT collection. :param token_id int: The ID of the NFT. :param deriv_data DerivativeDataInput: The derivative data for registerDerivative. - :param metadata dict: [Optional] Desired IP metadata. - :param ip_metadata_uri str: [Optional] The URI of the metadata for the IP. Defaults to "". - :param ip_metadata_hash str: [Optional] The hash of the metadata for the IP. Defaults to zero hash. - :param nft_metadata_uri str: [Optional] The URI of the metadata for the NFT. Defaults to "". - :param nft_metadata_hash str: [Optional] The hash of the metadata for the NFT. Defaults to zero hash. + :param metadata IPMetadataInput: Desired IP metadata. :param deadline int: [Optional] Signature deadline in milliseconds. :param tx_options dict: [Optional] Transaction options. :return dict: Dictionary with the tx hash and IP ID. @@ -840,7 +836,7 @@ def register_derivative_ip( nft_contract, token_id, validated_deriv_data, - get_ip_metadata(metadata), + IPMetadata.from_input(metadata).get_validated_data(), { "signer": self.account.address, "deadline": calculated_deadline, diff --git a/src/story_protocol_python_sdk/utils/ip_metadata.py b/src/story_protocol_python_sdk/utils/ip_metadata.py index 0a056a1..4242d79 100644 --- a/src/story_protocol_python_sdk/utils/ip_metadata.py +++ b/src/story_protocol_python_sdk/utils/ip_metadata.py @@ -1,16 +1,79 @@ +from dataclasses import dataclass, field + +from ens.ens import HexStr + from story_protocol_python_sdk.utils.constants import ZERO_HASH -def get_ip_metadata( - metadata: dict | None = None, -) -> dict: - return { - "ipMetadataURI": metadata.get("ip_metadata_uri", "") if metadata else "", - "ipMetadataHash": ( - metadata.get("ip_metadata_hash", ZERO_HASH) if metadata else ZERO_HASH - ), - "nftMetadataURI": metadata.get("nft_metadata_uri", "") if metadata else "", - "nftMetadataHash": ( - metadata.get("nft_metadata_hash", ZERO_HASH) if metadata else ZERO_HASH - ), - } +@dataclass +class IPMetadataInput: + """ + Input data structure for IP metadata. + + This type defines the data that users need to provide when setting IP metadata. + + Attributes: + ip_metadata_uri: [Optional] URI for IP metadata (default: ""). + ip_metadata_hash: [Optional] Hash for IP metadata (default: ZERO_HASH). + nft_metadata_uri: [Optional] URI for NFT metadata (default: ""). + nft_metadata_hash: [Optional] Hash for NFT metadata (default: ZERO_HASH). + """ + + ip_metadata_uri: str = field(default="") + ip_metadata_hash: HexStr = field(default=ZERO_HASH) + nft_metadata_uri: str = field(default="") + nft_metadata_hash: HexStr = field(default=ZERO_HASH) + + +@dataclass +class IPMetadata: + """Validated IP metadata for IP asset operations.""" + + ip_metadata_uri: str + ip_metadata_hash: HexStr + nft_metadata_uri: str + nft_metadata_hash: HexStr + + @classmethod + def from_input(cls, input_data: IPMetadataInput | None = None) -> "IPMetadata": + """ + Create an IPMetadata instance from IPMetadataInput. + + Args: + input_data: User-provided IP metadata + + Returns: + IPMetadata instance with validated data + """ + if input_data is None: + return cls( + ip_metadata_uri="", + ip_metadata_hash=ZERO_HASH, + nft_metadata_uri="", + nft_metadata_hash=ZERO_HASH, + ) + + return cls( + ip_metadata_uri=input_data.ip_metadata_uri, + ip_metadata_hash=input_data.ip_metadata_hash, + nft_metadata_uri=input_data.nft_metadata_uri, + nft_metadata_hash=input_data.nft_metadata_hash, + ) + + def __post_init__(self): + """Validate data after object creation.""" + self.get_validated_data() + + def get_validated_data(self) -> dict: + """ + Get the metadata as a dictionary in the format expected by the blockchain. + + Returns: + Dictionary with validated metadata fields + """ + return { + "ipMetadataURI": self.ip_metadata_uri, + "ipMetadataHash": self.ip_metadata_hash, + "nftMetadataURI": self.nft_metadata_uri, + "nftMetadataHash": self.nft_metadata_hash, + } diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index 0870191..26dd29d 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -4,6 +4,7 @@ from story_protocol_python_sdk.story_client import StoryClient from story_protocol_python_sdk.utils.derivative_data import DerivativeDataInput +from story_protocol_python_sdk.utils.ip_metadata import IPMetadataInput from .setup_for_integration import ( PIL_LICENSE_TEMPLATE, @@ -388,6 +389,12 @@ def test_register_derivative_ip( second_ip_id_response["license_terms_ids"][0], ], ), + metadata=IPMetadataInput( + nft_metadata_uri="https://ipfs.io/ipfs/Qm...", + nft_metadata_hash=web3.to_hex( + web3.keccak(text="test-nft-metadata-hash") + ), + ), deadline=1000, ) assert isinstance(result["tx_hash"], str) and result["tx_hash"] diff --git a/tests/unit/utils/test_ip_metadata.py b/tests/unit/utils/test_ip_metadata.py new file mode 100644 index 0000000..8649843 --- /dev/null +++ b/tests/unit/utils/test_ip_metadata.py @@ -0,0 +1,41 @@ +from ens.ens import HexStr + +from story_protocol_python_sdk.utils.constants import ZERO_HASH +from story_protocol_python_sdk.utils.ip_metadata import IPMetadata, IPMetadataInput +from tests.unit.fixtures.data import TX_HASH + + +class TestIPMetadata: + def test_from_input_with_default_values(self): + ip_metadata = IPMetadata.from_input(IPMetadataInput(ip_metadata_hash=TX_HASH)) + assert ip_metadata.get_validated_data() == { + "ipMetadataURI": "", + "ipMetadataHash": TX_HASH, + "nftMetadataURI": "", + "nftMetadataHash": ZERO_HASH, + } + + def test_from_input_with_custom_values(self): + ip_metadata = IPMetadata.from_input( + IPMetadataInput( + ip_metadata_uri="https://ipfs.io/ipfs/Qm...", + ip_metadata_hash=HexStr("0x1234567890"), + nft_metadata_uri="https://ipfs.io/ipfs/Qm...", + nft_metadata_hash=HexStr("0x1234567890"), + ) + ) + assert ip_metadata.get_validated_data() == { + "ipMetadataURI": "https://ipfs.io/ipfs/Qm...", + "ipMetadataHash": HexStr("0x1234567890"), + "nftMetadataURI": "https://ipfs.io/ipfs/Qm...", + "nftMetadataHash": HexStr("0x1234567890"), + } + + def test_from_input_with_none(self): + ip_metadata = IPMetadata.from_input(None) + assert ip_metadata.get_validated_data() == { + "ipMetadataURI": "", + "ipMetadataHash": ZERO_HASH, + "nftMetadataURI": "", + "nftMetadataHash": ZERO_HASH, + } From 668f264469a1a5702800f1b4e4abbdc7aba2326f Mon Sep 17 00:00:00 2001 From: Bonnie Date: Mon, 11 Aug 2025 15:03:23 +0800 Subject: [PATCH 16/16] Update parameter documentation in IPAsset and refactor client usage in DerivativeData for improved clarity and consistency --- src/story_protocol_python_sdk/resources/IPAsset.py | 4 ++-- src/story_protocol_python_sdk/utils/derivative_data.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index b1a1632..2aea9ff 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -786,8 +786,8 @@ def register_derivative_ip( :param nft_contract str: The address of the NFT collection. :param token_id int: The ID of the NFT. - :param deriv_data DerivativeDataInput: The derivative data for registerDerivative. - :param metadata IPMetadataInput: Desired IP metadata. + :param deriv_data `DerivativeDataInput`: The derivative data for registerDerivative. + :param metadata `IPMetadataInput`: [Optional] Desired IP metadata. :param deadline int: [Optional] Signature deadline in milliseconds. :param tx_options dict: [Optional] Transaction options. :return dict: Dictionary with the tx hash and IP ID. diff --git a/src/story_protocol_python_sdk/utils/derivative_data.py b/src/story_protocol_python_sdk/utils/derivative_data.py index 7ed643f..47aeacc 100644 --- a/src/story_protocol_python_sdk/utils/derivative_data.py +++ b/src/story_protocol_python_sdk/utils/derivative_data.py @@ -111,23 +111,21 @@ def validate_parent_ip_ids_and_license_terms_ids(self): "The number of parent IP IDs must match the number of license terms IDs." ) - ip_asset_registry_client: IPAssetRegistryClient = self.ip_asset_registry_client - license_registry_client: LicenseRegistryClient = self.license_registry_client total_royalty_percent = 0 for parent_ip_id, license_terms_id in zip( self.parent_ip_ids, self.license_terms_ids ): if not Web3.is_checksum_address(parent_ip_id): raise ValueError("The parent IP ID must be a valid address.") - if not ip_asset_registry_client.isRegistered(parent_ip_id): + if not self.ip_asset_registry_client.isRegistered(parent_ip_id): raise ValueError(f"The parent IP ID {parent_ip_id} must be registered.") - if not license_registry_client.hasIpAttachedLicenseTerms( + if not self.license_registry_client.hasIpAttachedLicenseTerms( parent_ip_id, self.license_template, license_terms_id ): raise ValueError( f"License terms id {license_terms_id} must be attached to the parent ipId {parent_ip_id} before registering derivative." ) - royalty_percent = license_registry_client.getRoyaltyPercent( + royalty_percent = self.license_registry_client.getRoyaltyPercent( parent_ip_id, self.license_template, license_terms_id ) total_royalty_percent += royalty_percent