Skip to content
Open
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 @@ -29,6 +29,12 @@ def __init__(self, web3: Web3):
abi = json.load(abi_file)
self.contract = self.web3.eth.contract(address=contract_address, abi=abi)

def aggregate3(self, calls):
return self.contract.functions.aggregate3(calls).transact()

def build_aggregate3_transaction(self, calls, tx_params):
return self.contract.functions.aggregate3(calls).build_transaction(tx_params)

def aggregate3Value(self, calls):
return self.contract.functions.aggregate3Value(calls).transact()

Expand Down
151 changes: 149 additions & 2 deletions src/story_protocol_python_sdk/resources/IPAsset.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,15 @@ def register(
:param nft_metadata_hash str: [Optional] Metadata hash for the NFT.
:param deadline int: [Optional] Signature deadline in seconds. (default: 1000 seconds)
:param tx_options dict: [Optional] Transaction options.
:return dict: Dictionary with the transaction hash and IP ID.
:param encodedTxDataOnly bool: [Optional] If True, return only encoded transaction data without sending.
:return dict: Dictionary with the transaction hash and IP ID, or encoded transaction data if encodedTxDataOnly is True.
"""
try:
tx_options = tx_options or {}
encoded_tx_data_only = tx_options.get("encodedTxDataOnly", False)

ip_id = self._get_ip_id(nft_contract, token_id)
if self.is_registered(ip_id):
if self.is_registered(ip_id) and not encoded_tx_data_only:
return {"tx_hash": None, "ip_id": ip_id}

req_object: dict = {
Expand Down Expand Up @@ -254,6 +258,15 @@ def register(
"signature": signature_response["signature"],
}

if encoded_tx_data_only:
encoded_data = self.registration_workflows_client.contract.functions.registerIp(
req_object["nftContract"],
req_object["tokenId"],
req_object["ipMetadata"],
req_object["sigMetadata"],
).build_transaction({"from": self.account.address})["data"]
return {"encoded_tx_data": encoded_data, "has_metadata": True}

response = build_and_send_transaction(
self.web3,
self.account,
Expand All @@ -265,6 +278,14 @@ def register(
tx_options=tx_options,
)
else:
if encoded_tx_data_only:
encoded_data = self.ip_asset_registry_client.contract.functions.register(
self.chain_id,
nft_contract,
token_id,
).build_transaction({"from": self.account.address})["data"]
return {"encoded_tx_data": encoded_data, "has_metadata": False}

response = build_and_send_transaction(
self.web3,
self.account,
Expand All @@ -284,6 +305,132 @@ def register(
except Exception as e:
raise e

def batch_register(
self,
args: list[dict],
tx_options: dict | None = None,
) -> dict:
"""
Batch register multiple NFTs as IPs, creating corresponding IP records.

This method uses the low-level register() method internally for encoding transactions.
While register() is deprecated for direct use, it remains the appropriate choice for
batch operations as it provides the necessary flexibility for encoding individual
registration calls before batching them via multicall.

:param args list[dict]: List of registration arguments, each containing:
:param nft_contract str: The address of the NFT.
:param token_id int: The token identifier of the NFT.
:param ip_metadata dict: [Optional] Metadata for the IP.
:param deadline int: [Optional] Signature deadline in seconds.
:param tx_options dict: [Optional] Transaction options (excluding encodedTxDataOnly).
:return dict: Dictionary with transaction hashes and results.
:return tx_hash str: [Optional] Transaction hash for registrations without metadata.
:return spg_tx_hash str: [Optional] Transaction hash for registrations with metadata (SPG workflow).
:return results list[dict]: List of results, each containing:
:return ip_id str: The IP ID.
:return token_id int: The token ID.
:return nft_contract str: The NFT contract address.
"""
try:
tx_options = tx_options or {}
spg_contracts = []
registry_contracts = []

for arg in args:
nft_contract = arg.get("nft_contract")
token_id = arg.get("token_id")
ip_metadata = arg.get("ip_metadata")
deadline = arg.get("deadline")

if not nft_contract or token_id is None:
raise ValueError("Each arg must contain 'nft_contract' and 'token_id'")

try:
result = self.register(
nft_contract=nft_contract,
token_id=token_id,
ip_metadata=ip_metadata,
deadline=deadline,
tx_options={"encodedTxDataOnly": True},
)
encoded_data = result["encoded_tx_data"]
has_metadata = result["has_metadata"]
except Exception as e:
error_msg = str(e).replace("Failed to register IP:", "").strip()
raise ValueError(f"Failed to encode registration for {nft_contract}:{token_id}: {error_msg}")

if has_metadata:
spg_contracts.append(encoded_data)
else:
registry_contracts.append({
"target": self.ip_asset_registry_client.contract.address,
"allowFailure": False,
"callData": encoded_data,
})

spg_tx_hash = None
tx_hash = None

if spg_contracts:
spg_response = build_and_send_transaction(
self.web3,
self.account,
self.registration_workflows_client.build_multicall_transaction,
spg_contracts,
tx_options=tx_options,
)
spg_tx_hash = spg_response["tx_hash"]

if registry_contracts:
registry_response = build_and_send_transaction(
self.web3,
self.account,
self.multicall3_client.build_aggregate3_transaction,
registry_contracts,
tx_options=tx_options,
)
tx_hash = registry_response["tx_hash"]

results = []

if tx_hash:
tx_receipt = self.web3.eth.get_transaction_receipt(tx_hash)
event_signature = self.web3.keccak(
text="IPRegistered(address,uint256,address,uint256,string,string,uint256)"
).hex()
for log in tx_receipt["logs"]:
if log["topics"][0].hex() == event_signature:
event_result = self.ip_asset_registry_client.contract.events.IPRegistered.process_log(log)
results.append({
"ip_id": self.web3.to_checksum_address(event_result["args"]["ipId"]),
"token_id": event_result["args"]["tokenId"],
"nft_contract": self.web3.to_checksum_address(event_result["args"]["tokenContract"]),
})

if spg_tx_hash:
spg_receipt = self.web3.eth.get_transaction_receipt(spg_tx_hash)
event_signature = self.web3.keccak(
text="IPRegistered(address,uint256,address,uint256,string,string,uint256)"
).hex()
for log in spg_receipt["logs"]:
if log["topics"][0].hex() == event_signature:
event_result = self.ip_asset_registry_client.contract.events.IPRegistered.process_log(log)
results.append({
"ip_id": self.web3.to_checksum_address(event_result["args"]["ipId"]),
"token_id": event_result["args"]["tokenId"],
"nft_contract": self.web3.to_checksum_address(event_result["args"]["tokenContract"]),
})

return {
"tx_hash": tx_hash,
"spg_tx_hash": spg_tx_hash,
"results": results,
}

except Exception as e:
raise e

@deprecated("Use link_derivative() instead.")
def register_derivative(
self,
Expand Down
110 changes: 110 additions & 0 deletions tests/integration/test_integration_ip_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1758,3 +1758,113 @@ def test_link_derivative_with_license_tokens(
assert "tx_hash" in response
assert isinstance(response["tx_hash"], str)
assert len(response["tx_hash"]) > 0


class TestIPAssetBatchRegister:
def test_batch_register_without_metadata(self, story_client: StoryClient):
"""Batch register multiple NFTs without metadata."""
token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account)
token_id_2 = get_token_id(MockERC721, story_client.web3, story_client.account)

response = story_client.IPAsset.batch_register(
args=[
{"nft_contract": MockERC721, "token_id": token_id_1},
{"nft_contract": MockERC721, "token_id": token_id_2},
],
)

assert response is not None
assert "tx_hash" in response
assert response["tx_hash"] is not None
assert "spg_tx_hash" in response
assert response["spg_tx_hash"] is None
assert "results" in response
assert len(response["results"]) == 2
assert all("ip_id" in result for result in response["results"])
assert all("token_id" in result for result in response["results"])
assert all("nft_contract" in result for result in response["results"])
assert response["results"][0]["token_id"] == token_id_1
assert response["results"][1]["token_id"] == token_id_2

def test_batch_register_with_metadata(self, story_client: StoryClient):
"""Batch register multiple NFTs with metadata."""
token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account)
token_id_2 = get_token_id(MockERC721, story_client.web3, story_client.account)

metadata_1 = {
"ip_metadata_uri": "test-uri-1",
"ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-1")),
"nft_metadata_uri": "test-nft-uri-1",
"nft_metadata_hash": web3.to_hex(web3.keccak(text="test-nft-metadata-1")),
}
metadata_2 = {
"ip_metadata_uri": "test-uri-2",
"ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata-2")),
"nft_metadata_uri": "test-nft-uri-2",
"nft_metadata_hash": web3.to_hex(web3.keccak(text="test-nft-metadata-2")),
}

response = story_client.IPAsset.batch_register(
args=[
{
"nft_contract": MockERC721,
"token_id": token_id_1,
"ip_metadata": metadata_1,
},
{
"nft_contract": MockERC721,
"token_id": token_id_2,
"ip_metadata": metadata_2,
},
],
)

assert response is not None
assert "spg_tx_hash" in response
assert response["spg_tx_hash"] is not None
assert "tx_hash" in response
assert response["tx_hash"] is None
assert "results" in response
assert len(response["results"]) == 2
assert response["results"][0]["token_id"] == token_id_1
assert response["results"][1]["token_id"] == token_id_2

def test_batch_register_mixed_with_and_without_metadata(
self, story_client: StoryClient
):
"""Batch register with mixed metadata and non-metadata NFTs."""
token_id_1 = get_token_id(MockERC721, story_client.web3, story_client.account)
token_id_2 = get_token_id(MockERC721, story_client.web3, story_client.account)
token_id_3 = get_token_id(MockERC721, story_client.web3, story_client.account)

metadata = {
"ip_metadata_uri": "test-uri",
"ip_metadata_hash": web3.to_hex(web3.keccak(text="test-metadata")),
"nft_metadata_uri": "test-nft-uri",
"nft_metadata_hash": web3.to_hex(web3.keccak(text="test-nft-metadata")),
}

response = story_client.IPAsset.batch_register(
args=[
{"nft_contract": MockERC721, "token_id": token_id_1},
{
"nft_contract": MockERC721,
"token_id": token_id_2,
"ip_metadata": metadata,
},
{"nft_contract": MockERC721, "token_id": token_id_3},
],
)

assert response is not None
assert "tx_hash" in response
assert response["tx_hash"] is not None
assert "spg_tx_hash" in response
assert response["spg_tx_hash"] is not None
assert "results" in response
assert len(response["results"]) == 3

result_token_ids = [r["token_id"] for r in response["results"]]
assert token_id_1 in result_token_ids
assert token_id_2 in result_token_ids
assert token_id_3 in result_token_ids
Loading
Loading