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 @@ -15,6 +15,7 @@
)
from .types.resource.IPAsset import (
LicenseTermsDataInput,
RegisterAndAttachAndDistributeRoyaltyTokensResponse,
RegisterPILTermsAndAttachResponse,
RegistrationResponse,
RegistrationWithRoyaltyVaultAndLicenseTermsResponse,
Expand Down Expand Up @@ -50,6 +51,7 @@
"RegistrationResponse",
"RegistrationWithRoyaltyVaultResponse",
"RegistrationWithRoyaltyVaultAndLicenseTermsResponse",
"RegisterAndAttachAndDistributeRoyaltyTokensResponse",
"LicenseTermsDataInput",
"ClaimRewardsResponse",
"ClaimReward",
Expand Down
182 changes: 182 additions & 0 deletions src/story_protocol_python_sdk/resources/IPAsset.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
from story_protocol_python_sdk.abi.IPAssetRegistry.IPAssetRegistry_client import (
IPAssetRegistryClient,
)
from story_protocol_python_sdk.abi.IpRoyaltyVaultImpl.IpRoyaltyVaultImpl_client import (
IpRoyaltyVaultImplClient,
)
from story_protocol_python_sdk.abi.LicenseAttachmentWorkflows.LicenseAttachmentWorkflows_client import (
LicenseAttachmentWorkflowsClient,
)
Expand Down Expand Up @@ -48,6 +51,7 @@
from story_protocol_python_sdk.types.common import AccessPermission
from story_protocol_python_sdk.types.resource.IPAsset import (
LicenseTermsDataInput,
RegisterAndAttachAndDistributeRoyaltyTokensResponse,
RegisterPILTermsAndAttachResponse,
RegistrationResponse,
RegistrationWithRoyaltyVaultAndLicenseTermsResponse,
Expand Down Expand Up @@ -1095,6 +1099,120 @@ def mint_and_register_ip_and_make_derivative_and_distribute_royalty_tokens(
f"Failed to mint, register IP, make derivative and distribute royalty tokens: {str(e)}"
) from e

def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens(
self,
nft_contract: Address,
token_id: int,
license_terms_data: list[LicenseTermsDataInput],
royalty_shares: list[RoyaltyShareInput],
ip_metadata: IPMetadataInput | None = None,
deadline: int | None = None,
tx_options: dict | None = None,
) -> RegisterAndAttachAndDistributeRoyaltyTokensResponse:
"""
Register the given NFT and attach license terms and distribute royalty
tokens. In order to successfully distribute royalty tokens, the first license terms
attached to the IP must be a commercial license.

:param nft_contract Address: The address of the NFT collection.
:param token_id int: The ID of the NFT.
:param license_terms_data `list[LicenseTermsDataInput]`: The data of the license and its configuration to be attached to the new group IP.
:param royalty_shares `list[RoyaltyShareInput]`: Authors of the IP and their shares of the royalty tokens.
:param ip_metadata `IPMetadataInput`: [Optional] The metadata for the newly registered IP.
:param deadline int: [Optional] The deadline for the signature in seconds. (default: 1000 seconds)
:param tx_options dict: [Optional] Transaction options.
:return `RegisterAndAttachAndDistributeRoyaltyTokensResponse`: Response with tx hash, license terms IDs, royalty vault address, and distribute royalty tokens transaction hash.
"""
try:
nft_contract = validate_address(nft_contract)
ip_id = self._get_ip_id(nft_contract, token_id)
if self._is_registered(ip_id):
raise ValueError(
f"The NFT with id {token_id} is already registered as IP."
)

license_terms = self._validate_license_terms_data(license_terms_data)
calculated_deadline = self.sign_util.get_deadline(deadline=deadline)
royalty_shares_obj = get_royalty_shares(royalty_shares)

signature_response = self.sign_util.get_permission_signature(
ip_id=ip_id,
deadline=calculated_deadline,
state=self.web3.to_bytes(hexstr=HexStr(ZERO_HASH)),
permissions=[
{
"ipId": ip_id,
"signer": self.royalty_token_distribution_workflows_client.contract.address,
"to": self.core_metadata_module_client.contract.address,
"permission": AccessPermission.ALLOW,
"func": "setAll(address,string,bytes32,bytes32)",
},
{
"ipId": ip_id,
"signer": self.royalty_token_distribution_workflows_client.contract.address,
"to": self.licensing_module_client.contract.address,
"permission": AccessPermission.ALLOW,
"func": "attachLicenseTerms(address,address,uint256)",
},
{
"ipId": ip_id,
"signer": self.royalty_token_distribution_workflows_client.contract.address,
"to": self.licensing_module_client.contract.address,
"permission": AccessPermission.ALLOW,
"func": "setLicensingConfig(address,address,uint256,(bool,uint256,address,bytes,uint32,bool,uint32,address))",
},
],
)

response = build_and_send_transaction(
self.web3,
self.account,
self.royalty_token_distribution_workflows_client.build_registerIpAndAttachPILTermsAndDeployRoyaltyVault_transaction,
nft_contract,
token_id,
IPMetadata.from_input(ip_metadata).get_validated_data(),
license_terms,
{
"signer": self.web3.to_checksum_address(self.account.address),
"deadline": calculated_deadline,
"signature": self.web3.to_bytes(
hexstr=signature_response["signature"]
),
},
tx_options=tx_options,
)
ip_registered = self._parse_tx_ip_registered_event(response["tx_receipt"])
license_terms_ids = self._parse_tx_license_terms_attached_event(
response["tx_receipt"]
)
royalty_vault = self.get_royalty_vault_address_by_ip_id(
response["tx_receipt"],
ip_registered["ip_id"],
)

# Distribute royalty tokens
distribute_tx_hash = self._distribute_royalty_tokens(
ip_id=ip_registered["ip_id"],
royalty_shares=royalty_shares_obj["royalty_shares"],
royalty_vault=royalty_vault,
total_amount=royalty_shares_obj["total_amount"],
tx_options=tx_options,
deadline=calculated_deadline,
)

return RegisterAndAttachAndDistributeRoyaltyTokensResponse(
tx_hash=response["tx_hash"],
license_terms_ids=license_terms_ids,
royalty_vault=royalty_vault,
distribute_royalty_tokens_tx_hash=distribute_tx_hash,
ip_id=ip_registered["ip_id"],
token_id=ip_registered["token_id"],
)
except Exception as e:
raise ValueError(
f"Failed to register IP, attach PIL terms and distribute royalty tokens: {str(e)}"
) from e

def register_pil_terms_and_attach(
self,
ip_id: Address,
Expand Down Expand Up @@ -1261,6 +1379,70 @@ def _validate_license_token_ids(self, license_token_ids: list) -> list:

return license_token_ids

def _distribute_royalty_tokens(
self,
ip_id: Address,
royalty_shares: list[RoyaltyShareInput],
deadline: int,
royalty_vault: Address,
total_amount: int,
tx_options: dict | None = None,
) -> HexStr:
"""
Distribute royalty tokens to specified recipients.

This is an internal method that handles the distribution of royalty tokens
from an IP's royalty vault to the specified recipients.

:param ip_id Address: The IP ID.
:param royalty_shares list[RoyaltyShareInput]: The validated royalty shares with recipient and percentage.
:param deadline int: The deadline for the signature.
:param royalty_vault Address: The address of the royalty vault.
:param total_amount int: The total amount of royalty tokens to distribute.
:param tx_options dict: [Optional] Transaction options.
:return HexStr: The transaction hash.
"""
try:
ip_account_impl_client = IPAccountImplClient(self.web3, ip_id)
state = ip_account_impl_client.state()

ip_royalty_vault_client = IpRoyaltyVaultImplClient(self.web3, royalty_vault)

signature_response = self.sign_util.get_signature(
state=state,
to=royalty_vault,
encode_data=ip_royalty_vault_client.contract.encode_abi(
abi_element_identifier="approve",
args=[
self.royalty_token_distribution_workflows_client.contract.address,
total_amount,
],
),
verifying_contract=ip_id,
deadline=deadline,
)

response = build_and_send_transaction(
self.web3,
self.account,
self.royalty_token_distribution_workflows_client.build_distributeRoyaltyTokens_transaction,
ip_id,
royalty_shares,
{
"signer": self.web3.to_checksum_address(self.account.address),
"deadline": deadline,
"signature": self.web3.to_bytes(
hexstr=signature_response["signature"]
),
},
tx_options=tx_options,
)

return response["tx_hash"]

except Exception as e:
raise ValueError(f"Failed to distribute royalty tokens: {str(e)}") from e

def _get_ip_id(self, token_contract: str, token_id: int) -> str:
"""
Get the IP ID for a given token.
Expand Down
15 changes: 15 additions & 0 deletions src/story_protocol_python_sdk/types/resource/IPAsset.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ class RegisterPILTermsAndAttachResponse(TypedDict):
license_terms_ids: list[int]


class RegisterAndAttachAndDistributeRoyaltyTokensResponse(
RegistrationWithRoyaltyVaultAndLicenseTermsResponse
):
"""
Response structure for IP asset registration operations with royalty vault, license terms and distribute royalty tokens.

Extends `RegistrationWithRoyaltyVaultAndLicenseTermsResponse` with distribute royalty tokens transaction hash.

Attributes:
distribute_royalty_tokens_tx_hash: The transaction hash of the distribute royalty tokens transaction.
"""

distribute_royalty_tokens_tx_hash: HexStr


@dataclass
class LicenseTermsDataInput:
"""
Expand Down
70 changes: 70 additions & 0 deletions tests/integration/test_integration_ip_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from story_protocol_python_sdk.abi.LicenseToken.LicenseToken_client import (
LicenseTokenClient,
)
from story_protocol_python_sdk.utils.licensing_config_data import LicensingConfig
from tests.integration.config.test_config import account_2
from tests.integration.config.utils import approve

Expand Down Expand Up @@ -692,6 +693,75 @@ def test_register_pil_terms_and_attach(
assert isinstance(response["tx_hash"], str)
assert len(response["license_terms_ids"]) == 2

def test_register_ip_and_attach_pil_terms_and_distribute_royalty_tokens(
self, story_client: StoryClient, nft_collection
):
"""Test registering an existing NFT as IP, attaching PIL terms and distributing royalty tokens with all optional parameters"""
# Mint an NFT first
token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account)

royalty_shares = [
RoyaltyShareInput(recipient=account.address, percentage=30.0),
RoyaltyShareInput(recipient=account_2.address, percentage=70.0),
]

response = story_client.IPAsset.register_ip_and_attach_pil_terms_and_distribute_royalty_tokens(
nft_contract=nft_collection,
token_id=token_id,
license_terms_data=[
LicenseTermsDataInput(
terms=LicenseTermsInput(
transferable=True,
royalty_policy=ROYALTY_POLICY,
default_minting_fee=10000,
expiration=1000,
commercial_use=True,
commercial_attribution=False,
commercializer_checker=ZERO_ADDRESS,
commercializer_checker_data=ZERO_HASH,
commercial_rev_share=10,
commercial_rev_ceiling=0,
derivatives_allowed=True,
derivatives_attribution=True,
derivatives_approval=False,
derivatives_reciprocal=True,
derivative_rev_ceiling=0,
currency=WIP_TOKEN_ADDRESS,
uri="test case with custom values",
),
licensing_config=LicensingConfig(
is_set=True,
minting_fee=10000,
licensing_hook=ZERO_ADDRESS,
hook_data=ZERO_HASH,
commercial_rev_share=10,
disabled=False,
expect_minimum_group_reward_share=0,
expect_group_reward_pool=ZERO_ADDRESS,
),
)
],
royalty_shares=royalty_shares,
ip_metadata=COMMON_IP_METADATA,
deadline=1000,
)

# Verify all response fields
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) and response["token_id"] == token_id
)
assert (
isinstance(response["license_terms_ids"], list)
and len(response["license_terms_ids"]) > 0
)
assert isinstance(response["royalty_vault"], str) and response["royalty_vault"]
assert (
isinstance(response["distribute_royalty_tokens_tx_hash"], str)
and response["distribute_royalty_tokens_tx_hash"]
)


class TestIPAssetMint:
@pytest.fixture(scope="module")
Expand Down
14 changes: 13 additions & 1 deletion tests/unit/fixtures/data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from story_protocol_python_sdk import LicenseTermsDataInput, LicenseTermsInput
from ens.ens import HexStr

from story_protocol_python_sdk import (
IPMetadataInput,
LicenseTermsDataInput,
LicenseTermsInput,
)
from story_protocol_python_sdk.utils.constants import ZERO_ADDRESS, ZERO_HASH

CHAIN_ID = 1315
Expand Down Expand Up @@ -71,3 +77,9 @@
},
)
]
IP_METADATA = IPMetadataInput(
ip_metadata_uri="https://example.com/ip-metadata.json",
ip_metadata_hash=HexStr("0x" + "a" * 64),
nft_metadata_uri="https://example.com/nft-metadata.json",
nft_metadata_hash=HexStr("0x" + "b" * 64),
)
Loading
Loading