diff --git a/bindings/nodejs/CHANGELOG.md b/bindings/nodejs/CHANGELOG.md index 9ac68c83a0..dc00ee7e7b 100644 --- a/bindings/nodejs/CHANGELOG.md +++ b/bindings/nodejs/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Account::{burn(), consolidateOutputs(), createAliasOutput(), meltNativeToken(), mintNativeToken(), createNativeToken(), mintNfts(), sendTransaction(), sendNativeTokens(), sendNft()}` methods; - `Client::outputIds()` method; - `GenericQueryParameter, UnlockableByAddress` types; +- `Irc27Metadata` and `Irc30Metadata` helpers; - `Utils::outputHexBytes`; ## 1.0.11 - 2023-09-14 diff --git a/bindings/nodejs/examples/client/15-build-nft-output.ts b/bindings/nodejs/examples/client/15-build-nft-output.ts index 000a594d06..161ce1f9d1 100644 --- a/bindings/nodejs/examples/client/15-build-nft-output.ts +++ b/bindings/nodejs/examples/client/15-build-nft-output.ts @@ -12,6 +12,7 @@ import { SenderFeature, Ed25519Address, IssuerFeature, + Irc27Metadata, } from '@iota/sdk'; require('dotenv').config({ path: '.env' }); @@ -35,14 +36,11 @@ async function run() { 'rms1qpllaj0pyveqfkwxmnngz2c488hfdtmfrj3wfkgxtk4gtyrax0jaxzt70zy', ); - // IOTA NFT Standard - IRC27: https://github.com/iotaledger/tips/blob/main/tips/TIP-0027/tip-0027.md - const tip27ImmutableMetadata = { - standard: 'IRC27', - version: 'v1.0', - type: 'image/jpeg', - uri: 'https://mywebsite.com/my-nft-files-1.jpeg', - name: 'My NFT #0001', - }; + const tip27ImmutableMetadata = new Irc27Metadata( + 'image/jpeg', + 'https://mywebsite.com/my-nft-files-1.jpeg', + 'My NFT #0001', + ); const nftOutput = await client.buildNftOutput({ // NftId needs to be null the first time @@ -52,9 +50,7 @@ async function run() { ], immutableFeatures: [ new IssuerFeature(new Ed25519Address(hexAddress)), - new MetadataFeature( - utf8ToHex(JSON.stringify(tip27ImmutableMetadata)), - ), + tip27ImmutableMetadata.asFeature(), ], features: [ new SenderFeature(new Ed25519Address(hexAddress)), diff --git a/bindings/nodejs/examples/how_tos/native_tokens/create.ts b/bindings/nodejs/examples/how_tos/native_tokens/create.ts index 3c2ae2d643..677020444e 100644 --- a/bindings/nodejs/examples/how_tos/native_tokens/create.ts +++ b/bindings/nodejs/examples/how_tos/native_tokens/create.ts @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { CreateNativeTokenParams, utf8ToHex } from '@iota/sdk'; +import { CreateNativeTokenParams, Irc30Metadata } from '@iota/sdk'; import { getUnlockedWallet } from '../../wallet/common'; @@ -51,11 +51,17 @@ async function run() { console.log('Preparing transaction to create native token...'); + const metadata = new Irc30Metadata( + 'My Native Token', + 'MNT', + 10, + ).withDescription('A native token to test the iota-sdk.'); + // If we omit the AccountAddress field the first address of the account is used by default const params: CreateNativeTokenParams = { circulatingSupply: CIRCULATING_SUPPLY, maximumSupply: MAXIMUM_SUPPLY, - foundryMetadata: utf8ToHex('Hello, World!'), + foundryMetadata: metadata.asHex(), }; const prepared = await account.prepareCreateNativeToken(params); diff --git a/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts b/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts index 686e62c146..c78aeba3b7 100644 --- a/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts +++ b/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { MintNftParams, NftId, utf8ToHex, Utils, Wallet } from '@iota/sdk'; +import { MintNftParams, NftId, Utils, Wallet, Irc27Metadata } from '@iota/sdk'; require('dotenv').config({ path: '.env' }); // The NFT collection size @@ -48,9 +48,7 @@ async function run() { // Create the metadata with another index for each for (let index = 0; index < NFT_COLLECTION_SIZE; index++) { const params: MintNftParams = { - immutableMetadata: utf8ToHex( - getImmutableMetadata(index, issuerNftId), - ), + immutableMetadata: getImmutableMetadata(index).asHex(), // The NFT address from the NFT we minted in mint_issuer_nft example issuer, }; @@ -97,21 +95,18 @@ async function run() { process.exit(0); } -function getImmutableMetadata(index: number, issuerNftId: NftId) { - // Note: we use parse and stringify to remove all unnecessary whitespace - return JSON.stringify( - JSON.parse(`{ - "standard":"IRC27", - "version":"v1.0", - "type":"video/mp4", - "uri":"ipfs://wrongcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5Ywrong", - "name":"Shimmer OG NFT ${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":"${issuerNftId}", - "collectionName":"Shimmer OG" - }`), - ); +function getImmutableMetadata(index: number) { + return new Irc27Metadata( + 'video/mp4', + 'https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT', + `Shimmer OG NFT ${index}`, + ) + .withDescription( + 'The Shimmer OG NFT was handed out 1337 times by the IOTA Foundation \ + to celebrate the official launch of the Shimmer Network.', + ) + .withIssuerName('IOTA Foundation') + .withCollectionName('Shimmer OG'); } run(); diff --git a/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts b/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts index 889e443e5e..d8853a5dca 100644 --- a/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts +++ b/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts @@ -10,6 +10,7 @@ import { utf8ToHex, Utils, Wallet, + Irc27Metadata, } from '@iota/sdk'; require('dotenv').config({ path: '.env' }); @@ -18,8 +19,6 @@ const NFT1_OWNER_ADDRESS = 'rms1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluaw60xu'; // The metadata of the first minted NFT const NFT1_METADATA = utf8ToHex('some NFT metadata'); -// The immutable metadata of the first minted NFT -const NFT1_IMMUTABLE_METADATA = utf8ToHex('some NFT immutable metadata'); // The tag of the first minted NFT const NFT1_TAG = utf8ToHex('some NFT tag'); // The base coin amount we sent with the second NFT @@ -52,13 +51,19 @@ async function run() { // We need to unlock stronghold. await wallet.setStrongholdPassword(process.env.STRONGHOLD_PASSWORD); + const metadata = new Irc27Metadata( + 'video/mp4', + 'https://ipfs.io/ipfs/QmPoYcVm9fx47YXNTkhpMEYSxCD3Bqh7PJYr7eo5YjLgiT', + 'Shimmer OG NFT', + ).withDescription('The original Shimmer NFT'); + const params: MintNftParams = { address: NFT1_OWNER_ADDRESS, // Remove or change to senderAddress to send to self sender: senderAddress, metadata: NFT1_METADATA, tag: NFT1_TAG, issuer: senderAddress, - immutableMetadata: NFT1_IMMUTABLE_METADATA, + immutableMetadata: metadata.asHex(), }; let transaction = await account.mintNfts([params]); console.log(`Transaction sent: ${transaction.transactionId}`); diff --git a/bindings/nodejs/lib/types/block/output/index.ts b/bindings/nodejs/lib/types/block/output/index.ts index fc1d95a381..3c03d98d44 100644 --- a/bindings/nodejs/lib/types/block/output/index.ts +++ b/bindings/nodejs/lib/types/block/output/index.ts @@ -5,3 +5,5 @@ export * from './feature'; export * from './unlock-condition'; export * from './output'; export * from './token-scheme'; +export * from './irc-27'; +export * from './irc-30'; diff --git a/bindings/nodejs/lib/types/block/output/irc-27.ts b/bindings/nodejs/lib/types/block/output/irc-27.ts new file mode 100644 index 0000000000..bd58ef0df1 --- /dev/null +++ b/bindings/nodejs/lib/types/block/output/irc-27.ts @@ -0,0 +1,111 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { utf8ToHex } from '../../../utils'; +import { MetadataFeature } from './feature'; + +/** + * The IRC27 NFT standard schema. + */ +class Irc27Metadata { + /** The IRC standard */ + readonly standard: string = 'IRC27'; + /** The current version. */ + readonly version: string = 'v1.0'; + /** 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. + */ + type: string; + /** URL pointing to the NFT file location. */ + uri: string; + /** The human-readable name of the native token. */ + name: string; + /** The human-readable collection name of the native token. */ + collectionName?: string; + /** Royalty payment addresses mapped to the payout percentage. */ + royalties: Map = new Map(); + /** The human-readable name of the native token creator. */ + issuerName?: string; + /** The human-readable description of the token. */ + description?: string; + /** Additional attributes which follow [OpenSea Metadata standards](https://docs.opensea.io/docs/metadata-standards). */ + attributes: Attribute[] = []; + + /** + * @param type The media type (MIME) of the asset. + * @param uri URL pointing to the NFT file location. + * @param name The human-readable name of the native token. + */ + constructor(type: string, uri: string, name: string) { + this.type = type; + this.uri = uri; + this.name = name; + } + + withCollectionName(collectionName: string): Irc27Metadata { + this.collectionName = collectionName; + return this; + } + + addRoyalty(address: string, percentage: number): Irc27Metadata { + this.royalties.set(address, percentage); + return this; + } + + withRoyalties(royalties: Map): Irc27Metadata { + this.royalties = royalties; + return this; + } + + withIssuerName(issuerName: string): Irc27Metadata { + this.issuerName = issuerName; + return this; + } + + withDescription(description: string): Irc27Metadata { + this.description = description; + return this; + } + + addAttribute(attribute: Attribute): Irc27Metadata { + this.attributes.push(attribute); + return this; + } + + withAttributes(attributes: Attribute[]): Irc27Metadata { + this.attributes = attributes; + return this; + } + + asHex(): string { + return utf8ToHex(JSON.stringify(this)); + } + + asFeature(): MetadataFeature { + return new MetadataFeature(this.asHex()); + } +} + +class Attribute { + trait_type: string; + value: any; + display_type?: string; + + constructor(trait_type: string, value: any) { + this.trait_type = trait_type; + this.value = value; + } + + withDisplayType(display_type: string): Attribute { + this.display_type = display_type; + return this; + } +} + +export { Irc27Metadata, Attribute }; diff --git a/bindings/nodejs/lib/types/block/output/irc-30.ts b/bindings/nodejs/lib/types/block/output/irc-30.ts new file mode 100644 index 0000000000..738de841a1 --- /dev/null +++ b/bindings/nodejs/lib/types/block/output/irc-30.ts @@ -0,0 +1,68 @@ +// Copyright 2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { utf8ToHex } from '../../../utils'; +import { MetadataFeature } from './feature'; + +/** + * The IRC30 native token metadata standard schema. + */ +class Irc30Metadata { + /** The IRC standard */ + readonly standard: string = 'IRC30'; + /** The human-readable name of the native token. */ + name: string; + /** The symbol/ticker of the token. */ + symbol: string; + /** Number of decimals the token uses (divide the token amount by `10^decimals` to get its user representation). */ + decimals: number; + /** The human-readable description of the token. */ + description?: string; + /** URL pointing to more resources about the token. */ + url?: string; + /** URL pointing to an image resource of the token logo. */ + logoUrl?: string; + /** The svg logo of the token encoded as a byte string. */ + logo?: string; + + /** + * @param name The human-readable name of the native token. + * @param symbol The symbol/ticker of the token. + * @param decimals Number of decimals the token uses. + */ + constructor(name: string, symbol: string, decimals: number) { + this.name = name; + this.symbol = symbol; + this.decimals = decimals; + } + + withDescription(description: string): Irc30Metadata { + this.description = description; + return this; + } + + withUrl(url: string): Irc30Metadata { + this.url = url; + return this; + } + + withLogoUrl(logoUrl: string): Irc30Metadata { + this.logoUrl = logoUrl; + return this; + } + + withLogo(logo: string): Irc30Metadata { + this.logo = logo; + return this; + } + + asHex(): string { + return utf8ToHex(JSON.stringify(this)); + } + + asFeature(): MetadataFeature { + return new MetadataFeature(this.asHex()); + } +} + +export { Irc30Metadata }; diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 6c0a28cc01..a29478f436 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - UX improvements (Ctrl+l, TAB completion/suggestions and more) during interactive account management; - `WalletCommand::SetPow` command; - Check for existing stronghold on `restore`; +- Sync native token foundries to show their metadata; ### Changed diff --git a/cli/src/command/account.rs b/cli/src/command/account.rs index 66c869cd73..f5754104f6 100644 --- a/cli/src/command/account.rs +++ b/cli/src/command/account.rs @@ -21,7 +21,7 @@ use iota_sdk::{ wallet::{ account::{ types::{AccountAddress, AccountIdentifier}, - Account, ConsolidationParams, OutputsToClaim, TransactionOptions, + Account, ConsolidationParams, OutputsToClaim, SyncOptions, TransactionOptions, }, CreateNativeTokenParams, MintNftParams, SendNativeTokensParams, SendNftParams, SendParams, }, @@ -768,7 +768,12 @@ pub async fn send_nft_command( // `sync` command pub async fn sync_command(account: &Account) -> Result<(), Error> { - let balance = account.sync(None).await?; + let balance = account + .sync(Some(SyncOptions { + sync_native_token_foundries: true, + ..Default::default() + })) + .await?; println_log_info!("Synced."); println_log_info!("{balance:#?}"); @@ -911,7 +916,7 @@ pub async fn voting_output_command(account: &Account) -> Result<(), Error> { async fn print_address(account: &Account, address: &AccountAddress) -> Result<(), Error> { let mut log = format!( - "Address {}:\n {:<10}{}\n {:<10}{:?}", + "Address: {}\n{:<9}{}\n{:<9}{:?}", address.key_index(), "Bech32:", address.address(), @@ -974,7 +979,7 @@ async fn print_address(account: &Account, address: &AccountAddress) -> Result<() } log = format!( - "{log}\n Outputs: {:#?}\n Base coin amount: {}\n Native Tokens: {:?}\n NFTs: {:?}\n Aliases: {:?}\n Foundries: {:?}\n", + "{log}\nOutputs: {:#?}\nBase coin amount: {}\nNative Tokens: {:#?}\nNFTs: {:#?}\nAliases: {:#?}\nFoundries: {:#?}\n", output_ids, amount, native_tokens.finish_vec()?, diff --git a/sdk/src/types/block/output/feature/metadata.rs b/sdk/src/types/block/output/feature/metadata.rs index eaebb7f72f..ee2cfab4e3 100644 --- a/sdk/src/types/block/output/feature/metadata.rs +++ b/sdk/src/types/block/output/feature/metadata.rs @@ -312,7 +312,7 @@ pub(crate) mod irc_30 { use super::*; - /// The IRC30 NFT standard schema. + /// The IRC30 native token metadata standard schema. #[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[serde(tag = "standard", rename = "IRC30")] @@ -322,7 +322,8 @@ pub(crate) mod irc_30 { name: String, /// The symbol/ticker of the token. symbol: String, - /// Number of decimals the token uses (divide the token amount by 10^decimals to get its user representation). + /// Number of decimals the token uses (divide the token amount by `10^decimals` to get its user + /// representation). decimals: u32, /// The human-readable description of the token. #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/sdk/src/wallet/account/types/balance.rs b/sdk/src/wallet/account/types/balance.rs index 16571f0235..71b9eafe7f 100644 --- a/sdk/src/wallet/account/types/balance.rs +++ b/sdk/src/wallet/account/types/balance.rs @@ -21,12 +21,12 @@ pub struct Balance { pub(crate) required_storage_deposit: RequiredStorageDeposit, /// Native tokens pub(crate) native_tokens: Vec, - /// Nfts - pub(crate) nfts: Vec, /// Aliases pub(crate) aliases: Vec, /// Foundries pub(crate) foundries: Vec, + /// Nfts + pub(crate) nfts: Vec, /// Outputs with multiple unlock conditions and if they can currently be spent or not. If there is a /// [`TimelockUnlockCondition`](crate::types::block::output::unlock_condition::TimelockUnlockCondition) or /// [`ExpirationUnlockCondition`](crate::types::block::output::unlock_condition::ExpirationUnlockCondition) this @@ -51,9 +51,9 @@ impl std::ops::AddAssign for Balance { } } - self.nfts.extend(rhs.nfts); self.aliases.extend(rhs.aliases); self.foundries.extend(rhs.foundries); + self.nfts.extend(rhs.nfts); } } @@ -88,11 +88,11 @@ impl std::ops::AddAssign for BaseCoinBalance { #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, CopyGetters)] #[getset(get_copy = "pub")] pub struct RequiredStorageDeposit { - #[serde(with = "crate::utils::serde::string")] - pub(crate) alias: u64, #[serde(with = "crate::utils::serde::string")] pub(crate) basic: u64, #[serde(with = "crate::utils::serde::string")] + pub(crate) alias: u64, + #[serde(with = "crate::utils::serde::string")] pub(crate) foundry: u64, #[serde(with = "crate::utils::serde::string")] pub(crate) nft: u64, @@ -100,8 +100,8 @@ pub struct RequiredStorageDeposit { impl std::ops::AddAssign for RequiredStorageDeposit { fn add_assign(&mut self, rhs: Self) { - self.alias += rhs.alias; self.basic += rhs.basic; + self.alias += rhs.alias; self.foundry += rhs.foundry; self.nft += rhs.nft; } @@ -205,8 +205,8 @@ impl Balance { voting_power: total / 4, }, required_storage_deposit: RequiredStorageDeposit { - alias: total / 16, basic: total / 8, + alias: total / 16, foundry: total / 4, nft: total / 2, },