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
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,8 @@ def build_tagIfRelatedIpInfringed_transaction(
ipIdToTag, infringerDisputeId
).build_transaction(tx_params)

def isIpTagged(self, ipId):
return self.contract.functions.isIpTagged(ipId).call()

def isWhitelistedDisputeTag(self, tag):
return self.contract.functions.isWhitelistedDisputeTag(tag).call()
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ def build_addIp_transaction(
groupIpId, ipIds, maxAllowedRewardShare
).build_transaction(tx_params)

def removeIp(self, groupIpId, ipIds):
return self.contract.functions.removeIp(groupIpId, ipIds).transact()

def build_removeIp_transaction(self, groupIpId, ipIds, tx_params):
return self.contract.functions.removeIp(
groupIpId, ipIds
).build_transaction(tx_params)

def claimReward(self, groupId, token, ipIds):
return self.contract.functions.claimReward(groupId, token, ipIds).transact()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,6 @@ def ipId(self, chainId, tokenContract, tokenId):

def isRegistered(self, id):
return self.contract.functions.isRegistered(id).call()

def isRegisteredGroup(self, groupId):
return self.contract.functions.isRegisteredGroup(groupId).call()
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def getRoyaltyPercent(self, ipId, licenseTemplate, licenseTermsId):
ipId, licenseTemplate, licenseTermsId
).call()

def hasDerivativeIps(self, parentIpId):
return self.contract.functions.hasDerivativeIps(parentIpId).call()

def hasIpAttachedLicenseTerms(self, ipId, licenseTemplate, licenseTermsId):
return self.contract.functions.hasIpAttachedLicenseTerms(
ipId, licenseTemplate, licenseTermsId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,10 @@ def __init__(self, web3: Web3):
abi = json.load(abi_file)
self.contract = self.web3.eth.contract(address=contract_address, abi=abi)

def getTotalTokensByLicensor(self, licensorIpId):
return self.contract.functions.getTotalTokensByLicensor(
licensorIpId
).call()

def ownerOf(self, tokenId):
return self.contract.functions.ownerOf(tokenId).call()
177 changes: 177 additions & 0 deletions src/story_protocol_python_sdk/resources/Group.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
from story_protocol_python_sdk.abi.CoreMetadataModule.CoreMetadataModule_client import (
CoreMetadataModuleClient,
)
from story_protocol_python_sdk.abi.DisputeModule.DisputeModule_client import (
DisputeModuleClient,
)
from story_protocol_python_sdk.abi.GroupingModule.GroupingModule_client import (
GroupingModuleClient,
)
Expand All @@ -19,6 +22,9 @@
from story_protocol_python_sdk.abi.LicenseRegistry.LicenseRegistry_client import (
LicenseRegistryClient,
)
from story_protocol_python_sdk.abi.LicenseToken.LicenseToken_client import (
LicenseTokenClient,
)
from story_protocol_python_sdk.abi.LicensingModule.LicensingModule_client import (
LicensingModuleClient,
)
Expand Down Expand Up @@ -58,9 +64,11 @@ def __init__(self, web3: Web3, account, chain_id: int):
self.grouping_module_client = GroupingModuleClient(web3)
self.grouping_workflows_client = GroupingWorkflowsClient(web3)
self.ip_asset_registry_client = IPAssetRegistryClient(web3)
self.dispute_module_client = DisputeModuleClient(web3)
self.core_metadata_module_client = CoreMetadataModuleClient(web3)
self.licensing_module_client = LicensingModuleClient(web3)
self.license_registry_client = LicenseRegistryClient(web3)
self.license_token_client = LicenseTokenClient(web3)
self.pi_license_template_client = PILicenseTemplateClient(web3)
self.module_registry_client = ModuleRegistryClient(web3)
self.sign_util = Sign(web3, self.chain_id, self.account)
Expand Down Expand Up @@ -453,6 +461,121 @@ def register_group_and_attach_license_and_add_ips(
f"Failed to register group and attach license and add IPs: {str(e)}"
)

def add_ips_to_group(
self,
group_ip_id: str,
ip_ids: list,
max_allowed_reward_share_percentage: int = 100,
tx_options: dict | None = None,
) -> dict:
"""
Add IPs to an existing group IP.

:param group_ip_id str: The ID of the group IP.
:param ip_ids list: List of IP IDs to add to the group.
:param max_allowed_reward_share_percentage int: [Optional] Maximum allowed reward share percentage (0-100). Default is 100.
:param tx_options dict: [Optional] The transaction options.
:return dict: A dictionary with the transaction hash.
"""
try:
if not self.web3.is_address(group_ip_id):
raise ValueError(f'Group IP ID "{group_ip_id}" is invalid.')

for ip_id in ip_ids:
if not self.web3.is_address(ip_id):
raise ValueError(f'IP ID "{ip_id}" is invalid.')

# Contract-level validation: groupId must not be disputed
if self.dispute_module_client.isIpTagged(group_ip_id):
raise ValueError(
f'Disputed group cannot add IP: group "{group_ip_id}" is tagged by dispute module.'
)

# Contract-level validation: ipIds must not contain disputed IPs or groups
for ip_id in ip_ids:
if self.dispute_module_client.isIpTagged(ip_id):
raise ValueError(
f'Cannot add disputed IP to group: IP "{ip_id}" is tagged by dispute module.'
)
if self.ip_asset_registry_client.isRegisteredGroup(ip_id):
raise ValueError(
f'Cannot add group to group: IP "{ip_id}" is a registered group.'
)

max_allowed_reward_share = get_revenue_share(
max_allowed_reward_share_percentage,
type=RevShareType.MAX_ALLOWED_REWARD_SHARE,
)

response = build_and_send_transaction(
self.web3,
self.account,
self.grouping_module_client.build_addIp_transaction,
group_ip_id,
ip_ids,
max_allowed_reward_share,
tx_options=tx_options,
)

result = {"tx_hash": response["tx_hash"]}
if "tx_receipt" in response:
result["tx_receipt"] = response["tx_receipt"]
return result

except Exception as e:
raise ValueError(f"Failed to add IP to group: {str(e)}")

def remove_ips_from_group(
self,
group_ip_id: str,
ip_ids: list,
tx_options: dict | None = None,
) -> dict:
"""
Remove IPs from a group IP.

:param group_ip_id str: The ID of the group IP.
:param ip_ids list: List of IP IDs to remove from the group.
:param tx_options dict: [Optional] The transaction options.
:return dict: A dictionary with the transaction hash.
"""
try:
if not self.web3.is_address(group_ip_id):
raise ValueError(f'Group IP ID "{group_ip_id}" is invalid.')

for ip_id in ip_ids:
if not self.web3.is_address(ip_id):
raise ValueError(f'IP ID "{ip_id}" is invalid.')

# Contract-level validation: group must not have derivative IPs
if self.license_registry_client.hasDerivativeIps(group_ip_id):
raise ValueError(
f'Group frozen: group "{group_ip_id}" has derivative IPs and cannot remove members.'
)

# Contract-level validation: group must not have minted license tokens
if self.license_token_client.getTotalTokensByLicensor(group_ip_id) > 0:
raise ValueError(
f'Group frozen: group "{group_ip_id}" has already minted license tokens and cannot remove members.'
)

response = build_and_send_transaction(
self.web3,
self.account,
self.grouping_module_client.build_removeIp_transaction,
group_ip_id,
ip_ids,
tx_options=tx_options,
)

result = {"tx_hash": response["tx_hash"]}
if "tx_receipt" in response:
result["tx_receipt"] = response["tx_receipt"]
return result

except Exception as e:
raise ValueError(f"Failed to remove IPs from group: {str(e)}")

def collect_and_distribute_group_royalties(
self,
group_ip_id: str,
Expand Down Expand Up @@ -847,3 +970,57 @@ def _parse_tx_royalty_paid_event(self, tx_receipt: dict) -> list:
)

return royalties_distributed

def get_added_ip_to_group_events(self, tx_receipt: dict) -> list:
"""
Parse AddedIpToGroup events from a transaction receipt (for chain-state verification).

:param tx_receipt dict: The transaction receipt.
:return list: List of dicts with groupId and ipIds (checksum addresses).
"""
events = []
for log in tx_receipt["logs"]:
try:
event_result = self.grouping_module_client.contract.events.AddedIpToGroup.process_log(
log
)
args = event_result["args"]
events.append(
{
"groupId": self.web3.to_checksum_address(args["groupId"]),
"ipIds": [
self.web3.to_checksum_address(addr)
for addr in args["ipIds"]
],
}
)
except Exception:
continue
return events

def get_removed_ip_from_group_events(self, tx_receipt: dict) -> list:
"""
Parse RemovedIpFromGroup events from a transaction receipt (for chain-state verification).

:param tx_receipt dict: The transaction receipt.
:return list: List of dicts with groupId and ipIds (checksum addresses).
"""
events = []
for log in tx_receipt["logs"]:
try:
event_result = self.grouping_module_client.contract.events.RemovedIpFromGroup.process_log(
log
)
args = event_result["args"]
events.append(
{
"groupId": self.web3.to_checksum_address(args["groupId"]),
"ipIds": [
self.web3.to_checksum_address(addr)
for addr in args["ipIds"]
],
}
)
except Exception:
continue
return events
Loading
Loading