From 7c00bbcf84678c6b1beb4cbb8357d2f808298433 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Thu, 5 Feb 2026 18:27:14 +0800 Subject: [PATCH 01/10] feat: add is_registered and get_balance public methods - Expose is_registered as public method in IPAsset class with input validation - Add get_balance(address) method to StoryClient for querying any address balance - Update get_wallet_balance to reuse get_balance method - Add proper input validation with clear error messages --- .../resources/IPAsset.py | 30 ++++++++++++------- src/story_protocol_python_sdk/story_client.py | 23 ++++++++++++-- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 2b0e7935..58f4237c 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -199,7 +199,7 @@ def register( """ try: ip_id = self._get_ip_id(nft_contract, token_id) - if self._is_registered(ip_id): + if self.is_registered(ip_id): return {"tx_hash": None, "ip_id": ip_id} req_object: dict = { @@ -316,7 +316,7 @@ def register_derivative( :return dict: A dictionary with the transaction hash """ try: - if not self._is_registered(child_ip_id): + if not self.is_registered(child_ip_id): raise ValueError( f"The child IP with id {child_ip_id} is not registered." ) @@ -378,7 +378,7 @@ def register_derivative_with_license_tokens( validate_max_rts(max_rts) # Validate child IP registration - if not self._is_registered(child_ip_id): + if not self.is_registered(child_ip_id): raise ValueError( f"The child IP with id {child_ip_id} is not registered." ) @@ -757,7 +757,7 @@ def register_ip_and_attach_pil_terms( """ try: ip_id = self._get_ip_id(nft_contract, token_id) - if self._is_registered(ip_id): + if self.is_registered(ip_id): raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) @@ -872,7 +872,7 @@ def register_derivative_ip( """ try: ip_id = self._get_ip_id(nft_contract, token_id) - if self._is_registered(ip_id): + if self.is_registered(ip_id): raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) @@ -1061,7 +1061,7 @@ def register_ip_and_make_derivative_with_license_tokens( """ try: ip_id = self._get_ip_id(nft_contract, token_id) - if self._is_registered(ip_id): + if self.is_registered(ip_id): raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) @@ -1290,7 +1290,7 @@ def register_derivative_ip_and_attach_pil_terms_and_distribute_royalty_tokens( try: nft_contract = validate_address(nft_contract) ip_id = self._get_ip_id(nft_contract, token_id) - if self._is_registered(ip_id): + if self.is_registered(ip_id): raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) @@ -1397,7 +1397,7 @@ def register_ip_and_attach_pil_terms_and_distribute_royalty_tokens( try: nft_contract = validate_address(nft_contract) ip_id = self._get_ip_id(nft_contract, token_id) - if self._is_registered(ip_id): + if self.is_registered(ip_id): raise ValueError( f"The NFT with id {token_id} is already registered as IP." ) @@ -1500,7 +1500,7 @@ def register_pil_terms_and_attach( :return RegisterPILTermsAndAttachResponse: Dictionary with the tx hash and license terms IDs. """ try: - if not self._is_registered(ip_id): + if not self.is_registered(ip_id): raise ValueError(f"The IP with id {ip_id} is not registered.") calculated_deadline = self.sign_util.get_deadline(deadline=deadline) ip_account_impl_client = IPAccountImplClient(self.web3, ip_id) @@ -2009,7 +2009,7 @@ def _validate_derivative_data(self, derivative_data: dict) -> dict: for parent_id, terms_id in zip( internal_data["parentIpIds"], internal_data["licenseTermsIds"] ): - if not self._is_registered(parent_id): + if not self.is_registered(parent_id): raise ValueError( f"The parent IP with id {parent_id} is not registered." ) @@ -2134,13 +2134,21 @@ def _get_ip_id(self, token_contract: str, token_id: int) -> str: self.chain_id, token_contract, token_id ) - def _is_registered(self, ip_id: str) -> bool: + def is_registered(self, ip_id: str) -> bool: """ Check if an IP is registered. :param ip_id str: The IP ID to check. :return bool: True if registered, False otherwise. + :raises ValueError: If the ip_id is empty or has invalid format. """ + if not ip_id: + raise ValueError("is_registered: ip_id is required") + + if not self.web3.is_address(ip_id): + raise ValueError(f"is_registered: invalid IP ID address format: {ip_id}") + + ip_id = self.web3.to_checksum_address(ip_id) return self.ip_asset_registry_client.isRegistered(ip_id) def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> list[RegisteredIP]: diff --git a/src/story_protocol_python_sdk/story_client.py b/src/story_protocol_python_sdk/story_client.py index 51ea6b56..655ac4b4 100644 --- a/src/story_protocol_python_sdk/story_client.py +++ b/src/story_protocol_python_sdk/story_client.py @@ -155,14 +155,31 @@ def Group(self) -> Group: self._group = Group(self.web3, self.account, self.chain_id) return self._group + def get_balance(self, address: str) -> int: + """ + Get the native token (IP) balance of the specified address. + + :param address str: The address to query the balance for. + :return int: The native token balance of the specified address in wei. + :raises ValueError: If the address is invalid. + """ + if not address: + raise ValueError("Address must be provided") + + if not self.web3.is_address(address): + raise ValueError(f"Invalid address format: {address}") + + checksum_address = self.web3.to_checksum_address(address) + return self.web3.eth.get_balance(checksum_address) + def get_wallet_balance(self) -> int: """ - Get the WIP token balance of the current wallet. + Get the native token (IP) balance of the current wallet. - :return int: The WIP token balance of the current wallet. + :return int: The native token balance of the current wallet in wei. :raises ValueError: If no account is found. """ if not self.account or not hasattr(self.account, "address"): raise ValueError("No account found in wallet") - return self.web3.eth.get_balance(self.account.address) + return self.get_balance(self.account.address) From cbb1db48d9e98a468f209ca540ff6aa1f8ffd667 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 09:53:39 +0800 Subject: [PATCH 02/10] fix: improve is_registered to handle different input types Co-authored-by: Cursor --- src/story_protocol_python_sdk/resources/IPAsset.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 58f4237c..cfe1889a 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -2144,6 +2144,12 @@ def is_registered(self, ip_id: str) -> bool: """ if not ip_id: raise ValueError("is_registered: ip_id is required") + + # Convert to string if it's not already (e.g., bytes from contract call) + if isinstance(ip_id, bytes): + ip_id = self.web3.to_hex(ip_id) + elif not isinstance(ip_id, str): + ip_id = str(ip_id) if not self.web3.is_address(ip_id): raise ValueError(f"is_registered: invalid IP ID address format: {ip_id}") From 0137ef371a4eaa6a0cd68ea0ffa14c819a71ac69 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 09:57:10 +0800 Subject: [PATCH 03/10] fix: remove to_checksum_address conversion in is_registered to match original behavior Co-authored-by: Cursor --- src/story_protocol_python_sdk/resources/IPAsset.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index cfe1889a..cee26cf2 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -2145,16 +2145,9 @@ def is_registered(self, ip_id: str) -> bool: if not ip_id: raise ValueError("is_registered: ip_id is required") - # Convert to string if it's not already (e.g., bytes from contract call) - if isinstance(ip_id, bytes): - ip_id = self.web3.to_hex(ip_id) - elif not isinstance(ip_id, str): - ip_id = str(ip_id) - if not self.web3.is_address(ip_id): raise ValueError(f"is_registered: invalid IP ID address format: {ip_id}") - ip_id = self.web3.to_checksum_address(ip_id) return self.ip_asset_registry_client.isRegistered(ip_id) def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> list[RegisteredIP]: From a66c5542e55c1c55d5d31f9d1d6569cfb69f3816 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 09:59:56 +0800 Subject: [PATCH 04/10] fix: add to_checksum_address conversion in is_registered and claim_rewards for address format consistency Co-authored-by: Cursor --- src/story_protocol_python_sdk/resources/Group.py | 5 +++++ src/story_protocol_python_sdk/resources/IPAsset.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 1789f8b3..938ff927 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -561,6 +561,11 @@ def claim_rewards( if not self.web3.is_address(ip_id): raise ValueError(f"Invalid member IP ID: {ip_id}") + # Convert addresses to checksum format for consistency + group_ip_id = self.web3.to_checksum_address(group_ip_id) + currency_token = self.web3.to_checksum_address(currency_token) + member_ip_ids = [self.web3.to_checksum_address(ip_id) for ip_id in member_ip_ids] + claim_reward_param = { "groupIpId": group_ip_id, "token": currency_token, diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index cee26cf2..c7042a1f 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -200,6 +200,8 @@ def register( try: ip_id = self._get_ip_id(nft_contract, token_id) if self.is_registered(ip_id): + # Ensure ip_id is in checksum format when returning + ip_id = self.web3.to_checksum_address(ip_id) return {"tx_hash": None, "ip_id": ip_id} req_object: dict = { @@ -2148,6 +2150,8 @@ def is_registered(self, ip_id: str) -> bool: if not self.web3.is_address(ip_id): raise ValueError(f"is_registered: invalid IP ID address format: {ip_id}") + # Convert to checksum address format (same as TypeScript SDK's validateAddress) + ip_id = self.web3.to_checksum_address(ip_id) return self.ip_asset_registry_client.isRegistered(ip_id) def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> list[RegisteredIP]: From fc959f88e6b0a4eac1fbde3b1775f101e039afc2 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 10:07:13 +0800 Subject: [PATCH 05/10] fix: add checksum address conversion in is_registered method Co-authored-by: Cursor --- src/story_protocol_python_sdk/resources/IPAsset.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index cee26cf2..74ffc70e 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -200,6 +200,8 @@ def register( try: ip_id = self._get_ip_id(nft_contract, token_id) if self.is_registered(ip_id): + # Ensure ip_id is in checksum format when returning + ip_id = self.web3.to_checksum_address(ip_id) return {"tx_hash": None, "ip_id": ip_id} req_object: dict = { @@ -2148,6 +2150,8 @@ def is_registered(self, ip_id: str) -> bool: if not self.web3.is_address(ip_id): raise ValueError(f"is_registered: invalid IP ID address format: {ip_id}") + # Convert to checksum address format for consistency + ip_id = self.web3.to_checksum_address(ip_id) return self.ip_asset_registry_client.isRegistered(ip_id) def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> list[RegisteredIP]: From 0f189378aae0f2c151dd7f65e0dda3b72f48a152 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 10:17:31 +0800 Subject: [PATCH 06/10] fix: update test_register_already_registered to expect checksum format ip_id Co-authored-by: Cursor --- tests/unit/resources/test_ip_asset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index e1a6467e..bf95ecfc 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -156,7 +156,9 @@ def test_register_already_registered( ): with mock_get_ip_id(), mock_is_registered(True): response = ip_asset.register(ADDRESS, 3) - assert response["ip_id"] == IP_ID + # IP_ID will be converted to checksum format in register method + expected_ip_id = ip_asset.web3.to_checksum_address(IP_ID) + assert response["ip_id"] == expected_ip_id assert response["tx_hash"] is None def test_register_successful( From 50c27d77aaa44e02c43ffcbc4dd7a45f5e0f9e33 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 14:43:50 +0800 Subject: [PATCH 07/10] fix: reduce mint_license_tokens amount to 10 in test to avoid RPC gas limits - test_integration_group: change amount from 100 to 10 mintLicenseTokens gas scales with amount; amount>=100 exceeds RPC node gas cap, causing estimate_gas to fail. amount=10 keeps gas ~2-3M. - IPAsset: add to_checksum_address in _parse_tx_ip_registered_event - Group: add checksum validation in claim_rewards Co-authored-by: Cursor --- src/story_protocol_python_sdk/resources/Group.py | 4 ++++ src/story_protocol_python_sdk/resources/IPAsset.py | 2 +- tests/integration/test_integration_group.py | 8 +++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 938ff927..5f023902 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -404,6 +404,10 @@ def register_group_and_attach_license_and_add_ips( :return dict: A dictionary with the transaction hash and group ID. """ try: + # Convert addresses to checksum format for consistency + group_pool = self.web3.to_checksum_address(group_pool) + ip_ids = [self.web3.to_checksum_address(ip_id) for ip_id in ip_ids] + if not self.web3.is_address(group_pool): raise ValueError(f'Group pool address "{group_pool}" is invalid.') diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index c7042a1f..54caf6c2 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -2172,7 +2172,7 @@ def _parse_tx_ip_registered_event(self, tx_receipt: dict) -> list[RegisteredIP]: ) registered_ips.append( RegisteredIP( - ip_id=event_result["args"]["ipId"], + ip_id=self.web3.to_checksum_address(event_result["args"]["ipId"]), token_id=event_result["args"]["tokenId"], ) ) diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index 6f8f3001..e8729e05 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -227,11 +227,17 @@ def test_claim_reward(self, story_client: StoryClient, nft_collection: Address): ) # Mint license tokens to the IP id which doesn't have a royalty vault + # Note: Using amount=10 instead of 100 because: + # - mintLicenseTokens is a batch operation that mints multiple tokens in one transaction + # - Gas consumption increases linearly with amount (amount=1: ~1M, amount=50: ~12M, amount=100: ~24M+) + # - When amount >= 100, gas estimation exceeds RPC node limits (16M), causing estimate_gas to fail + # - RPC nodes have a gas limit cap (typically 16M-30M), and transactions exceeding this cap will fail + # - Using amount=10 keeps gas consumption reasonable (~2-3M) and avoids RPC node limits story_client.License.mint_license_tokens( licensor_ip_id=ip_id, license_template=PIL_LICENSE_TEMPLATE, license_terms_id=license_terms_id, - amount=100, + amount=10, # Reduced from 100 to avoid RPC gas estimation limits receiver=ip_id, max_minting_fee=1, max_revenue_share=100, From 00664d8be31f61075e32bc83aad720ef665a3488 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 14:51:38 +0800 Subject: [PATCH 08/10] revert: remove unnecessary Group.py checksum changes Co-authored-by: Cursor --- src/story_protocol_python_sdk/resources/Group.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/Group.py b/src/story_protocol_python_sdk/resources/Group.py index 5f023902..1789f8b3 100644 --- a/src/story_protocol_python_sdk/resources/Group.py +++ b/src/story_protocol_python_sdk/resources/Group.py @@ -404,10 +404,6 @@ def register_group_and_attach_license_and_add_ips( :return dict: A dictionary with the transaction hash and group ID. """ try: - # Convert addresses to checksum format for consistency - group_pool = self.web3.to_checksum_address(group_pool) - ip_ids = [self.web3.to_checksum_address(ip_id) for ip_id in ip_ids] - if not self.web3.is_address(group_pool): raise ValueError(f'Group pool address "{group_pool}" is invalid.') @@ -565,11 +561,6 @@ def claim_rewards( if not self.web3.is_address(ip_id): raise ValueError(f"Invalid member IP ID: {ip_id}") - # Convert addresses to checksum format for consistency - group_ip_id = self.web3.to_checksum_address(group_ip_id) - currency_token = self.web3.to_checksum_address(currency_token) - member_ip_ids = [self.web3.to_checksum_address(ip_id) for ip_id in member_ip_ids] - claim_reward_param = { "groupIpId": group_ip_id, "token": currency_token, From 12cf3ce4fc7982e26da090204cd682e96691fb26 Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Tue, 24 Feb 2026 14:57:22 +0800 Subject: [PATCH 09/10] revert: remove unnecessary to_checksum_address in register when IP already registered --- src/story_protocol_python_sdk/resources/IPAsset.py | 2 -- tests/unit/resources/test_ip_asset.py | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 54caf6c2..5066ad28 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -200,8 +200,6 @@ def register( try: ip_id = self._get_ip_id(nft_contract, token_id) if self.is_registered(ip_id): - # Ensure ip_id is in checksum format when returning - ip_id = self.web3.to_checksum_address(ip_id) return {"tx_hash": None, "ip_id": ip_id} req_object: dict = { diff --git a/tests/unit/resources/test_ip_asset.py b/tests/unit/resources/test_ip_asset.py index bf95ecfc..e1a6467e 100644 --- a/tests/unit/resources/test_ip_asset.py +++ b/tests/unit/resources/test_ip_asset.py @@ -156,9 +156,7 @@ def test_register_already_registered( ): with mock_get_ip_id(), mock_is_registered(True): response = ip_asset.register(ADDRESS, 3) - # IP_ID will be converted to checksum format in register method - expected_ip_id = ip_asset.web3.to_checksum_address(IP_ID) - assert response["ip_id"] == expected_ip_id + assert response["ip_id"] == IP_ID assert response["tx_hash"] is None def test_register_successful( From 4ae5393aec82dbd5f04e545f3f48b0b7212fb2aa Mon Sep 17 00:00:00 2001 From: chao-peng-story Date: Wed, 25 Feb 2026 11:38:14 +0800 Subject: [PATCH 10/10] chore: update CODEOWNERS and add debug print in test_claim_reward --- .github/CODEOWNERS | 2 +- tests/integration/test_integration_group.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index eedd37c9..66b29b57 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @sebsadface @edisonz0718 @bonnie57 @lucas2brh @AndyBoWu @limengformal @roycezhaoca +* @roycezhaoca @lucas2brh @limengformal @yingyangxu2026 @chao-peng-story diff --git a/tests/integration/test_integration_group.py b/tests/integration/test_integration_group.py index e8729e05..b0a1d1ba 100644 --- a/tests/integration/test_integration_group.py +++ b/tests/integration/test_integration_group.py @@ -250,6 +250,8 @@ def test_claim_reward(self, story_client: StoryClient, nft_collection: Address): member_ip_ids=[ip_id], ) + print(result) + assert "tx_hash" in result assert isinstance(result["tx_hash"], str) assert "claimed_rewards" in result