Herramienta para crear y administrar NFTs usando el programa Metaplex Core en Solana. Incluye dos modos de uso:
nft-funciones.ts— script Node.js conKeypairlocal, útil para scripts, bots o pruebas desde terminalApp.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).
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 installCollection (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.
// 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).
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.
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.
Las regalías se expresan en basis points: 100 = 1%, 500 = 5%, 1000 = 10%.
// 5% de regalías
addRoyalties(umi, assetAddress, 500, creatorWallet)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.
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 PhantomCrea 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",
})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ónLee 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, currentSizeTransfiere 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
transferV1internamente (recibe publicKeys directamente). Para assets standalone se hacefetchAssetprevio ya quetransfer()requiere el objetoAssetV1completo.
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.
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
)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
)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)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
)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")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
| 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 |
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.
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>
}- Agrega el id al array
TABS:
const TABS = [
...
{ id: "airdrop-nft", label: "Airdrop NFT" },
] as const- 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} />
</>
)
}- Regístralo en el
switchderenderPanel:
case "airdrop-nft": return <AirdropNftPanel {...props} />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.
// 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.
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_KEYAgrega .env.production a .gitignore.
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>
)}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.