Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/story_protocol_python_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
BatchMintAndRegisterIPInput,
BatchMintAndRegisterIPResponse,
LicenseTermsDataInput,
LinkDerivativeResponse,
MintedNFT,
MintNFT,
RegisterAndAttachAndDistributeRoyaltyTokensResponse,
Expand Down Expand Up @@ -77,6 +78,7 @@
"MintedNFT",
"RegisterIpAssetResponse",
"RegisterDerivativeIpAssetResponse",
"LinkDerivativeResponse",
# Constants
"ZERO_ADDRESS",
"ZERO_HASH",
Expand Down
70 changes: 70 additions & 0 deletions src/story_protocol_python_sdk/resources/IPAsset.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
BatchMintAndRegisterIPInput,
BatchMintAndRegisterIPResponse,
LicenseTermsDataInput,
LinkDerivativeResponse,
MintedNFT,
MintNFT,
RegisterAndAttachAndDistributeRoyaltyTokensResponse,
Expand Down Expand Up @@ -276,6 +277,7 @@ def register(
except Exception as e:
raise e

@deprecated("Use link_derivative() instead.")
def register_derivative(
self,
child_ip_id: str,
Expand Down Expand Up @@ -343,6 +345,7 @@ def register_derivative(
except Exception as e:
raise ValueError(f"Failed to register derivative: {str(e)}") from e

@deprecated("Use link_derivative() instead.")
def register_derivative_with_license_tokens(
self,
child_ip_id: str,
Expand Down Expand Up @@ -395,6 +398,73 @@ def register_derivative_with_license_tokens(
f"Failed to register derivative with license tokens: {str(e)}"
)

def link_derivative(
self,
child_ip_id: Address,
parent_ip_ids: list[Address] | None = None,
license_terms_ids: list[int] | None = None,
license_token_ids: list[int] | None = None,
max_minting_fee: int = 0,
max_rts: int = MAX_ROYALTY_TOKEN,
max_revenue_share: int = 100,
license_template: str | None = None,
tx_options: dict | None = None,
) -> LinkDerivativeResponse:
"""
Link a derivative IP asset using parent IP's license terms or license tokens.

Supports the following workflows:
- If `parent_ip_ids` is provided, calls `registerDerivative`(contract method)
- If `license_token_ids` is provided, calls `registerDerivativeWithLicenseTokens`(contract method)

:param child_ip_id Address: The derivative IP ID.
:param parent_ip_ids list[Address]: [Optional] The parent IP IDs. Required if using license terms.
:param license_terms_ids list[int]: [Optional] The IDs of the license terms that the parent IP supports. Required if `parent_ip_ids` is provided.
:param license_token_ids list[int]: [Optional] The IDs of the license tokens.
:param max_minting_fee int: [Optional] The maximum minting fee that the caller is willing to pay.
if set to 0 then no limit. (default: 0) Only used with `parent_ip_ids`.
:param max_rts int: [Optional] The maximum number of royalty tokens that can be distributed
(max: 100,000,000) (default: 100,000,000)
:param max_revenue_share int: [Optional] The maximum revenue share percentage allowed.
Must be between 0 and 100. (default: 100) Only used with `parent_ip_ids`.
:param license_template str: [Optional] The license template address.
Only used with `parent_ip_ids`.
:param tx_options dict: [Optional] Transaction options.
:return `LinkDerivativeResponse`: A dictionary with the transaction hash.
"""
try:
if parent_ip_ids is not None:
if license_terms_ids is None:
raise ValueError(
"license_terms_ids is required when parent_ip_ids is provided."
)
response = self.register_derivative(
child_ip_id=child_ip_id,
parent_ip_ids=parent_ip_ids,
license_terms_ids=license_terms_ids,
max_minting_fee=max_minting_fee,
max_rts=max_rts,
max_revenue_share=max_revenue_share,
license_template=license_template,
tx_options=tx_options,
)
return LinkDerivativeResponse(tx_hash=response["tx_hash"])
elif license_token_ids is not None:
response = self.register_derivative_with_license_tokens(
child_ip_id=child_ip_id,
license_token_ids=license_token_ids,
max_rts=max_rts,
tx_options=tx_options,
)
return LinkDerivativeResponse(tx_hash=response["tx_hash"])
else:
raise ValueError(
"either parent_ip_ids or license_token_ids must be provided."
)

except Exception as e:
raise ValueError(f"Failed to link derivative: {str(e)}") from e

@deprecated("Use register_ip_asset() instead.")
def mint_and_register_ip_asset_with_pil_terms(
self,
Expand Down
11 changes: 11 additions & 0 deletions src/story_protocol_python_sdk/types/resource/IPAsset.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,14 @@ class RegisterDerivativeIpAssetResponse(TypedDict, total=False):
token_id: int
royalty_vault: Address
distribute_royalty_tokens_tx_hash: HexStr


class LinkDerivativeResponse(TypedDict):
"""
Response structure for linking a derivative IP asset.

Attributes:
tx_hash: The transaction hash of the link derivative transaction.
"""

tx_hash: HexStr
10 changes: 8 additions & 2 deletions tests/integration/config/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
import hmac
import os
from typing import TypedDict

import base58
from dotenv import load_dotenv
Expand Down Expand Up @@ -279,9 +280,14 @@ def setup_royalty_vault(story_client, parent_ip_id, account):
return response


class ParentIpAndLicenseTerms(TypedDict):
parent_ip_id: str
license_terms_id: int


def mint_and_approve_license_token(
story_client: StoryClient,
parent_ip_and_license_terms: dict,
parent_ip_and_license_terms: ParentIpAndLicenseTerms,
account: LocalAccount,
) -> list[int]:
"""
Expand Down Expand Up @@ -319,7 +325,7 @@ def mint_and_approve_license_token(

def create_parent_ip_and_license_terms(
story_client: StoryClient, nft_collection, account: LocalAccount
) -> dict[str, int]:
) -> ParentIpAndLicenseTerms:
"""Create a parent IP with license terms for testing."""
response = story_client.IPAsset.register_ip_asset(
nft=MintNFT(
Expand Down
75 changes: 75 additions & 0 deletions tests/integration/test_integration_ip_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1748,3 +1748,78 @@ def test_register_derivative_ip_asset_mint_with_license_token_ids(
assert isinstance(response["tx_hash"], str) and response["tx_hash"]
assert isinstance(response["ip_id"], str) and response["ip_id"]
assert isinstance(response["token_id"], int)


class TestLinkDerivative:
def test_link_derivative_with_license_terms(
self,
story_client: StoryClient,
nft_collection,
):
"""Link derivative using parent IP IDs and license terms IDs."""
# Create parent IP and license terms
parent_ip_and_license_terms = create_parent_ip_and_license_terms(
story_client, nft_collection, account
)
# Register child IP
child_response = story_client.IPAsset.register_ip_asset(
nft=MintNFT(
type="mint",
spg_nft_contract=nft_collection,
recipient=account.address,
allow_duplicates=True,
),
)
# Link derivative
response = story_client.IPAsset.link_derivative(
child_ip_id=child_response["ip_id"],
parent_ip_ids=[parent_ip_and_license_terms["parent_ip_id"]],
license_terms_ids=[parent_ip_and_license_terms["license_terms_id"]],
max_minting_fee=10_000,
max_rts=10_000_000,
max_revenue_share=50,
license_template=PIL_LICENSE_TEMPLATE,
)

assert response is not None
assert isinstance(response, dict)
assert "tx_hash" in response
assert isinstance(response["tx_hash"], str)
assert len(response["tx_hash"]) > 0

def test_link_derivative_with_license_tokens(
self,
story_client: StoryClient,
nft_collection,
):
"""Link derivative using license token IDs."""
# Create parent IP and license terms
parent_ip_and_license_terms = create_parent_ip_and_license_terms(
story_client, nft_collection, account
)
# Mint and approve license tokens
license_token_ids = mint_and_approve_license_token(
story_client,
parent_ip_and_license_terms,
account,
)
# Register child IP
child_response = story_client.IPAsset.register_ip_asset(
nft=MintNFT(
type="mint",
spg_nft_contract=nft_collection,
recipient=account.address,
allow_duplicates=True,
),
)
response = story_client.IPAsset.link_derivative(
child_ip_id=child_response["ip_id"],
license_token_ids=license_token_ids,
max_rts=80_000_000,
)

assert response is not None
assert isinstance(response, dict)
assert "tx_hash" in response
assert isinstance(response["tx_hash"], str)
assert len(response["tx_hash"]) > 0
Loading
Loading