Tokens no fungibles (NFTs)
¿Cómo crear un NFT?
Para crear un NFT tu tienes que:
- Subir la imagen a IPFS como Arweave
- Subir los metadatos en json a IPFS como Arweave
- Llamar a metaplex para crear la cuenta para el NFT
Subir a Arweave
import fs from "fs";
import Arweave from "arweave";
(async () => {
const arweave = Arweave.init({
host: "arweave.net",
port: 443,
protocol: "https",
timeout: 20000,
logging: false,
});
// Upload image to Arweave
const data = fs.readFileSync("./code/nfts/arweave-upload/lowres-dog.png");
const transaction = await arweave.createTransaction({
data: data,
});
transaction.addTag("Content-Type", "image/png");
const wallet = JSON.parse(fs.readFileSync("wallet.json", "utf-8"))
await arweave.transactions.sign(transaction, wallet);
const response = await arweave.transactions.post(transaction);
console.log(response);
const id = transaction.id;
const imageUrl = id ? `https://arweave.net/${id}` : undefined;
console.log("imageUrl", imageUrl);
// Upload metadata to Arweave
const metadata = {
name: "Custom NFT #1",
symbol: "CNFT",
description: "A description about my custom NFT #1",
seller_fee_basis_points: 500,
external_url: "https://www.customnft.com/",
attributes: [
{
trait_type: "NFT type",
value: "Custom",
},
],
collection: {
name: "Test Collection",
family: "Custom NFTs",
},
properties: {
files: [
{
uri: imageUrl,
type: "image/png",
},
],
category: "image",
maxSupply: 0,
creators: [
{
address: "CBBUMHRmbVUck99mTCip5sHP16kzGj3QTYB8K3XxwmQx",
share: 100,
},
],
},
image: imageUrl,
};
const metadataRequest = JSON.stringify(metadata);
const metadataTransaction = await arweave.createTransaction({
data: metadataRequest,
});
metadataTransaction.addTag("Content-Type", "application/json");
await arweave.transactions.sign(metadataTransaction, wallet);
console.log("metadata txid", metadataTransaction.id);
console.log(await arweave.transactions.post(metadataTransaction));
})();
// 1. Upload image to Arweave
const data = fs.readFileSync("./code/nfts/arweave-upload/lowres-dog.png");
const transaction = await arweave.createTransaction({
data: data,
});
transaction.addTag("Content-Type", "image/png");
const wallet = JSON.parse(fs.readFileSync("wallet.json", "utf-8"))
await arweave.transactions.sign(transaction, wallet);
const response = await arweave.transactions.post(transaction);
console.log(response);
const id = transaction.id;
const imageUrl = id ? `https://arweave.net/${id}` : undefined;
// 2. Upload metadata to Arweave
const metadataRequest = JSON.stringify(metadata);
const metadataTransaction = await arweave.createTransaction({
data: metadataRequest,
});
metadataTransaction.addTag("Content-Type", "application/json");
await arweave.transactions.sign(metadataTransaction, wallet);
await arweave.transactions.post(metadataTransaction);
from arweave.arweave_lib import Wallet, Transaction, API_URL
import json
# Load your arweave wallet
your_ar_wallet = Wallet('wallet.json')
with open('./code/nfts/arweave-upload/lowres-dog.png', 'rb') as f:
img_in_bytes = f.read()
# Upload image to Arweave
transaction = Transaction(your_ar_wallet, data=img_in_bytes)
transaction.add_tag('Content-Type', 'image/png')
transaction.sign()
transaction.send()
image_url = API_URL+"/"+transaction.id
# Define metadata
metadata = {
"name": "Custom NFT #1",
"symbol": "CNFT",
"description": "A description about my custom NFT #1",
"seller_fee_basis_points": 500,
"external_url": "https://www.customnft.com/",
"attributes": [
{
"trait_type": "NFT type",
"value": "Custom"
}
],
"collection": {
"name": "Test Collection",
"family": "Custom NFTs",
},
"properties": {
"files": [
{
"uri": image_url,
"type": "image/png",
},
],
"category": "image",
"maxSupply": 0,
"creators": [
{
"address": "CBBUMHRmbVUck99mTCip5sHP16kzGj3QTYB8K3XxwmQx",
"share": 100,
},
],
},
"image": image_url,
}
# Upload metadata to Arweave
meta_transaction = Transaction(your_ar_wallet, data=json.dumps(metadata))
meta_transaction.add_tag('Content-Type', 'text/html')
meta_transaction.sign()
meta_transaction.send()
metadata_url = API_URL+"/"+meta_transaction.id
print(metadata_url)
# 1. Load your arweave wallet
your_ar_wallet = Wallet('wallet.json')
# 2. Upload image to Arweave
with open('./code/nfts/arweave-upload/lowres-dog.png', 'rb') as f:
img_in_bytes = f.read()
transaction = Transaction(your_ar_wallet, data=img_in_bytes)
transaction.add_tag('Content-Type', 'image/png')
transaction.sign()
transaction.send()
image_url = API_URL+"/"+transaction.id
# 3. Upload metadata to Arweave
meta_transaction = Transaction(your_ar_wallet, data=json.dumps(metadata))
meta_transaction.add_tag('Content-Type', 'text/html')
meta_transaction.sign()
meta_transaction.send()
metadata_url = API_URL+"/"+meta_transaction.id
Crear (mint) el NFT
Si ya tiene la imagen y los metadatos cargados, puede hacer mint del NFT con el siguiente código.
import { Metaplex, keypairIdentity } from "@metaplex-foundation/js";
import {
Connection,
clusterApiUrl,
Keypair,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import dotenv from "dotenv";
dotenv.config();
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const keypair = Keypair.fromSecretKey(
Buffer.from(JSON.parse(process.env.SOLANA_KEYPAIR!.toString()))
);
const metaplex = new Metaplex(connection);
metaplex.use(keypairIdentity(keypair));
const feePayerAirdropSignature = await connection.requestAirdrop(
keypair.publicKey,
LAMPORTS_PER_SOL
);
await connection.confirmTransaction(feePayerAirdropSignature);
const mintNFTResponse = await metaplex.nfts().create({
uri: "https://ffaaqinzhkt4ukhbohixfliubnvpjgyedi3f2iccrq4efh3s.arweave.net/KUAIIbk6p8oo4XHRcq0U__C2r0mwQaNl0gQow4Qp9yk",
maxSupply: 1,
});
console.log(mintNFTResponse);
})();
const mintNFTResponse = await metaplex.nfts().create({
uri: "https://ffaaqinzhkt4ukhbohixfliubnvpjgyedi3f2iccrq4efh3s.arweave.net/KUAIIbk6p8oo4XHRcq0U__C2r0mwQaNl0gQow4Qp9yk",
maxSupply: 1,
});
TIP
No puedes hacer mint de un NFT con un creador diferente al de tu billetera. Si tienes problemas al crear, asegúrese de que sus metadatos lo incluyan como el creador.
¿Cómo obtener metadatos de un NFT?
Los NFTs de Metaplex tienen metadatos que se almacenan en Arweave. Para obtener los metadatos de Arweave, debes obtener el PDA de Metaplex PDA y decodificar los datos de la cuenta.
import { Metaplex, keypairIdentity } from "@metaplex-foundation/js";
import { Connection, clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
(async () => {
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const keypair = Keypair.generate();
const metaplex = new Metaplex(connection);
metaplex.use(keypairIdentity(keypair));
const mintAddress = new PublicKey(
"Ay1U9DWphDgc7hq58Yj1yHabt91zTzvV2YJbAWkPNbaK"
);
const nft = await metaplex.nfts().findByMint({ mintAddress });
console.log(nft.json);
/*
{
name: 'SMB #139',
symbol: 'SMB',
description: 'SMB is a collection of 5000 randomly generated 24x24 pixels NFTs on the Solana Blockchain. Each SolanaMonkey is unique and comes with different type and attributes varying in rarity.',
seller_fee_basis_points: 600,
image: 'https://arweave.net/tZrNpbFUizSoFnyTqP4n2e1Tf7WvP3siUwFWKMMid_Q',
external_url: 'https://solanamonkey.business/',
collection: { name: 'SMB Gen2', family: 'SMB' },
attributes: [
{ trait_type: 'Attributes Count', value: 2 },
{ trait_type: 'Type', value: 'Solana' },
{ trait_type: 'Clothes', value: 'Orange Shirt' },
{ trait_type: 'Ears', value: 'None' },
{ trait_type: 'Mouth', value: 'None' },
{ trait_type: 'Eyes', value: 'Cool Glasses' },
{ trait_type: 'Hat', value: 'None' }
],
properties: {
files: [ [Object], [Object] ],
category: 'image',
creators: [ [Object] ]
}
}
*/
})();
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const keypair = Keypair.generate();
const metaplex = new Metaplex(connection);
metaplex.use(keypairIdentity(keypair));
const mintAddress = new PublicKey(
"Ay1U9DWphDgc7hq58Yj1yHabt91zTzvV2YJbAWkPNbaK"
);
const nft = await metaplex.nfts().findByMint({ mintAddress });
console.log(nft.json);
¿Cómo obtener el dueño de un NFT?
Si tiene la clave de mint de un NFT, puede encontrar su propietario actual echando un vistazo a la cuenta de token más grande para ese mint.
Recuerde que los NFT tienen un suministro de 1 y son indivisibles, lo que significa que solo una cuenta de token mantendrá ese token en cualquier punto en el tiempo, mientras que todas las demás cuentas de token para esa clave de mint tienen un saldo de 0.
Una vez que se identifica la cuenta de token más grande, podemos recuperar a su propietario.
import { Connection, PublicKey } from "@solana/web3.js";
(async () => {
const connection = new Connection("https://api.mainnet-beta.solana.com");
const tokenMint = "9ARngHhVaCtH5JFieRdSS5Y8cdZk2TMF4tfGSWFB9iSK";
const largestAccounts = await connection.getTokenLargestAccounts(
new PublicKey(tokenMint)
);
const largestAccountInfo = await connection.getParsedAccountInfo(
largestAccounts.value[0].address
);
console.log(largestAccountInfo.value.data.parsed.info.owner);
/*
PublicKey {
_bn: <BN: 6ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9>
}
*/
})();
const connection = new Connection("https://api.mainnet-beta.solana.com");
const tokenMint = "9ARngHhVaCtH5JFieRdSS5Y8cdZk2TMF4tfGSWFB9iSK";
const largestAccounts = await connection.getTokenLargestAccounts(
new PublicKey(tokenMint)
);
const largestAccountInfo = await connection.getParsedAccountInfo(
largestAccounts.value[0].address
);
console.log(largestAccountInfo.value.data.parsed.info.owner);
¿Cómo obtener direcciones de mint de NFT?
Si conoces la clave pública de Candy Machine, puede obtener la lista de todas las direcciones de mint NFT generadas desde esa Candy Machine usando el siguiente código. Ten en cuenta que podemos usar el siguiente filtro memcmp
porque, en v1, el primer creador siempre es la dirección de Candy Machine.
Candy Machine V1
import { Connection, clusterApiUrl, PublicKey } from "@solana/web3.js";
import bs58 from "bs58";
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const MAX_NAME_LENGTH = 32;
const MAX_URI_LENGTH = 200;
const MAX_SYMBOL_LENGTH = 10;
const MAX_CREATOR_LEN = 32 + 1 + 1;
const MAX_CREATOR_LIMIT = 5;
const MAX_DATA_SIZE =
4 +
MAX_NAME_LENGTH +
4 +
MAX_SYMBOL_LENGTH +
4 +
MAX_URI_LENGTH +
2 +
1 +
4 +
MAX_CREATOR_LIMIT * MAX_CREATOR_LEN;
const MAX_METADATA_LEN = 1 + 32 + 32 + MAX_DATA_SIZE + 1 + 1 + 9 + 172;
const CREATOR_ARRAY_START =
1 +
32 +
32 +
4 +
MAX_NAME_LENGTH +
4 +
MAX_URI_LENGTH +
4 +
MAX_SYMBOL_LENGTH +
2 +
1 +
4;
const TOKEN_METADATA_PROGRAM = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
const candyMachineId = new PublicKey("ENTER_YOUR_CANDY_MACHINE_ID_HERE");
const getMintAddresses = async (firstCreatorAddress: PublicKey) => {
const metadataAccounts = await connection.getProgramAccounts(
TOKEN_METADATA_PROGRAM,
{
// The mint address is located at byte 33 and lasts for 32 bytes.
dataSlice: { offset: 33, length: 32 },
filters: [
// Only get Metadata accounts.
{ dataSize: MAX_METADATA_LEN },
// Filter using the first creator.
{
memcmp: {
offset: CREATOR_ARRAY_START,
bytes: firstCreatorAddress.toBase58(),
},
},
],
}
);
return metadataAccounts.map((metadataAccountInfo) =>
bs58.encode(metadataAccountInfo.account.data)
);
};
getMintAddresses(candyMachineId);
const getMintAddresses = async (firstCreatorAddress: PublicKey) => {
const metadataAccounts = await connection.getProgramAccounts(
TOKEN_METADATA_PROGRAM,
{
// The mint address is located at byte 33 and lasts for 32 bytes.
dataSlice: { offset: 33, length: 32 },
filters: [
// Only get Metadata accounts.
{ dataSize: MAX_METADATA_LEN },
// Filter using the first creator.
{
memcmp: {
offset: CREATOR_ARRAY_START,
bytes: firstCreatorAddress.toBase58(),
},
},
],
}
);
return metadataAccounts.map((metadataAccountInfo) =>
bs58.encode(metadataAccountInfo.account.data)
);
};
getMintAddresses(candyMachineId);
Candy Machine V2
Si estás utilizando Candy Machine v2, primero deberás acceder a su dirección "Candy Machine Creator", que es un PDA simple que utiliza candy_machine
y la dirección de Candy Machine v2 como semillas. Una vez que tengas la dirección del creador, puede usarla de la misma manera que lo hicimos para v1.
import { Connection, clusterApiUrl, PublicKey } from "@solana/web3.js";
import bs58 from "bs58";
const connection = new Connection(clusterApiUrl("mainnet-beta"));
const MAX_NAME_LENGTH = 32;
const MAX_URI_LENGTH = 200;
const MAX_SYMBOL_LENGTH = 10;
const MAX_CREATOR_LEN = 32 + 1 + 1;
const MAX_CREATOR_LIMIT = 5;
const MAX_DATA_SIZE =
4 +
MAX_NAME_LENGTH +
4 +
MAX_SYMBOL_LENGTH +
4 +
MAX_URI_LENGTH +
2 +
1 +
4 +
MAX_CREATOR_LIMIT * MAX_CREATOR_LEN;
const MAX_METADATA_LEN = 1 + 32 + 32 + MAX_DATA_SIZE + 1 + 1 + 9 + 172;
const CREATOR_ARRAY_START =
1 +
32 +
32 +
4 +
MAX_NAME_LENGTH +
4 +
MAX_URI_LENGTH +
4 +
MAX_SYMBOL_LENGTH +
2 +
1 +
4;
const TOKEN_METADATA_PROGRAM = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
const CANDY_MACHINE_V2_PROGRAM = new PublicKey(
"cndy3Z4yapfJBmL3ShUp5exZKqR3z33thTzeNMm2gRZ"
);
const candyMachineId = new PublicKey("ENTER_YOUR_CANDY_MACHINE_ID_HERE");
const getMintAddresses = async (firstCreatorAddress: PublicKey) => {
const metadataAccounts = await connection.getProgramAccounts(
TOKEN_METADATA_PROGRAM,
{
// The mint address is located at byte 33 and lasts for 32 bytes.
dataSlice: { offset: 33, length: 32 },
filters: [
// Only get Metadata accounts.
{ dataSize: MAX_METADATA_LEN },
// Filter using the first creator.
{
memcmp: {
offset: CREATOR_ARRAY_START,
bytes: firstCreatorAddress.toBase58(),
},
},
],
}
);
return metadataAccounts.map((metadataAccountInfo) =>
bs58.encode(metadataAccountInfo.account.data)
);
};
const getCandyMachineCreator = async (
candyMachine: PublicKey
): Promise<[PublicKey, number]> =>
PublicKey.findProgramAddress(
[Buffer.from("candy_machine"), candyMachine.toBuffer()],
CANDY_MACHINE_V2_PROGRAM
);
(async () => {
const candyMachineCreator = await getCandyMachineCreator(candyMachineId);
getMintAddresses(candyMachineCreator[0]);
})();
const getCandyMachineCreator = async (
candyMachine: PublicKey
): Promise<[PublicKey, number]> =>
PublicKey.findProgramAddress(
[Buffer.from("candy_machine"), candyMachine.toBuffer()],
CANDY_MACHINE_V2_PROGRAM
);
const candyMachineCreator = await getCandyMachineCreator(candyMachineId);
getMintAddresses(candyMachineCreator[0]);
¿Cómo obtener todos los NFT de una billetera?
Al obtener todos los NFT de una billetera, deberá obtener todas las cuentas de token y luego analizar cuáles son NFT. Todo esto se puede hacer usando findDataByOwner
de la librería js de Metaplex.
import { Metaplex, keypairIdentity } from "@metaplex-foundation/js";
import { Connection, clusterApiUrl, Keypair, PublicKey } from "@solana/web3.js";
(async () => {
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const keypair = Keypair.generate();
const metaplex = new Metaplex(connection);
metaplex.use(keypairIdentity(keypair));
const owner = new PublicKey("2R4bHmSBHkHAskerTHE6GE1Fxbn31kaD5gHqpsPySVd7");
const allNFTs = await metaplex.nfts().findAllByOwner({ owner });
console.log(allNFTs);
})();
const connection = new Connection(clusterApiUrl("mainnet-beta"), "confirmed");
const keypair = Keypair.generate();
const metaplex = new Metaplex(connection);
metaplex.use(keypairIdentity(keypair));
const owner = new PublicKey("2R4bHmSBHkHAskerTHE6GE1Fxbn31kaD5gHqpsPySVd7");
const allNFTs = await metaplex.nfts().findAllByOwner({ owner });
console.log(allNFTs);