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
3 changes: 3 additions & 0 deletions src/story_protocol_python_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .resources.WIP import WIP
from .story_client import StoryClient
from .types.common import AccessPermission
from .types.resource.IPAsset import RegistrationResponse
from .utils.constants import (
DEFAULT_FUNCTION_SELECTOR,
MAX_ROYALTY_TOKEN,
Expand All @@ -28,9 +29,11 @@
"IPAccount",
"Dispute",
"WIP",
# Types
"AccessPermission",
"DerivativeDataInput",
"IPMetadataInput",
"RegistrationResponse",
# Constants
"ZERO_ADDRESS",
"ZERO_HASH",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ def __init__(self, web3: Web3):
abi = json.load(abi_file)
self.contract = self.web3.eth.contract(address=contract_address, abi=abi)

def mintAndRegisterIpAndMakeDerivative(
self, spgNftContract, derivData, ipMetadata, recipient, allowDuplicates
):
return self.contract.functions.mintAndRegisterIpAndMakeDerivative(
spgNftContract, derivData, ipMetadata, recipient, allowDuplicates
).transact()

def build_mintAndRegisterIpAndMakeDerivative_transaction(
self,
spgNftContract,
derivData,
ipMetadata,
recipient,
allowDuplicates,
tx_params,
):
return self.contract.functions.mintAndRegisterIpAndMakeDerivative(
spgNftContract, derivData, ipMetadata, recipient, allowDuplicates
).build_transaction(tx_params)

def registerIpAndMakeDerivative(
self, nftContract, tokenId, derivData, ipMetadata, sigMetadataAndRegister
):
Expand Down
55 changes: 53 additions & 2 deletions src/story_protocol_python_sdk/resources/IPAsset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Module for handling IP Account operations and transactions."""

from ens.ens import HexStr
from ens.ens import Address, HexStr
from web3 import Web3

from story_protocol_python_sdk.abi.AccessController.AccessController_client import (
Expand Down Expand Up @@ -35,6 +35,7 @@
)
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.types.resource.IPAsset import RegistrationResponse
from story_protocol_python_sdk.utils.constants import (
MAX_ROYALTY_TOKEN,
ZERO_ADDRESS,
Expand All @@ -49,6 +50,7 @@
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
from story_protocol_python_sdk.utils.validation import validate_address


class IPAsset:
Expand Down Expand Up @@ -291,7 +293,7 @@ def register_derivative(
return {"tx_hash": response["tx_hash"]}

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

def register_derivative_with_license_tokens(
self,
Expand Down Expand Up @@ -774,6 +776,55 @@ def register_derivative_ip(
except Exception as e:
raise e

def mint_and_register_ip_and_make_derivative(
self,
spg_nft_contract: str,
deriv_data: DerivativeDataInput,
ip_metadata: IPMetadataInput | None = None,
recipient: Address | None = None,
allow_duplicates: bool = True,
tx_options: dict | None = None,
) -> RegistrationResponse:
"""
Mint an NFT from a collection and register it as a derivative IP without license tokens.

:param spg_nft_contract str: The address of the SPGNFT collection.
:param deriv_data `DerivativeDataInput`: The derivative data to be used for register derivative.
:param ip_metadata `IPMetadataInput`: [Optional] The desired metadata for the newly minted NFT and newly registered IP.
:param recipient str: [Optional] The address to receive the minted NFT. If not provided, the client's own wallet address will be used.
:param allow_duplicates bool: [Optional] Set to true to allow minting an NFT with a duplicate metadata hash. (default: True)
:param tx_options dict: [Optional] Transaction options.
:return RegistrationResponse: Dictionary with the tx hash, IP ID and token ID.
"""

try:
validated_deriv_data = DerivativeData.from_input(
web3=self.web3, input_data=deriv_data
).get_validated_data()
response = build_and_send_transaction(
self.web3,
self.account,
self.derivative_workflows_client.build_mintAndRegisterIpAndMakeDerivative_transaction,
validate_address(spg_nft_contract),
validated_deriv_data,
IPMetadata.from_input(ip_metadata).get_validated_data(),
(
validate_address(recipient)
if recipient is not None
else self.account.address
),
allow_duplicates,
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"],
"token_id": ip_registered["token_id"],
}
except Exception as e:
raise e

def _validate_max_rts(self, max_rts: int):
"""
Validates the maximum number of royalty tokens.
Expand Down
5 changes: 4 additions & 1 deletion src/story_protocol_python_sdk/scripts/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@
{
"contract_name": "DerivativeWorkflows",
"contract_address": "0x9e2d496f72C547C2C535B167e06ED8729B374a4f",
"functions": ["registerIpAndMakeDerivative"]
"functions": [
"registerIpAndMakeDerivative",
"mintAndRegisterIpAndMakeDerivative"
]
}
]
}
18 changes: 18 additions & 0 deletions src/story_protocol_python_sdk/types/resource/IPAsset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import Optional, TypedDict

from ens.ens import Address, HexStr


class RegistrationResponse(TypedDict):
"""
Response structure for IP asset registration operations.

Attributes:
ip_id: The IP ID of the registered IP asset
tx_hash: The transaction hash of the registration transaction
token_id: [Optional] The token ID of the registered IP asset
"""

ip_id: Address
tx_hash: HexStr
token_id: Optional[int]
66 changes: 65 additions & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import pytest

from story_protocol_python_sdk.story_client import StoryClient
from story_protocol_python_sdk.utils.constants import (
ROYALTY_POLICY_LAP_ADDRESS,
ZERO_ADDRESS,
)
from tests.integration.config.test_config import account, account_2, web3
from tests.integration.config.utils import get_story_client
from tests.integration.config.utils import MockERC20, get_story_client


@pytest.fixture(scope="session")
Expand All @@ -15,3 +19,63 @@ def story_client() -> StoryClient:
def story_client_2() -> StoryClient:
"""Fixture to provide the secondary story client"""
return get_story_client(web3, account_2)


@pytest.fixture(scope="module")
def nft_collection(story_client: StoryClient):
"""Fixture to provide the SPG NFT collection"""
tx_data = story_client.NFTClient.create_nft_collection(
name="test-collection",
symbol="TEST",
max_supply=100,
is_public_minting=True,
mint_open=True,
contract_uri="test-uri",
mint_fee_recipient=account.address,
)
return tx_data["nft_contract"]


@pytest.fixture(scope="module")
def parent_ip_and_license_terms(story_client: StoryClient, nft_collection):
"""Fixture to provide the parent IP and license terms"""
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_LAP_ADDRESS,
"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,
},
}
],
)
return {
"parent_ip_id": response["ip_id"],
"license_terms_id": response["license_terms_ids"][0],
}
100 changes: 44 additions & 56 deletions tests/integration/test_integration_ip_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,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 tests.integration.config.test_config import account_2

from .setup_for_integration import (
PIL_LICENSE_TEMPLATE,
Expand Down Expand Up @@ -254,62 +255,6 @@ def test_mint_register_ip(self, story_client: StoryClient, nft_collection):


class TestSPGNFTOperations:
@pytest.fixture(scope="module")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we remove these since they are included in the newly added test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I move into conftest.py, which saves the common fixture.

def nft_collection(self, story_client: StoryClient):
tx_data = story_client.NFTClient.create_nft_collection(
name="test-collection",
symbol="TEST",
max_supply=100,
is_public_minting=True,
mint_open=True,
contract_uri="test-uri",
mint_fee_recipient=account.address,
)
return tx_data["nft_contract"]

@pytest.fixture(scope="module")
def parent_ip_and_license_terms(self, story_client: StoryClient, nft_collection):
response = story_client.IPAsset.mint_and_register_ip_asset_with_pil_terms(
spg_nft_contract=nft_collection,
terms=[
{
"terms": {
"transferable": True,
"royalty_policy": ROYALTY_POLICY,
"default_minting_fee": 0,
"expiration": 0,
"commercial_use": True,
"commercial_attribution": False,
"commercializer_checker": ZERO_ADDRESS,
"commercializer_checker_data": ZERO_ADDRESS,
"commercial_rev_share": 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,
},
}
],
)

return {
"parent_ip_id": response["ip_id"],
"license_terms_id": response["license_terms_ids"][0],
}

# def test_register_ip_asset_with_metadata(self, story_client, nft_collection):
# token_id = mint_by_spg(nft_collection, story_client.web3, story_client.account, "test-metadata")
Expand Down Expand Up @@ -693,3 +638,46 @@ def test_mint_with_existing_metadata_hash_no_duplicates(
metadata_hash=metadata_hash,
allow_duplicates=False,
)


class TestMintAndRegisterIpAndMakeDerivative:
def test_default_value(
self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms
):
response = story_client.IPAsset.mint_and_register_ip_and_make_derivative(
spg_nft_contract=nft_collection,
deriv_data=DerivativeDataInput(
parent_ip_ids=[parent_ip_and_license_terms["parent_ip_id"]],
license_terms_ids=[parent_ip_and_license_terms["license_terms_id"]],
),
)
assert response is not None
assert isinstance(response["tx_hash"], str)
assert isinstance(response["ip_id"], str)
assert isinstance(response["token_id"], int)

def test_with_custom_value(
self, story_client: StoryClient, nft_collection, parent_ip_and_license_terms
):
response = story_client.IPAsset.mint_and_register_ip_and_make_derivative(
spg_nft_contract=nft_collection,
deriv_data=DerivativeDataInput(
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=10000,
max_rts=10,
max_revenue_share=100,
),
ip_metadata=IPMetadataInput(
ip_metadata_uri="https://example.com/metadata/custom-value.json",
ip_metadata_hash=web3.keccak(text="custom-value-metadata"),
nft_metadata_uri="https://example.com/metadata/custom-value.json",
nft_metadata_hash=web3.keccak(text="custom-value-metadata"),
),
recipient=account_2.address,
allow_duplicates=False,
)
assert response is not None
assert isinstance(response["tx_hash"], str)
assert isinstance(response["ip_id"], str)
assert isinstance(response["token_id"], int)
Loading
Loading