diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 3572a4a646..ba6b887760 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ConflictReason` display implementation with an explanation of the conflict; - `Account::{burn(), consolidate_outputs(), create_alias_output(), create_native_token(), melt_native_token(), mint_native_token(), mint_nfts(), send_transaction(), send_native_tokens(), send_nft()}` methods; +- `Irc27Metadata` and `Irc30Metadata` helpers; - `Client::output_ids()` method; - `QueryParameter::unlockable_by_address` field; diff --git a/bindings/python/examples/client/10_mint_nft.py b/bindings/python/examples/client/10_mint_nft.py index c571c46584..a38e63aae2 100644 --- a/bindings/python/examples/client/10_mint_nft.py +++ b/bindings/python/examples/client/10_mint_nft.py @@ -4,7 +4,7 @@ from iota_sdk import (AddressUnlockCondition, Client, Ed25519Address, MetadataFeature, MnemonicSecretManager, Utils, - utf8_to_hex) + utf8_to_hex, Irc27Metadata) load_dotenv() @@ -18,6 +18,13 @@ secret_manager = MnemonicSecretManager(os.environ['MNEMONIC']) +metadata = Irc27Metadata( + "video/mp4", + "https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT", + "Shimmer OG NFT", + description="The original Shimmer NFT" +) + nft_output = client.build_nft_output( unlock_conditions=[ AddressUnlockCondition( @@ -28,7 +35,7 @@ nft_id='0x0000000000000000000000000000000000000000000000000000000000000000', amount=1000000, immutable_features=[ - MetadataFeature(utf8_to_hex('Hello, World!')) + metadata.as_feature() ], features=[ MetadataFeature(utf8_to_hex('Hello, World!')) diff --git a/bindings/python/examples/client/build_nft.py b/bindings/python/examples/client/build_nft.py index e0f3cb631e..bf74c2c1db 100644 --- a/bindings/python/examples/client/build_nft.py +++ b/bindings/python/examples/client/build_nft.py @@ -3,9 +3,18 @@ from dotenv import load_dotenv -from iota_sdk import (AddressUnlockCondition, Client, Ed25519Address, - IssuerFeature, MetadataFeature, SenderFeature, - TagFeature, Utils, utf8_to_hex) +from iota_sdk import ( + AddressUnlockCondition, + Client, + Ed25519Address, + IssuerFeature, + MetadataFeature, + SenderFeature, + TagFeature, + Utils, + utf8_to_hex, + Irc27Metadata, +) load_dotenv() @@ -17,15 +26,11 @@ hexAddress = Utils.bech32_to_hex( 'rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy') -# IOTA NFT Standard - IRC27: -# https://github.com/iotaledger/tips/blob/main/tips/TIP-0027/tip-0027.md -tip_27_immutable_metadata = { - "standard": "IRC27", - "version": "v1.0", - "type": "image/jpeg", - "uri": "https://mywebsite.com/my-nft-files-1.jpeg", - "name": "My NFT #0001" -} +tip_27_immutable_metadata = Irc27Metadata( + "image/jpeg", + "https://mywebsite.com/my-nft-files-1.jpeg", + "My NFT #0001", +) # Build NFT output nft_output = client.build_nft_output( @@ -36,8 +41,7 @@ nft_id='0x0000000000000000000000000000000000000000000000000000000000000000', immutable_features=[ IssuerFeature(Ed25519Address(hexAddress)), - MetadataFeature(utf8_to_hex(json.dumps( - tip_27_immutable_metadata, separators=(',', ':')))) + tip_27_immutable_metadata.as_feature() ], features=[ SenderFeature(Ed25519Address(hexAddress)), diff --git a/bindings/python/examples/how_tos/native_tokens/create.py b/bindings/python/examples/how_tos/native_tokens/create.py index d04c55d07d..964057a9b9 100644 --- a/bindings/python/examples/how_tos/native_tokens/create.py +++ b/bindings/python/examples/how_tos/native_tokens/create.py @@ -2,7 +2,7 @@ from dotenv import load_dotenv -from iota_sdk import CreateNativeTokenParams, Wallet, utf8_to_hex +from iota_sdk import CreateNativeTokenParams, Wallet, Irc30Metadata load_dotenv() @@ -38,10 +38,14 @@ print('Preparing transaction to create native token...') +metadata = Irc30Metadata( + "My Native Token", "MNT", 10, description="A native token to test the iota-sdk." +) + params = CreateNativeTokenParams( 100, 100, - utf8_to_hex('Hello, World!'), + metadata.as_hex(), ) prepared_transaction = account.prepare_create_native_token(params, None) diff --git a/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py b/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py index 79e22b7008..4160daf2d1 100644 --- a/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py +++ b/bindings/python/examples/how_tos/nft_collection/01_mint_collection_nft.py @@ -1,10 +1,9 @@ -import json import os import sys from dotenv import load_dotenv -from iota_sdk import MintNftParams, Utils, Wallet, utf8_to_hex +from iota_sdk import MintNftParams, Utils, Wallet, Irc27Metadata load_dotenv() @@ -36,41 +35,43 @@ issuer = Utils.nft_id_to_bech32(issuer_nft_id, bech32_hrp) -def get_immutable_metadata(index: int, collection_id: str) -> str: +def get_immutable_metadata(index: int) -> str: """Returns the immutable metadata for the NFT with the given index""" - data = { - "standard": "IRC27", - "version": "v1.0", - "type": "video/mp4", - "uri": "ipfs://wrongcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5Ywrong", - "name": "Shimmer OG NFT #" + str(index), - "description": "The Shimmer OG NFT was handed out 1337 times by the IOTA Foundation to celebrate the official launch of the Shimmer Network.", - "issuerName": "IOTA Foundation", - "collectionId": collection_id, - "collectionName": "Shimmer OG" - } - return json.dumps(data, separators=(',', ':')) + Irc27Metadata( + "video/mp4", + "https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT", + "Shimmer OG NFT #" + str(index), + description="The Shimmer OG NFT was handed out 1337 times by the IOTA Foundation to celebrate the official launch of the Shimmer Network.", + issuerName="IOTA Foundation", + collectionName="Shimmer OG", + ).as_hex() # Create the metadata with another index for each -nft_mint_params = list(map(lambda index: MintNftParams( - immutableMetadata=utf8_to_hex( - get_immutable_metadata(index, issuer_nft_id)), - issuer=issuer -), range(NFT_COLLECTION_SIZE))) +nft_mint_params = list( + map( + lambda index: MintNftParams( + immutableMetadata=get_immutable_metadata(index), issuer=issuer + ), + range(NFT_COLLECTION_SIZE), + ) +) while nft_mint_params: - chunk, nft_mint_params = nft_mint_params[:NUM_NFTS_MINTED_PER_TRANSACTION], nft_mint_params[NUM_NFTS_MINTED_PER_TRANSACTION:] + chunk, nft_mint_params = ( + nft_mint_params[:NUM_NFTS_MINTED_PER_TRANSACTION], + nft_mint_params[NUM_NFTS_MINTED_PER_TRANSACTION:], + ) print( - f'Minting {len(chunk)} NFTs... ({NFT_COLLECTION_SIZE-len(nft_mint_params)}/{NFT_COLLECTION_SIZE})') + f'Minting {len(chunk)} NFTs... ({NFT_COLLECTION_SIZE-len(nft_mint_params)}/{NFT_COLLECTION_SIZE})' + ) transaction = account.mint_nfts(chunk) # Wait for transaction to get included block_id = account.retry_transaction_until_included( transaction.transactionId) - print( - f'Block sent: {os.environ["EXPLORER_URL"]}/block/{block_id}') + print(f'Block sent: {os.environ["EXPLORER_URL"]}/block/{block_id}') # Sync so the new outputs are available again for new transactions account.sync() diff --git a/bindings/python/iota_sdk/__init__.py b/bindings/python/iota_sdk/__init__.py index 8371099ee9..a443d691d7 100644 --- a/bindings/python/iota_sdk/__init__.py +++ b/bindings/python/iota_sdk/__init__.py @@ -19,6 +19,8 @@ from .types.common import * from .types.event import * from .types.feature import * +from .types.irc_27 import * +from .types.irc_30 import * from .types.filter_options import * from .types.input import * from .types.native_token import * diff --git a/bindings/python/iota_sdk/types/irc_27.py b/bindings/python/iota_sdk/types/irc_27.py new file mode 100644 index 0000000000..a589a7748a --- /dev/null +++ b/bindings/python/iota_sdk/types/irc_27.py @@ -0,0 +1,66 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +import json +from iota_sdk import utf8_to_hex, MetadataFeature +from dataclasses import dataclass, field +from typing import Optional, List, Any +from dacite import from_dict + + +@dataclass +class Attribute: + """An attribute which follows [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). + Attributes: + trait_type: The trait type. + value: The value of the specified Attribute. + display_type: The optional type used to display the Attribute. + """ + + trait_type: str + value: Any + display_type: Optional[str] = None + + +@dataclass +class Irc27Metadata: + """The IRC27 NFT standard schema. + Attributes: + standard: The metadata standard (IRC27). + version: The metadata spec version (v1.0). + type: The media type (MIME) of the asset. + Examples: + - Image files: `image/jpeg`, `image/png`, `image/gif`, etc. + - Video files: `video/x-msvideo` (avi), `video/mp4`, `video/mpeg`, etc. + - Audio files: `audio/mpeg`, `audio/wav`, etc. + - 3D Assets: `model/obj`, `model/u3d`, etc. + - Documents: `application/pdf`, `text/plain`, etc. + uri: URL pointing to the NFT file location. + name: The human-readable name of the native token. + collectionName: The human-readable collection name of the native token. + royalties: Royalty payment addresses mapped to the payout percentage. + issuerName: The human-readable name of the native token creator. + description: The human-readable description of the token. + attributes: Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). + """ + + standard: str = field(default="IRC27", init=False) + version: str = field(default="v1.0", init=False) + type: str + uri: str + name: str + collectionName: Optional[str] = None + royalties: dict[str, float] = field(default_factory=dict) + issuerName: Optional[str] = None + description: Optional[str] = None + attributes: List[Attribute] = field(default_factory=list) + + @staticmethod + def from_dict(metadata_dict: dict): + return from_dict(Irc27Metadata, metadata_dict) + + def as_hex(self): + utf8_to_hex(json.dumps(self.as_dict(), separators=(",", ":"))) + + def as_feature(self): + MetadataFeature(self.as_hex()) diff --git a/bindings/python/iota_sdk/types/irc_30.py b/bindings/python/iota_sdk/types/irc_30.py new file mode 100644 index 0000000000..ebb958fbdb --- /dev/null +++ b/bindings/python/iota_sdk/types/irc_30.py @@ -0,0 +1,43 @@ +# Copyright 2023 IOTA Stiftung +# SPDX-License-Identifier: Apache-2.0 + +import json +from iota_sdk.types.common import HexStr +from iota_sdk import utf8_to_hex, MetadataFeature +from dataclasses import dataclass, field +from typing import Optional +from dacite import from_dict + + +@dataclass +class Irc30Metadata: + """The IRC30 native token metadata standard schema. + Attributes: + standard: The metadata standard (IRC30). + name: The human-readable name of the native token. + symbol: The symbol/ticker of the token. + decimals: Number of decimals the token uses (divide the token amount by 10^decimals to get its user representation). + description: The human-readable description of the token. + url: URL pointing to more resources about the token. + logoUrl: URL pointing to an image resource of the token logo. + logo: The svg logo of the token encoded as a byte string. + """ + + standard: str = field(default="IRC30", init=False) + name: str + symbol: str + decimals: int + description: Optional[str] = None + url: Optional[str] = None + logoUrl: Optional[str] = None + logo: Optional[HexStr] = None + + @staticmethod + def from_dict(metadata_dict: dict): + return from_dict(Irc30Metadata, metadata_dict) + + def as_hex(self): + utf8_to_hex(json.dumps(self.as_dict(), separators=(",", ":"))) + + def as_feature(self): + MetadataFeature(self.as_hex()) diff --git a/bindings/python/tests/test_offline.py b/bindings/python/tests/test_offline.py index 9cb35d1ac7..0e7b2601ab 100644 --- a/bindings/python/tests/test_offline.py +++ b/bindings/python/tests/test_offline.py @@ -2,7 +2,7 @@ # Copyright 2023 IOTA Stiftung # SPDX-License-Identifier: Apache-2.0 -from iota_sdk import Block, Client, MnemonicSecretManager, Utils, SecretManager, OutputId, hex_to_utf8, utf8_to_hex, Bip44, CoinType +from iota_sdk import Block, Client, MnemonicSecretManager, Utils, SecretManager, OutputId, hex_to_utf8, utf8_to_hex, Bip44, CoinType, Irc27Metadata, Irc30Metadata import json import unittest @@ -102,3 +102,49 @@ def test_block(): "0xd76cdb7acf228ecdad590a42b91acc077c1518c1a271411229e33e050fc19b44", "0xecef38d3af7e63da78a5e70128efe371f2191088b31879f7b0e81da657fa21c6"], "payload": {"type": 5, "tag": "0x68656c6c6f", "data": "0x68656c6c6f"}, "nonce": "6917529027641139843"} block = Block.from_dict(block_dict) assert block.id() == "0x7ce5ad074d4162e57f83cfa01cd2303ef5356567027ce0bcee0c9f57bc11656e" + + +def test_irc_27(): + metadata = Irc27Metadata( + "video/mp4", + "https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT", + "Shimmer OG NFT", + description="The original Shimmer NFT" + ) + metadata_dict = { + "standard": "IRC27", + "version": metadata.version, + "type": metadata.type, + "uri": metadata.uri, + "name": metadata.name, + "collectionName": metadata.collectionName, + "royalties": metadata.royalties, + "issuerName": metadata.issuerName, + "description": metadata.description, + "attributes": metadata.attributes + } + metadata_deser = Irc27Metadata.from_dict(metadata_dict) + assert metadata == metadata_deser + + +def test_irc_30(): + metadata = Irc30Metadata( + "FooCoin", + "FOO", + 3, + description="FooCoin is the utility and governance token of FooLand, \ + a revolutionary protocol in the play-to-earn crypto gaming field.", + url="https://foocoin.io/", + logoUrl="https://ipfs.io/ipfs/QmR36VFfo1hH2RAwVs4zVJ5btkopGip5cW7ydY4jUQBrkR" + ) + metadata_dict = { + "standard": "IRC30", + "name": metadata.name, + "description": metadata.description, + "decimals": metadata.decimals, + "symbol": metadata.symbol, + "url": metadata.url, + "logoUrl": metadata.logoUrl + } + metadata_deser = Irc30Metadata.from_dict(metadata_dict) + assert metadata == metadata_deser