Skip to content

WayLearnLatam/Solana-NFTs-Template

 
 

Repository files navigation

NFT Manager — Metaplex Core (mpl-core)

Herramienta para crear y administrar NFTs usando el programa Metaplex Core en Solana. Incluye dos modos de uso:

  • nft-funciones.ts — script Node.js con Keypair local, útil para scripts, bots o pruebas desde terminal
  • App.tsx — interfaz React que conecta con Phantom (o cualquier wallet adapter), donde el usuario firma desde su wallet

Ambos archivos implementan las mismas operaciones. La diferencia es quién firma: un Keypair en memoria (script) o la wallet del usuario (React).


Instalación

Incorpora las siguientes dependencias en tu package.json (si no las tienes)

"dependencies": {
    "@metaplex-foundation/mpl-core": "^1.10.0",
    "@metaplex-foundation/umi": "^1.5.1",
    "@metaplex-foundation/umi-bundle-defaults": "^1.5.1",
    "@solana/program-client-core": "^6.8.0",
    "@solana/spl-token": "^0.4.14",
    "@solana/wallet-adapter-react": "^0.15.39",
    "@solana/wallet-adapter-react-ui": "^0.9.39",
    "@solana/wallet-adapter-wallets": "^0.16.1",
    "@solana/web3.js": "^1.98.4",
    "buffer": "^6.0.3",
    "react": "^19.2.6",
    "react-dom": "^19.2.6"
  },

e instalalas con:

npm install

Conceptos que debes conocer

Asset vs Collection

Collection (CoRE...abc)          ← agrupa assets: royalties, plugins compartidos
  │
  ├── Asset #1 (7xPq...123)      ← NFT individual, hereda plugins de la colección
  ├── Asset #2 (9mKr...456)
  └── Asset #3 (2xH2...789)
  • Un Asset es un NFT individual. Su dirección on-chain es su identificador único.
  • Una Collection es un contenedor opcional. Los assets pueden existir sin colección (standalone).
  • Los plugins de colección (ej. Royalties) aplican automáticamente a todos sus assets.

Assets standalone vs assets en colección

// Standalone — no requiere colección
await createAsset(umi, { name: "Mi NFT", uri: "https://..." })

// En colección — hereda sus plugins
await createAsset(umi, {
  name:       "Mi NFT #1",
  uri:        "https://...",
  collection: "CoRE...abc",
})

La diferencia principal en las operaciones: transfer y burn sobre assets en colección requieren pasar la dirección de la colección, o detectarla con collectionAddress(asset).

Metadata JSON

El campo uri debe apuntar a un JSON público con el siguiente formato:

{
  "name":        "Mi NFT #1",
  "description": "Descripción del NFT",
  "image":       "https://raw.githubusercontent.com/user/repo/main/image.png",
  "attributes": [ // Opcional, no obligario
    { "trait_type": "Background", "value": "Azul"       },
    { "trait_type": "Rareza",     "value": "Legendario" }
  ],
  "properties": {
    "files":    [{ "uri": "https://...", "type": "image/png" }],
    "category": "image"
  }
}

La URL debe ser pública y accesible (Ejemlpo desde Github). Para devnet sirve cualquier URL (GitHub raw, imgur, etc.). Para producción usa Arweave (permanente) o IPFS via Pinata / NFT.Storage.

Plugins

Los plugins son módulos opcionales que agregan comportamiento a un Asset o Collection:

Plugin Aplica a Qué hace
Royalties Asset / Collection Cobra un % en cada venta en marketplaces
Attributes Asset / Collection Guarda traits directamente on-chain
FreezeDelegate Asset Permite congelar el asset (no transferible)
BurnDelegate Asset Delega permiso de quema
TransferDelegate Asset Delega permiso de transferencia
UpdateDelegate Asset / Collection Delega permiso de actualización de metadata

Los plugins Permanent (ej. PermanentFreezeDelegate) no pueden eliminarse una vez agregados.

Basis points

Las regalías se expresan en basis points: 100 = 1%, 500 = 5%, 1000 = 10%.

// 5% de regalías
addRoyalties(umi, assetAddress, 500, creatorWallet)

Funciones disponibles

initUmi

Inicializa Umi con un keypair en formato array de bytes. Punto de entrada para nft-funciones.ts.

const keypairBytes = JSON.parse(fs.readFileSync("keypair.json", "utf8"))
const umi = initUmi(keypairBytes)
// o en devnet por defecto:
const umi = initUmi(keypairBytes, "https://api.devnet.solana.com")

En App.tsx esto está integrado en el hook useUmi(), que construye Umi a partir de la wallet conectada.


1. createAsset

Crea un nuevo NFT (Asset) en Solana. Opcionalmente puede pertenecer a una Colección y tener un owner distinto al signer.

// nft-funciones.ts (Keypair)
const { assetAddress, signature } = await createAsset(umi, {
  name:       "Mi NFT #1",
  uri:        "https://raw.githubusercontent.com/user/repo/main/nft1.json",
  collection: "CoRE...abc",  // opcional
  owner:      "8xKp...def",  // opcional — default: signer
})
// App.tsx (wallet adapter) — pestaña "Create Asset"
// El usuario rellena los campos en el formulario y firma con Phantom

2. createNftCollection

Crea una Colección a la que luego se pueden agregar Assets. Los plugins de colección (especialmente Royalties) aplican a todos sus assets automáticamente.

// nft-funciones.ts
const { collectionAddress, signature } = await createNftCollection(umi, {
  name: "Mi Colección",
  uri:  "https://raw.githubusercontent.com/user/repo/main/collection.json",
})

3. getAsset

Lee los datos on-chain de un Asset: nombre, URI, owner, update authority, colección y plugins activos. No genera transacción ni costo.

const asset = await getAsset(umi, "7xPq...123")
// Logs: nombre, URI, owner, updateAuthority, colección

4. getCollection

Lee los datos on-chain de una Colección: nombre, URI, update authority, cantidad de assets minteados y tamaño actual.

const col = await getCollection(umi, "CoRE...abc")
// Logs: nombre, URI, updateAuthority, numMinted, currentSize

5. transferAsset

Transfiere un Asset a una nueva wallet. Solo puede hacerlo el owner actual o un Transfer Delegate. Los delegates se revocan automáticamente tras una transferencia exitosa.

// nft-funciones.ts
await transferAsset(
  umi,
  "7xPq...123",  // asset
  "9mKr...456",  // nuevo owner
  "CoRE...abc"   // colección — requerido si el asset pertenece a una
)

Para assets en colección se usa transferV1 internamente (recibe publicKeys directamente). Para assets standalone se hace fetchAsset previo ya que transfer() requiere el objeto AssetV1 completo.


6. burnAsset

Destruye permanentemente un Asset y recupera el SOL de renta (~0.0028 SOL). Solo puede hacerlo el owner o un Burn Delegate. La colección se detecta automáticamente desde los datos on-chain.

// nft-funciones.ts
await burnAsset(umi, "7xPq...123")
// La colección se detecta con collectionAddress(asset) — no necesitas pasarla

⚠️ Acción irreversible.


7. updateAsset

Modifica el nombre y/o URI de metadata de un Asset. Solo puede hacerlo la update authority o un Update Delegate. Omite los campos que no quieras cambiar.

// nft-funciones.ts
await updateAsset(
  umi,
  "7xPq...123",
  "Mi NFT #1 v2",                                          // nuevo nombre — opcional
  "https://raw.githubusercontent.com/user/repo/main/v2.json" // nueva URI — opcional
)

8. updateNftCollection

Modifica el nombre y/o URI de metadata de una Colección. Solo puede hacerlo la update authority de la colección.

// nft-funciones.ts
await updateNftCollection(
  umi,
  "CoRE...abc",
  "Mi Colección v2",  // nuevo nombre — opcional
  // undefined → no cambia la URI
)

9. Plugins — addRoyalties / addAttributes / addFreezeDelegate

Agrega plugins a un Asset existente. Cada plugin tiene su propio conjunto de campos requeridos.

// Royalties — 5% para el creador
await addRoyalties(umi, "7xPq...123", 500, "8xKp...def")

// Attributes — traits on-chain
await addAttributes(umi, "7xPq...123", [
  { key: "Background", value: "Azul"       },
  { key: "Rareza",     value: "Legendario" },
])

// FreezeDelegate — permite congelar el asset
await addFreezeDelegate(umi, "7xPq...123", false)

10. addCollectionRoyalties

Agrega Royalties a una Colección. Aplica a todos sus assets en marketplaces compatibles.

await addCollectionRoyalties(
  umi,
  "CoRE...abc",  // colección
  500,           // 5%
  "8xKp...def"   // creator que recibe regalías
)

11. removeAssetPlugin / removeCollectionPlugin_

Elimina un plugin de un Asset o Colección. La renta del plugin se recupera. Solo puede hacerlo quien tenga autoridad sobre ese plugin. Los plugins Permanent no pueden eliminarse.

// Eliminar de un Asset
await removeAssetPlugin(umi, "7xPq...123", "FreezeDelegate")

// Eliminar de una Colección
await removeCollectionPlugin_(umi, "CoRE...abc", "Royalties")

Flujo completo de ejemplo

1. createNftCollection          → collectionAddress
2. addCollectionRoyalties       → royalties en toda la colección (5%)
3. createAsset (en colección)   → assetAddress
4. getAsset                     → verificar datos on-chain
5. addAttributes                → agregar traits on-chain
6. updateAsset                  → actualizar nombre o URI
7. transferAsset                → enviar a otra wallet
8. burnAsset                    → destruir asset standalone

Diferencias entre nft-funciones.ts y App.tsx

Aspecto nft-funciones.ts App.tsx
Entorno Node.js (terminal) Browser (React)
Signer Keypair en memoria Wallet del usuario (Phantom)
Inicialización Umi initUmi(keypairBytes) useUmi(network) hook
Wallet adapter keypairIdentity(signer) walletAdapterIdentity(wallet)
Uso recomendado Scripts, bots, CI/CD, tests dApps con usuarios

El hook useUmi en App.tsx

Todas las operaciones de App.tsx obtienen Umi a través de este hook:

function useUmi(network: NetworkKey): Umi | null {
  const wallet = useWallet()
  return useMemo(() => {
    if (!wallet.publicKey) return null
    return createUmi(ENDPOINTS[network])
      .use(mplCore())
      .use(walletAdapterIdentity(wallet))
  }, [wallet, network])
}

Si wallet.publicKey es null (wallet no conectada), retorna null y los paneles muestran el botón de conexión.


Cómo adaptar a tu propio proyecto

Opción 1: Usar solo una función en un componente existente

import { useWallet } from "@solana/wallet-adapter-react"
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"
import { mplCore, create } from "@metaplex-foundation/mpl-core"
import { walletAdapterIdentity, generateSigner } from "@metaplex-foundation/umi"

function MiComponente() {
  const wallet = useWallet()

  const mintear = async () => {
    if (!wallet.publicKey) return
    const umi = createUmi("https://api.devnet.solana.com")
      .use(mplCore())
      .use(walletAdapterIdentity(wallet))

    const assetSigner = generateSigner(umi)
    await create(umi, {
      asset: assetSigner,
      name:  "Mi NFT",
      uri:   "https://raw.githubusercontent.com/user/repo/main/nft.json",
    }).sendAndConfirm(umi)

    console.log("Asset:", assetSigner.publicKey.toString())
  }

  return <button onClick={mintear}>Mintear</button>
}

Opción 2: Agregar una nueva pestaña a App.tsx

  1. Agrega el id al array TABS:
const TABS = [
  ...
  { id: "airdrop-nft", label: "Airdrop NFT" },
] as const
  1. Escribe el panel React:
function AirdropNftPanel({ umi, cluster }: PanelProps) {
  const [recipient, setRecipient] = useState("")
  const [uri, setUri]             = useState("")
  const [loading, setLoading]     = useState(false)
  const [result, setResult]       = useState<TxResult | null>(null)

  const handle = async () => {
    setLoading(true); setResult(null)
    try {
      const assetSigner = generateSigner(umi)
      const tx = await create(umi, {
        asset: assetSigner,
        name:  "Airdrop NFT",
        uri,
        owner: toPublicKey(recipient),
      }).sendAndConfirm(umi)
      setResult({ type: "success", message: `Asset: ${assetSigner.publicKey}`, sig: sigStr(tx.signature), cluster })
    } catch (e) {
      setResult({ type: "error", message: errMsg(e) })
    } finally { setLoading(false) }
  }

  return (
    <>
      <Field label="Recipient"><TextInput value={recipient} onChange={setRecipient} placeholder="Wallet destino" /></Field>
      <Field label="Metadata URI"><TextInput value={uri} onChange={setUri} placeholder="https://..." /></Field>
      <Btn onClick={handle} loading={loading} disabled={!recipient || !uri}>Enviar NFT</Btn>
      <TxFeedback r={result} />
    </>
  )
}
  1. Regístralo en el switch de renderPanel:
case "airdrop-nft": return <AirdropNftPanel {...props} />

Opción 3: Vibe Coding

Este repositorio está pensado como punto de partida y contexto para cualquier modelo de lenguaje (GPT, Claude, DeepSeek, etc.) que genere código. Para usarlo como contexto considera compartir el App.tsx y el README. También puedes proporcionar la documentación oficial: Metaplex Core Docs.


¿Cómo cambiar a Mainnet?

// App.tsx — el selector de red ya está integrado en la UI
// Cambia el default en el useState:
const [network, setNetwork] = useState<NetworkKey>("mainnet")

// O con un RPC privado (recomendado en producción)
const ENDPOINTS: Record<NetworkKey, string> = {
  devnet:  "https://api.devnet.solana.com",
  mainnet: "https://rpc.helius.xyz/?api-key=TU_API_KEY",
}
// nft-funciones.ts
const umi = initUmi(keypairBytes, "https://api.mainnet-beta.solana.com")

En mainnet no existe requestAirdrop. El signer debe tener SOL real.


⚠️ Consideraciones importantes para producción

Fijar la Collection Address

Si tu proyecto opera sobre una colección ya existente, define su dirección como constante para no pedirla en cada formulario:

// App.tsx — definir junto a ENDPOINTS
const COLLECTION_ADDRESS = import.meta.env.VITE_COLLECTION_ADDRESS ?? ""
# .env.development
VITE_COLLECTION_ADDRESS=CoRE...abc
VITE_RPC_URL=https://api.devnet.solana.com

# .env.production
VITE_COLLECTION_ADDRESS=<colección real en mainnet>
VITE_RPC_URL=https://rpc.helius.xyz/?api-key=TU_API_KEY

Agrega .env.production a .gitignore.

Validar que la wallet sea la update authority

El código no verifica que quien conecta la wallet sea la update authority de la colección o el asset. En producción conviene comprobarlo antes de mostrar paneles de administración:

const esAdmin = wallet.publicKey?.toBase58() === import.meta.env.VITE_ADMIN_WALLET

{!esAdmin && wallet.connected && (
  <div style={{ background: "#fff3e0", padding: 10, borderRadius: 4 }}>
    ⚠️ Esta wallet no es la update authority. Las operaciones de update y plugins fallarán.
  </div>
)}

Almacenamiento permanente de metadata

Para producción la imagen y el JSON de metadata deben estar en almacenamiento permanente:

Servicio Costo Permanencia
Arweave (via Irys) ~$0.01 por MB Permanente
IPFS via NFT.Storage Gratis Permanente (Filecoin)
IPFS via Pinata Gratis hasta 1GB Mientras mantengas el pin

Si el servidor donde está la imagen se cae, tu NFT queda sin imagen para siempre.


Referencias

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages