Skip to content

Commit 8f29ccd

Browse files
authored
feat: add register_pil_terms_and_attach method to IPAsset class (#132)
* feat: add register_pil_terms_and_attach method to IPAsset class * test: introduce integration and unit tests for the new register_pil_terms_and_attach method. * fix: update deposit amount in WIP integration test to a smaller value * fix: allow duplicates in mint and register IP integration test
1 parent 62fdf77 commit 8f29ccd

File tree

6 files changed

+327
-6
lines changed

6 files changed

+327
-6
lines changed

src/story_protocol_python_sdk/resources/IPAsset.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from story_protocol_python_sdk.abi.DerivativeWorkflows.DerivativeWorkflows_client import (
1313
DerivativeWorkflowsClient,
1414
)
15+
from story_protocol_python_sdk.abi.IPAccountImpl.IPAccountImpl_client import (
16+
IPAccountImplClient,
17+
)
1518
from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import (
1619
IPAssetRegistryClient,
1720
)
@@ -35,7 +38,10 @@
3538
)
3639
from story_protocol_python_sdk.abi.SPGNFTImpl.SPGNFTImpl_client import SPGNFTImplClient
3740
from story_protocol_python_sdk.types.common import AccessPermission
38-
from story_protocol_python_sdk.types.resource.IPAsset import RegistrationResponse
41+
from story_protocol_python_sdk.types.resource.IPAsset import (
42+
RegisterPILTermsAndAttachResponse,
43+
RegistrationResponse,
44+
)
3945
from story_protocol_python_sdk.utils.constants import (
4046
MAX_ROYALTY_TOKEN,
4147
ZERO_ADDRESS,
@@ -881,6 +887,90 @@ def mint_and_register_ip_and_make_derivative_with_license_tokens(
881887
except Exception as e:
882888
raise e
883889

890+
def register_pil_terms_and_attach(
891+
self,
892+
ip_id: Address,
893+
license_terms_data: list,
894+
deadline: int | None = None,
895+
tx_options: dict | None = None,
896+
) -> RegisterPILTermsAndAttachResponse:
897+
"""
898+
Register Programmable IP License Terms (if unregistered) and attach it to IP.
899+
900+
:param ip_id Address: The IP ID.
901+
:param license_terms_data list: The data of the license and its configuration to be attached to the IP.
902+
:param deadline int: [Optional] Signature deadline in milliseconds. If not provided, the current time + 1000ms will be used.
903+
:param tx_options dict: [Optional] Transaction options.
904+
:return RegisterPILTermsAndAttachResponse: Dictionary with the tx hash and license terms IDs.
905+
"""
906+
try:
907+
if not self._is_registered(ip_id):
908+
raise ValueError(f"The IP with id {ip_id} is not registered.")
909+
calculated_deadline = self.sign_util.get_deadline(deadline=deadline)
910+
ip_account_impl_client = IPAccountImplClient(self.web3, ip_id)
911+
state = ip_account_impl_client.state()
912+
license_terms = []
913+
for term in license_terms_data:
914+
license_terms.append(
915+
{
916+
"terms": self.license_terms_util.validate_license_terms(
917+
term["terms"]
918+
),
919+
"licensingConfig": self.license_terms_util.validate_licensing_config(
920+
term["licensing_config"]
921+
),
922+
}
923+
)
924+
signature_response = self.sign_util.get_permission_signature(
925+
ip_id=ip_id,
926+
deadline=calculated_deadline,
927+
state=state,
928+
permissions=[
929+
{
930+
"ipId": ip_id,
931+
"signer": self.license_attachment_workflows_client.contract.address,
932+
"to": self.licensing_module_client.contract.address,
933+
"permission": AccessPermission.ALLOW,
934+
"func": get_function_signature(
935+
self.licensing_module_client.contract.abi,
936+
"attachLicenseTerms",
937+
),
938+
},
939+
{
940+
"ipId": ip_id,
941+
"signer": self.license_attachment_workflows_client.contract.address,
942+
"to": self.licensing_module_client.contract.address,
943+
"permission": AccessPermission.ALLOW,
944+
"func": get_function_signature(
945+
self.licensing_module_client.contract.abi,
946+
"setLicensingConfig",
947+
),
948+
},
949+
],
950+
)
951+
response = build_and_send_transaction(
952+
self.web3,
953+
self.account,
954+
self.license_attachment_workflows_client.build_registerPILTermsAndAttach_transaction,
955+
ip_id,
956+
license_terms,
957+
{
958+
"signer": self.web3.to_checksum_address(self.account.address),
959+
"deadline": calculated_deadline,
960+
"signature": signature_response["signature"],
961+
},
962+
tx_options=tx_options,
963+
)
964+
license_terms_ids = self._parse_tx_license_terms_attached_event(
965+
response["tx_receipt"]
966+
)
967+
return RegisterPILTermsAndAttachResponse(
968+
tx_hash=response["tx_hash"],
969+
license_terms_ids=license_terms_ids,
970+
)
971+
except Exception as e:
972+
raise e
973+
884974
def _validate_derivative_data(self, derivative_data: dict) -> dict:
885975
"""
886976
Validates the derivative data and returns processed internal data.
@@ -1034,7 +1124,7 @@ def _parse_tx_license_term_attached_event(self, tx_receipt: dict) -> int | None:
10341124
return license_terms_id
10351125
return None
10361126

1037-
def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list | None:
1127+
def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list[int]:
10381128
"""
10391129
Parse the LicenseTermsAttached events from a transaction receipt.
10401130
@@ -1052,4 +1142,4 @@ def _parse_tx_license_terms_attached_event(self, tx_receipt: dict) -> list | Non
10521142
license_terms_id = int.from_bytes(data[-32:], byteorder="big")
10531143
license_terms_ids.append(license_terms_id)
10541144

1055-
return license_terms_ids if license_terms_ids else None
1145+
return license_terms_ids

src/story_protocol_python_sdk/types/resource/IPAsset.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,16 @@ class RegistrationResponse(TypedDict):
1616
ip_id: Address
1717
tx_hash: HexStr
1818
token_id: Optional[int]
19+
20+
21+
class RegisterPILTermsAndAttachResponse(TypedDict):
22+
"""
23+
Response structure for Programmable IP License Terms registration and attachment operations.
24+
25+
Attributes:
26+
tx_hash: The transaction hash of the registration transaction
27+
license_terms_ids: The IDs of the registered license terms
28+
"""
29+
30+
tx_hash: HexStr
31+
license_terms_ids: list[int]

tests/integration/test_integration_ip_asset.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,9 +789,88 @@ def test_with_custom_value(
789789
nft_metadata_hash=web3.keccak(text="custom-value-metadata"),
790790
),
791791
recipient=account_2.address,
792-
allow_duplicates=False,
792+
allow_duplicates=True,
793793
)
794794
assert response is not None
795795
assert isinstance(response["tx_hash"], str)
796796
assert isinstance(response["ip_id"], str)
797797
assert isinstance(response["token_id"], int)
798+
799+
800+
class TestRegisterPilTermsAndAttach:
801+
def test_successful_registration(
802+
self,
803+
story_client: StoryClient,
804+
parent_ip_and_license_terms,
805+
):
806+
response = story_client.IPAsset.register_pil_terms_and_attach(
807+
ip_id=parent_ip_and_license_terms["parent_ip_id"],
808+
license_terms_data=[
809+
{
810+
"terms": {
811+
"transferable": True,
812+
"royalty_policy": ROYALTY_POLICY,
813+
"default_minting_fee": 1,
814+
"expiration": 0,
815+
"commercial_use": True,
816+
"commercial_attribution": False,
817+
"commercializer_checker": ZERO_ADDRESS,
818+
"commercializer_checker_data": ZERO_ADDRESS,
819+
"commercial_rev_share": 90,
820+
"commercial_rev_ceiling": 0,
821+
"derivatives_allowed": True,
822+
"derivatives_attribution": True,
823+
"derivatives_approval": False,
824+
"derivatives_reciprocal": True,
825+
"derivative_rev_ceiling": 0,
826+
"currency": MockERC20,
827+
"uri": "",
828+
},
829+
"licensing_config": {
830+
"is_set": True,
831+
"minting_fee": 1,
832+
"hook_data": "",
833+
"licensing_hook": ZERO_ADDRESS,
834+
"commercial_rev_share": 90,
835+
"disabled": False,
836+
"expect_minimum_group_reward_share": 0,
837+
"expect_group_reward_pool": ZERO_ADDRESS,
838+
},
839+
},
840+
{
841+
"terms": {
842+
"transferable": True,
843+
"royalty_policy": ROYALTY_POLICY,
844+
"default_minting_fee": 10,
845+
"expiration": 0,
846+
"commercial_use": True,
847+
"commercial_attribution": False,
848+
"commercializer_checker": ZERO_ADDRESS,
849+
"commercializer_checker_data": ZERO_ADDRESS,
850+
"commercial_rev_share": 10,
851+
"commercial_rev_ceiling": 0,
852+
"derivatives_allowed": True,
853+
"derivatives_attribution": True,
854+
"derivatives_approval": False,
855+
"derivatives_reciprocal": True,
856+
"derivative_rev_ceiling": 0,
857+
"currency": MockERC20,
858+
"uri": "",
859+
},
860+
"licensing_config": {
861+
"is_set": False,
862+
"minting_fee": 1,
863+
"hook_data": "",
864+
"licensing_hook": ZERO_ADDRESS,
865+
"commercial_rev_share": 90,
866+
"disabled": False,
867+
"expect_minimum_group_reward_share": 0,
868+
"expect_group_reward_pool": ZERO_ADDRESS,
869+
},
870+
},
871+
],
872+
deadline=10000,
873+
)
874+
assert response is not None
875+
assert isinstance(response["tx_hash"], str)
876+
assert len(response["license_terms_ids"]) == 2

tests/integration/test_integration_wip.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class TestWIPDeposit:
1717
def test_deposit(self, story_client: StoryClient):
1818
"""Test depositing IP to WIP"""
19-
ip_amt = web3.to_wei(1, "ether") # or Web3.to_wei("0.01", 'ether')
19+
ip_amt = web3.to_wei(0.000001, "ether")
2020

2121
# Get balances before deposit
2222
balance_before = story_client.get_wallet_balance()

tests/unit/fixtures/data.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"derivative_rev_ceiling": 100,
2222
"uri": "https://example.com",
2323
"transferable": True,
24-
"expect_minimum_group_reward_share": 10,
2524
}
2625

2726
LICENSING_CONFIG = {

0 commit comments

Comments
 (0)