WEB3DEV Español

Hector
Hector

Posted on

Cómo acuñar NFT en Solana usando Rust y Metaplex

En este tutorial, aprenderás a cómo acuñar un NFT en Solana, escribiendo un contrato inteligente en Rust, usando la Metadata del token del programa de Metaplex.

Image description

Este artículo es una traducción de Anoushk Kharangate, hecha por Héctor Botero. Puedes encontrar el artículo original aquí

¡Bienvenidos lectores! Este es el comienzo de una nueva serie de publicaciones en el blog sobre el desarrollo de Solana y, en este, aprenderás a cómo escribir un contrato personalizado para acuñar tus NFT en tan sólo cuatro pasos.

Algunos consejos generales sobre el desarrollo en Solana

En el desarrollo de Solana, te encontrarás con muchos errores extraños y bugs, y puede ser muy difícil y frustrante repararlos ya que el ecosistema de Solana dev no es tan grande como el ecosistema de Eth dev. Pero, ¡no te preocupes! Cuando te atascas, simplemente tienes que ver en el lugar correcto para encontrar la solución.

Mientras desarrollaba, estaba constantemente preguntando mis dudas en el servidor de discord de Anchor, los servidores de Metaplex y Superteam y viendo otros repositorios de códigos en GitHub y en la misma biblioteca del programa de Metaplex.

Visión general del Proyecto

Las herramientas que estaremos usando son:

  1. Solana CLI Tools - El conjunto oficial de herramientas de Solana CLI
  2. Anchor Framework - Un framework de alto nivel para desarrollar programas de Solana. Esta herramienta es esencial a menos que seas un desarrollador nivel dios que, en ese caso, no estarías leyendo este blog.
  3. Solana/web3.js - Una versión web3.js de Solana.
  4. Solana/spl-token - Un paquete para trabajar con los tokens spl.
  5. Mocha - Una herramienta JS de prueba

Primeros Pasos

Trabajo Previo

Usa el CLI para configurar tu red en devnet con el siguiente comando:

solana config set --url devnet
Enter fullscreen mode Exit fullscreen mode

Para confirmar que funciona, revisa la salida luego de colocar el cmd:

Config File: /Users/anoushkkharangate/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/anoushkkharangate/.config/solana/id.json
Commitment: confirmed
Enter fullscreen mode Exit fullscreen mode

Luego, si no lo haz hecho aún, configura una cartera del sistema de archivos usando esta guía, Solana wallet docs, y también añade algunos devnet usando el comando solana airdrop 1

Por último, usa anchor CLI para hacer un proyecto anchor con este comando:

anchor init <name-of-your-project>
Enter fullscreen mode Exit fullscreen mode

Asegúrate que ´Anchor.toml´ también esté configurado en devnet

[features]

seeds = false

[programs.devnet]

metaplex_anchor_nft = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"

[registry]

url = "https://anchor.projectserum.com"

[provider]

cluster = "devnet"

wallet = "/Users/<user-name>/.config/solana/id.json"

[scripts]

test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

Enter fullscreen mode Exit fullscreen mode

Esto es todo. Ahora, ¡estás listo para el reto!

Paso 1. Importa las dependencias

En tu proyecto, debe haber una carpeta llamada programas. Ve a programs/<your-project-name>/Cargo.toml, y añade estas dependencias. Asegúrate de usar la versión 0.24.2 y que puedas usar [avm](https://book.anchor-lang.com/chapter_5/avm.html) para cambiarlo

[dependencies]
anchor-lang = "0.24.2"
anchor-spl = "0.24.2"
mpl-token-metadata = {version = "1.2.7", features = ["no-entrypoint"]}
Enter fullscreen mode Exit fullscreen mode

Anchor ha bajado todas las versiones anteriores a 0.24.2 por una vulnerabilidad de la seguridad, por lo tanto, asegúrate de usar esta.

Luego ve al archivo lib.rs en src e importa esto:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke;
use anchor_spl::token;
use anchor_spl::token::{MintTo, Token};
use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v2};
Enter fullscreen mode Exit fullscreen mode

Genial. Ahora, ¡vamos a crear las estructuras para la función ´mint´!

#[derive(Accounts)]

pub struct MintNFT<'info> {

   #[account(mut)]

   pub mint_authority: Signer<'info>,

/// CHECK: This is not dangerous because we don't read or write from this account

   #[account(mut)]

   pub mint: UncheckedAccount<'info>,

   // #[account(mut)]

   pub token_program: Program<'info, Token>,

   /// CHECK: This is not dangerous because we don't read or write from this account

   #[account(mut)]

   pub metadata: UncheckedAccount<'info>,

   /// CHECK: This is not dangerous because we don't read or write from this account

   #[account(mut)]

   pub token_account: UncheckedAccount<'info>,

   /// CHECK: This is not dangerous because we don't read or write from this account

   pub token_metadata_program: UncheckedAccount<'info>,

   /// CHECK: This is not dangerous because we don't read or write from this account

   #[account(mut)]

   pub payer: AccountInfo<'info>,

   pub system_program: Program<'info, System>,

   /// CHECK: This is not dangerous because we don't read or write from this account

   pub rent: AccountInfo<'info>,

   /// CHECK: This is not dangerous because we don't read or write from this account

   #[account(mut)]

   pub master_edition: UncheckedAccount<'info>,

}

Enter fullscreen mode Exit fullscreen mode

No te preocupes sobre las cuentas sin marcar, ya que las pasaremos al programa Metaplex, el cual lo revisará por nosotros.

Para poder usar cuentas sin marcar en Anchor, necesitas añadir este comentario sobre cada cuenta:

/// CHECK: This is not dangerous because we don't read or write from this account
Enter fullscreen mode Exit fullscreen mode

Paso 3. La Función Mint

Vamos a hacer una función que usa la estructura que acabamos de hacer para acuñar el token:

pub fn mint_nft(

       ctx: Context<MintNFT>,

       creator_key: Pubkey,

       uri: String,

       title: String,

   ) -> Result<()> {

       msg!("Initializing Mint NFT");

       let cpi_accounts = MintTo {

           mint: ctx.accounts.mint.to_account_info(),

           to: ctx.accounts.token_account.to_account_info(),

           authority: ctx.accounts.payer.to_account_info(),

       };

msg!("CPI Accounts Assigned");

       let cpi_program = ctx.accounts.token_program.to_account_info();

       msg!("CPI Program Assigned");

       let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

       msg!("CPI Context Assigned");

       token::mint_to(cpi_ctx, 1)?;

       msg!("Token Minted !!!");

       let account_info = vec![

           ctx.accounts.metadata.to_account_info(),

           ctx.accounts.mint.to_account_info(),

           ctx.accounts.mint_authority.to_account_info(),

           ctx.accounts.payer.to_account_info(),

           ctx.accounts.token_metadata_program.to_account_info(),

           ctx.accounts.token_program.to_account_info(),

           ctx.accounts.system_program.to_account_info(),

           ctx.accounts.rent.to_account_info(),

       ];

       msg!("Account Info Assigned");

       let creator = vec![

           mpl_token_metadata::state::Creator {

               address: creator_key,

               verified: false,

               share: 100,

           },

           mpl_token_metadata::state::Creator {

               address: ctx.accounts.mint_authority.key(),

               verified: false,

               share: 0,

           },

       ];

       msg!("Creator Assigned");

       let symbol = std::string::ToString::to_string("symb");

       invoke(

           &create_metadata_accounts_v2(

               ctx.accounts.token_metadata_program.key(),

               ctx.accounts.metadata.key(),

               ctx.accounts.mint.key(),

               ctx.accounts.mint_authority.key(),

               ctx.accounts.payer.key(),

               ctx.accounts.payer.key(),

               title,

               symbol,

               uri,

               Some(creator),

               1,

               true,

               false,

               None,

               None,

           ),

           account_info.as_slice(),

       )?;

       msg!("Metadata Account Created !!!");

       let master_edition_infos = vec![

           ctx.accounts.master_edition.to_account_info(),

           ctx.accounts.mint.to_account_info(),

           ctx.accounts.mint_authority.to_account_info(),

           ctx.accounts.payer.to_account_info(),

           ctx.accounts.metadata.to_account_info(),

           ctx.accounts.token_metadata_program.to_account_info(),

           ctx.accounts.token_program.to_account_info(),

           ctx.accounts.system_program.to_account_info(),

           ctx.accounts.rent.to_account_info(),

       ];

       msg!("Master Edition Account Infos Assigned");

       invoke(

           &create_master_edition_v3(

               ctx.accounts.token_metadata_program.key(),

               ctx.accounts.master_edition.key(),

               ctx.accounts.mint.key(),

               ctx.accounts.payer.key(),

               ctx.accounts.mint_authority.key(),

               ctx.accounts.metadata.key(),

               ctx.accounts.payer.key(),

               Some(0),

           ),

           master_edition_infos.as_slice(),

       )?;

       msg!("Master Edition Nft Minted !!!");

       Ok(())

   }
Enter fullscreen mode Exit fullscreen mode

Si quieres depurar tu programa, mejor usa msg!() para registrar cualquier valor que quieras revisar. Acepta cadenas así que tendrás que usar std::string::ToString para convertirlos. Tus registros aparecerán en el terminal o en .anchor/program-logs/<program-id>

Image description

Así que, algunas cosas aquí

La matriz creator necesita tener a la persona acuñando los NFTs como parte de ello, pero puedes colocar el cambio a 0, así que realmente no importa. Este es el código:

let creator = vec![

           mpl_token_metadata::state::Creator {

               address: creator_key,

               verified: false,

               share: 100,

           },

           mpl_token_metadata::state::Creator {

               address: ctx.accounts.mint_authority.key(),

               verified: false,

               share: 0,

           },

       ];
Enter fullscreen mode Exit fullscreen mode

No he implementado las Colecciones, ya que no está en el propósito de esta guía pero, puedes hacerlo usando:

mpl_token_metadata::instruction::set_and_verify_collection
Enter fullscreen mode Exit fullscreen mode

Con respecto a por qué he colocado el suministro máximo en 0 aquí, en Metaplex, si el token está destinado a ser uno de una clase, entonces tienes que establecer su suministro máximo a cero, ya que, el suministro total - suministro reclamado (1-1) es igual a 0

&create_master_edition_v3(

               ctx.accounts.token_metadata_program.key(),

               ctx.accounts.master_edition.key(),

               ctx.accounts.mint.key(),

               ctx.accounts.payer.key(),

               ctx.accounts.mint_authority.key(),

               ctx.accounts.metadata.key(),

               ctx.accounts.payer.key(),

               Some(0), // max supply 0

           ),
Enter fullscreen mode Exit fullscreen mode

Una vez que hayas escrito la función, ejecuta anchor build && anchor deploy y, deberías poder ver el Program ID desplegado.

Image description

Copia este Program ID en tu Anchor.toml y el archivo lib.rs donde veas este default ID Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS

Paso 4. Invocando a la Función Mint

Antes de hacer nada, asegúrate que hayas importado @solana/web3.js y @solana/spl-token. Dentro de tests/<test-file>.ts añade estos importes y constantes:

import {

 TOKEN_PROGRAM_ID,

 createAssociatedTokenAccountInstruction,

 getAssociatedTokenAddress,

 createInitializeMintInstruction,

 MINT_SIZE,

} from "@solana/spl-token";

import { LAMPORTS_PER_SOL } from "@solana/web3.js";

const { PublicKey, SystemProgram } = anchor.web3; q

const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey(

     "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"

   );

   const lamports: number =

     await program.provider.connection.getMinimumBalanceForRentExemption(

       MINT_SIZE

     );

   const getMetadata = async (

     mint: anchor.web3.PublicKey

   ): Promise<anchor.web3.PublicKey> => {

     return (

       await anchor.web3.PublicKey.findProgramAddress(

         [

           Buffer.from("metadata"),

           TOKEN_METADATA_PROGRAM_ID.toBuffer(),

           mint.toBuffer(),

         ],

         TOKEN_METADATA_PROGRAM_ID

       )

     )[0];

   };

   const getMasterEdition = async (

     mint: anchor.web3.PublicKey

   ): Promise<anchor.web3.PublicKey> => {

     return (

       await anchor.web3.PublicKey.findProgramAddress(

         [

           Buffer.from("metadata"),

           TOKEN_METADATA_PROGRAM_ID.toBuffer(),

           mint.toBuffer(),

           Buffer.from("edition"),

         ],

         TOKEN_METADATA_PROGRAM_ID

       )

     )[0];

   };

   const mintKey: anchor.web3.Keypair = anchor.web3.Keypair.generate();
Enter fullscreen mode Exit fullscreen mode

Ahora, vamos a hacer el token y la cuenta asociada al token, como se muestra aquí:

const NftTokenAccount = await getAssociatedTokenAddress(

     mintKey.publicKey,

     program.provider.wallet.publicKey

   );

   console.log("NFT Account: ", NftTokenAccount.toBase58());

   const mint_tx = new anchor.web3.Transaction().add(

     anchor.web3.SystemProgram.createAccount({

       fromPubkey: program.provider.wallet.publicKey,

       newAccountPubkey: mintKey.publicKey,

       space: MINT_SIZE,

       programId: TOKEN_PROGRAM_ID,

       lamports,

     }),

     createInitializeMintInstruction(

       mintKey.publicKey,

       0,

       program.provider.wallet.publicKey,

       program.provider.wallet.publicKey

     ),

     createAssociatedTokenAccountInstruction(

       program.provider.wallet.publicKey,

       NftTokenAccount,

       program.provider.wallet.publicKey,

       mintKey.publicKey

     )

   );

   const res = await program.provider.send(mint_tx, [mintKey]);

   console.log(

     await program.provider.connection.getParsedAccountInfo(mintKey.publicKey)

   );

   console.log("Account: ", res);

   console.log("Mint key: ", mintKey.publicKey.toString());

   console.log("User: ", program.provider.wallet.publicKey.toString());

   const metadataAddress = await getMetadata(mintKey.publicKey);

   const masterEdition = await getMasterEdition(mintKey.publicKey);

   console.log("Metadata address: ", metadataAddress.toBase58());

   console.log("MasterEdition: ", masterEdition.toBase58());
Enter fullscreen mode Exit fullscreen mode

Nota: la autoridad para acuñar y congelar tiene que ser la misma, si no, no funcionará.

createInitializeMintInstruction( mintKey.publicKey, 0, program.provider.wallet.publicKey,// mint auth program.provider.wallet.publicKey // freeze auth
),
Enter fullscreen mode Exit fullscreen mode

Ahora, llamaremos a la función acuñar y pasar toda la data y las cuentas

const tx = await program.rpc.mintNft(

     mintKey.publicKey,

     "https://arweave.net/y5e5DJsiwH0s_ayfMwYk-SnrZtVZzHLQDSTZ5dNRUHA",

     "NFT Title",

     {

       accounts: {

         mintAuthority: program.provider.wallet.publicKey,

         mint: mintKey.publicKey,

         tokenAccount: NftTokenAccount,

         tokenProgram: TOKEN_PROGRAM_ID,

         metadata: metadataAddress,

         tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,

         payer: program.provider.wallet.publicKey,

         systemProgram: SystemProgram.programId,

         rent: anchor.web3.SYSVAR_RENT_PUBKEY,

         masterEdition: masterEdition,

       },

     }

   );

   console.log("Your transaction signature", tx);
Enter fullscreen mode Exit fullscreen mode

¡Eso es todo! Ahora ejecuta la prueba anchor y deberías poder acuñar tu NFT.

Account:  4swRFMNovHCkXY3gDgAGBXZwpfFuVyxWpWsgXqbYvoZG1M63nZHxyPRm7KTqAjSdTpHn2ivyPr6jQfxeLsB6a1nX
Mint key:  DehGx61vZPYNaMWm9KYdP91UYXXLu1XKoc2CCu3NZFNb
User:  7CtWnYdTNBb3P9eViqSZKUekjcKnMcaasSMC7NbTVKuE
Metadata address:  7ut8YMzGqZAXvRDro8jLKkPnUccdeQxsfzNv1hjzc3Bo
MasterEdition:  Au76v2ZDnWSLj23TCu9NRVEYWrbVUq6DAGNnCuALaN6o
Your transaction signature KwEst87H3dZ5GwQ5CDL1JtiRKwcXJKNzyvQShaTLiGxz4HQGsDA7EW6rrhqwbJ2TqQFRWzZFvhfBU1CpyYH7WhH
   ✔ Is initialized! (6950ms)
 1 passing (7s)
✨  Done in 9.22s.
Enter fullscreen mode Exit fullscreen mode

Si tienes algún error del programa personalizado con un valor hex como 0x1, convierte el valor hex a texto simple, luego ve al metaplex github y busca, usando tu navegador, el número +1 th aparición de la palabra “error)”

Puedes revisar el NFT aquí:

https://solscan.io/token/DehGx61vZPYNaMWm9KYdP91UYXXLu1XKoc2CCu3NZFNb?cluster=devnet

Conclusión

Espero que esta guía haya sido útil para todos los geeks de Solana que están por allí. Cuando intenté acuñar un NFT por primera vez, me jalaba mi cabello, pero poco a poco empezó a tener sentido, una vez que algunas personas que les gustan los retos, me lo explicaron. Con suerte, espero que lo haya hecho más fácil para tí.

Aquí está el GitHub de este proyecto:

https://github.com/anoushk1234/metaplex-anchor-nft

Puedes seguirme en mi Twitter y GitHub. Hasta la próxima vez, ¡sigue disfrutando de los retos!

Muchas gracias a Pratik Saria y a 0xDeep por ayudarme a entender cómo los NFT de Solana y Anchor funcionan. Si no fueran por ellos, aún seguiría atascado intentando resolverlo.

Discussion (0)