From 0c65c3b5f3569467ebb6c3252441c8e5fc196d58 Mon Sep 17 00:00:00 2001 From: Andrew Chung <58543094+aandrewchung@users.noreply.github.com> Date: Wed, 26 Feb 2025 02:59:28 +0900 Subject: [PATCH 1/2] [UPDATE] New Release & Register() Metadata Bug Fix (#38) * Updated demo to aeneid * Fixed metadata camelcase bug * Updated readme to aeneid * manual upload of new ver to pypi --- README.md | 10 ++-- setup.py | 4 +- .../resources/IPAsset.py | 12 ++-- tests/demo/demo.py | 58 +++++++------------ tests/demo/demo_utils.py | 42 ++++++++------ .../integration/test_integration_ip_asset.py | 16 ++++- 6 files changed, 72 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index aa6c49e..a78c136 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Story Protocol SDK +# Story SDK -Welcome to the documents for Story Protocol Python SDK. The Python SDK provides the APIs for developers to build applications with Story Protocol. By using the SDK, developers can create the resources like IP assets and perform actions to interact with the resource. +Welcome to the documents for Story Python SDK. The Python SDK provides the APIs for developers to build applications with Story. By using the SDK, developers can create the resources like IP assets and perform actions to interact with the resource. ## How to use Story Protocol SDK in Your Project @@ -48,14 +48,14 @@ The preceding code created the `account` object for creating the SDK client. To set up the SDK client, import `StoryClient` from `story_protocol_python_sdk`. Write the following code, utilizing the `account` we created previously. -> :information-source: Make sure to have RPC_PROVIDER_URL for your desired chain set up in your .env file. We recommend using the Sepolia network with `RPC_PROVIDER_URL=https://rpc.ankr.com/eth_sepolia`. +> :information-source: Make sure to have RPC_PROVIDER_URL for your desired chain set up in your .env file. We recommend using the public default one with `RPC_PROVIDER_URL=https://aeneid.storyrpc.io`. ```Python main.py from story_protocol_python_sdk import StoryClient # Create StoryClient instance -odyssey_chain_id = 1516 -story_client = StoryClient(web3, account, odyssey_chain_id) +aeneid_chain_id = 1315 +story_client = StoryClient(web3, account, aeneid_chain_id) ``` ## Running test cases diff --git a/setup.py b/setup.py index 9b46a12..655e893 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='story_protocol_python_sdk', - version='0.3.5', + version='0.3.7', packages=find_packages(where='src', exclude=["tests"]), package_dir={'': 'src'}, install_requires=[ @@ -18,7 +18,7 @@ license='MIT', author='Andrew Chung', author_email='andrew@storyprotocol.xyz', - description='A Python SDK for interacting with the Story Protocol.', + description='A Python SDK for interacting with Story.', long_description=open('README.md').read(), long_description_content_type='text/markdown', classifiers=[ diff --git a/src/story_protocol_python_sdk/resources/IPAsset.py b/src/story_protocol_python_sdk/resources/IPAsset.py index 539a835..23f99a8 100644 --- a/src/story_protocol_python_sdk/resources/IPAsset.py +++ b/src/story_protocol_python_sdk/resources/IPAsset.py @@ -62,9 +62,9 @@ def register( :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 ip_metadata_URI str: [Optional] Metadata URI for the IP. + :param ip_metadata_uri str: [Optional] Metadata URI for the IP. :param ip_metadata_hash str: [Optional] Metadata hash for the IP. - :param nft_metadata_URI str: [Optional] Metadata URI for the NFT. + :param nft_metadata_uri str: [Optional] Metadata URI for the NFT. :param nft_metadata_hash str: [Optional] Metadata hash for the NFT. :param deadline int: [Optional] Signature deadline in milliseconds. :param tx_options dict: [Optional] Transaction options. @@ -96,10 +96,10 @@ def register( if ip_metadata: req_object['ipMetadata'].update({ - 'ipMetadataURI': ip_metadata.get('ipMetadataURI', ""), - 'ipMetadataHash': ip_metadata.get('ipMetadataHash', ZERO_HASH), - 'nftMetadataURI': ip_metadata.get('nftMetadataURI', ""), - 'nftMetadataHash': ip_metadata.get('nftMetadataHash', ZERO_HASH), + 'ipMetadataURI': ip_metadata.get('ip_metadata_uri', ""), + 'ipMetadataHash': ip_metadata.get('ip_metadata_hash', ZERO_HASH), + 'nftMetadataURI': ip_metadata.get('nft_metadata_uri', ""), + 'nftMetadataHash': ip_metadata.get('nft_metadata_hash', ZERO_HASH), }) calculated_deadline = self.sign_util.get_deadline(deadline=deadline) diff --git a/tests/demo/demo.py b/tests/demo/demo.py index 81015f3..4b08035 100644 --- a/tests/demo/demo.py +++ b/tests/demo/demo.py @@ -3,7 +3,7 @@ from web3 import Web3 from story_protocol_python_sdk import StoryClient -from demo_utils import get_token_id, MockERC721, MockERC20, mint_tokens +from demo_utils import get_token_id, MockERC721, MockERC20, mint_tokens, ROYALTY_POLICY, ROYALTY_MODULE, PIL_LICENSE_TEMPLATE def main(): # 1. Set up your Story Config @@ -11,6 +11,11 @@ def main(): private_key = os.getenv('WALLET_PRIVATE_KEY') rpc_url = os.getenv('RPC_PROVIDER_URL') + if not private_key: + raise ValueError("WALLET_PRIVATE_KEY environment variable is not set") + if not rpc_url: + raise ValueError("RPC_PROVIDER_URL environment variable is not set") + # Initialize Web3 web3 = Web3(Web3.HTTPProvider(rpc_url)) if not web3.is_connected(): @@ -20,26 +25,22 @@ def main(): account = web3.eth.account.from_key(private_key) # Create StoryClient instance - story_client = StoryClient(web3, account, 11155111) + story_client = StoryClient(web3, account, 1315) # 2. Register an IP Asset token_id = get_token_id(MockERC721, story_client.web3, story_client.account) + registered_ip_asset_response = story_client.IPAsset.register( - token_contract=MockERC721, + nft_contract=MockERC721, token_id=token_id ) print(f"Root IPA created at transaction hash {registered_ip_asset_response['txHash']}, IPA ID: {registered_ip_asset_response['ipId']}") # 3. Register PIL Terms - commercial_use_params = { - 'currency': MockERC20, - 'minting_fee': 2, - 'royalty_policy': "0xAAbaf349C7a2A84564F9CC4Ac130B3f19A718E86" - } register_pil_terms_response = story_client.License.registerCommercialUsePIL( - minting_fee=commercial_use_params['minting_fee'], - currency=commercial_use_params['currency'], - royalty_policy=commercial_use_params['royalty_policy'] + default_minting_fee=1, + currency=MockERC20, + royalty_policy=ROYALTY_POLICY ) if 'txHash' in register_pil_terms_response: print(f"PIL Terms registered at transaction hash {register_pil_terms_response['txHash']}, License Terms ID: {register_pil_terms_response['licenseTermsId']}") @@ -50,7 +51,7 @@ def main(): try: attach_license_terms_response = story_client.License.attachLicenseTerms( ip_id=registered_ip_asset_response['ipId'], - license_template="0x260B6CB6284c89dbE660c0004233f7bB99B5edE7", + license_template=PIL_LICENSE_TEMPLATE, license_terms_id=register_pil_terms_response['licenseTermsId'] ) print(f"Attached License Terms to IP at transaction hash {attach_license_terms_response['txHash']}") @@ -58,51 +59,34 @@ def main(): print(f"License Terms ID {register_pil_terms_response['licenseTermsId']} already attached to this IPA.") #Before you mint make sure you have enough ERC20 tokens according to the minting fee above - token_ids = mint_tokens(MockERC20, web3, account, account.address, 2) + token_ids = mint_tokens(MockERC20, web3, account, account.address, 10000) # 5. Mint License mint_license_response = story_client.License.mintLicenseTokens( licensor_ip_id=registered_ip_asset_response['ipId'], - license_template="0x260B6CB6284c89dbE660c0004233f7bB99B5edE7", + license_template=PIL_LICENSE_TEMPLATE, license_terms_id=register_pil_terms_response['licenseTermsId'], amount=1, - receiver=account.address + receiver=account.address, + max_minting_fee=1, + max_revenue_share=0 ) print(f"License Token minted at transaction hash {mint_license_response['txHash']}, License Token IDs: {mint_license_response['licenseTokenIds']}") # 6. Mint derivative IP Asset using your license derivative_token_id = get_token_id(MockERC721, story_client.web3, story_client.account) registered_ip_asset_derivative_response = story_client.IPAsset.register( - token_contract=MockERC721, + nft_contract=MockERC721, token_id=derivative_token_id ) print(f"Derivative IPA created at transaction hash {registered_ip_asset_derivative_response['txHash']}, IPA ID: {registered_ip_asset_derivative_response['ipId']}") link_derivative_response = story_client.IPAsset.registerDerivativeWithLicenseTokens( child_ip_id=registered_ip_asset_derivative_response['ipId'], - license_token_ids=mint_license_response['licenseTokenIds'] + license_token_ids=mint_license_response['licenseTokenIds'], + max_rts=5 * 10 ** 6 ) print(f"Derivative IPA linked to parent at transaction hash {link_derivative_response['txHash']}") - # 7. Collect Royalty Tokens - collect_royalty_tokens_response = story_client.Royalty.collectRoyaltyTokens( - parent_ip_id=registered_ip_asset_response['ipId'], - child_ip_id=registered_ip_asset_derivative_response['ipId'] - ) - print(f"Collected royalty token {collect_royalty_tokens_response['royaltyTokensCollected']} at transaction hash {collect_royalty_tokens_response['txHash']}") - - # 8. Claim Revenue - snapshot_response = story_client.Royalty.snapshot( - child_ip_id=registered_ip_asset_derivative_response['ipId'] - ) - print(f"Took a snapshot with ID {snapshot_response['snapshotId']} at transaction hash {snapshot_response['txHash']}") - - claim_revenue_response = story_client.Royalty.claimRevenue( - snapshot_ids=[snapshot_response['snapshotId']], - child_ip_id=registered_ip_asset_derivative_response['ipId'], - token=MockERC20 - ) - print(f"Claimed revenue token {claim_revenue_response['claimableToken']} at transaction hash {claim_revenue_response['txHash']}") - if __name__ == "__main__": main() diff --git a/tests/demo/demo_utils.py b/tests/demo/demo_utils.py index e284427..039254f 100644 --- a/tests/demo/demo_utils.py +++ b/tests/demo/demo_utils.py @@ -1,8 +1,13 @@ # Mock ERC721 contract address -MockERC721 = "0x7ee32b8B515dEE0Ba2F25f612A04a731eEc24F49" +MockERC721 = "0xa1119092ea911202E0a65B743a13AE28C5CF2f21" # Mock ERC20 contract address (same as used in TypeScript tests) -MockERC20 = "0xB132A6B7AE652c974EE1557A3521D53d18F6739f" +MockERC20 = "0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E" + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ROYALTY_POLICY="0xBe54FB168b3c982b7AaE60dB6CF75Bd8447b390E" #Royalty Policy LAP +ROYALTY_MODULE="0xD2f60c40fEbccf6311f8B47c4f2Ec6b040400086" +PIL_LICENSE_TEMPLATE="0x2E896b0b2Fdb7457499B56AAaA4AE55BCB4Cd316" def get_token_id(nft_contract, web3, account): contract_abi = [ @@ -15,23 +20,26 @@ def get_token_id(nft_contract, web3, account): } ] - # Fetch the current average gas price from the node plus 10% - current_gas_price = int(web3.eth.gas_price * 1.1) - contract = web3.eth.contract(address=nft_contract, abi=contract_abi) - transaction = contract.functions.mint(account.address).build_transaction({ - 'from': account.address, - 'nonce': web3.eth.get_transaction_count(account.address), - 'gas': 2000000, - 'gasPrice': current_gas_price - }) - signed_txn = account.sign_transaction(transaction) - tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) - tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) + + try: + transaction = contract.functions.mint(account.address).build_transaction({ + 'from': account.address, + 'nonce': web3.eth.get_transaction_count(account.address), + 'gas': 2000000 + }) + + signed_txn = account.sign_transaction(transaction) + tx_hash = web3.eth.send_raw_transaction(signed_txn.raw_transaction) + tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash) - logs = tx_receipt['logs'] - if logs[0]['topics'][3]: - return int(logs[0]['topics'][3].hex(), 16) + logs = tx_receipt['logs'] + if len(logs) > 0 and len(logs[0]['topics']) > 3: + return int(logs[0]['topics'][3].hex(), 16) + raise ValueError(f"No token ID in logs: {tx_receipt}") + + except Exception as e: + raise e def mint_tokens(erc20_contract_address, web3, account, to_address, amount): contract_abi = [ diff --git a/tests/integration/test_integration_ip_asset.py b/tests/integration/test_integration_ip_asset.py index b0e0854..4d899d5 100644 --- a/tests/integration/test_integration_ip_asset.py +++ b/tests/integration/test_integration_ip_asset.py @@ -44,9 +44,10 @@ def test_register_ip_asset(story_client, child_ip_id): def test_register_ip_asset_with_metadata(story_client): token_id = get_token_id(MockERC721, story_client.web3, story_client.account) metadata = { - 'metadataURI': "test-uri", - 'metadataHash': web3.to_hex(web3.keccak(text="test-metadata-hash")), - 'nftMetadataHash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) + 'ip_metadata_uri': "test-uri", + 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash")), + 'nft_metadata_uri': "test-nft-uri", + 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) } response = story_client.IPAsset.register( @@ -143,6 +144,14 @@ def nft_collection(story_client): return txData['nftContract'] def test_mint_register_attach_terms(story_client, nft_collection): + + metadata = { + 'ip_metadata_uri': "test-uri", + 'ip_metadata_hash': web3.to_hex(web3.keccak(text="test-metadata-hash")), + 'nft_metadata_uri': "test-nft-uri", + 'nft_metadata_hash': web3.to_hex(web3.keccak(text="test-nft-metadata-hash")) + } + response = story_client.IPAsset.mintAndRegisterIpAssetWithPilTerms( spg_nft_contract=nft_collection, terms=[{ @@ -176,6 +185,7 @@ def test_mint_register_attach_terms(story_client, nft_collection): 'expect_group_reward_pool': ZERO_ADDRESS } }], + ip_metadata=metadata ) assert 'txHash' in response From 3cdf32d8cc701f60524902bca73149065ac39c09 Mon Sep 17 00:00:00 2001 From: Andrew Chung <58543094+aandrewchung@users.noreply.github.com> Date: Fri, 28 Feb 2025 22:51:11 +0900 Subject: [PATCH 2/2] [UPDATE] Added mainnet support (#39) --- src/story_protocol_python_sdk/story_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/story_protocol_python_sdk/story_client.py b/src/story_protocol_python_sdk/story_client.py index 3560d27..49d2fd2 100644 --- a/src/story_protocol_python_sdk/story_client.py +++ b/src/story_protocol_python_sdk/story_client.py @@ -36,7 +36,7 @@ def __init__(self, web3, account, chain_id: int): if not web3 or not account: raise ValueError("web3 and account must be provided") - if chain_id != 1315: + if chain_id not in [1315, 1514]: raise ValueError("only support story devnet") self.web3 = web3