ERC-721 su Solana

Il Token Program (Con Metadati)

Su Solana, non esiste un nuovo smart contract per ogni collezione NFT. Invece, gli NFT vengono creati sotto un singolo Token Program onchain, lo stesso utilizzato per i token fungibili - semplicemente coniati con una fornitura di 1. Per memorizzare dettagli aggiuntivi degli NFT (come nome, simbolo, URI dell'immagine, ecc.), gli sviluppatori utilizzano il Metaplex Token Metadata Program.

Metaplex Metadata è un protocollo e una piattaforma open-source per NFT (Non-Fungible Token) e asset digitali. Costruito sulla blockchain Solana, Metaplex facilita l'aggiunta di metadati aggiuntivi ai token su Solana.

Questo è diverso da Ethereum, dove tipicamente si distribuisce un nuovo contratto ERC721 per ogni collezione. Perché esiste un unico programma principale per gli NFT su Solana? È dovuto al modello di account Solana, che separa lo stato (i dati) dalla logica di esecuzione (il programma). Poiché Solana riutilizza il Token Program per tutti i token, non è necessario scrivere e distribuire un nuovo contratto NFT - basta un nuovo mint account con supply = 1.

solidity
// SPDX-License-Identifier: MIT license
pragma solidity =0.8.28;

struct Mint {
    uint8 decimals;
    uint256 supply;
    address mintAuthority;
    address freezeAuthority;
    address mintAddress;
}

struct TokenAccount {
    address mintAddress;
    address owner;
    uint256 balance;
    bool isFrozen;
}

struct Metadata {
    string name;
    string symbol;
    string tokenURI;
}

contract Spl721 {
    mapping(address => Mint) public mints;
    mapping(address => TokenAccount) public tokenAccounts;
    mapping(address => Metadata) public nftMetadata;
    mapping(address => bool) public mintAddresses;
    mapping(address => bool) public tokenAddresses;

    function initializeMint(
        uint8 decimals,
        address mintAuthority,
        address freezeAuthority,
        address mintAddress
    )
        public
        returns (Mint memory)
    {
        require(!mintAddresses[mintAddress], "Mint already exists");
        mints[mintAddress] = Mint(decimals, 0, mintAuthority, freezeAuthority, mintAddress);
        mintAddresses[mintAddress] = true;
        return mints[mintAddress];
    }

    function setMetadata(
        address mintAddress,
        string memory name,
        string memory symbol,
        string memory tokenURI
    )
        public
    {
        require(mintAddresses[mintAddress], "Mint does not exist");
        nftMetadata[mintAddress] = Metadata(name, symbol, tokenURI);
    }

    function mintNFT(address toMintTokens, address mintAddress) public {
        require(mintAddresses[mintAddress], "NFT mint does not exist");
        require(mints[mintAddress].mintAuthority == msg.sender, "Only the mint authority can mint");
        require(mints[mintAddress].supply == 0, "NFT already minted");
        mints[mintAddress].supply = 1;
        address tokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(toMintTokens, mintAddress)))));
        if (!tokenAddresses[tokenAddress]) {
            tokenAccounts[tokenAddress] = TokenAccount(mintAddress, toMintTokens, 0, false);
            tokenAddresses[tokenAddress] = true;
        }
        tokenAccounts[tokenAddress].balance = 1;
        tokenAccounts[tokenAddress].owner = toMintTokens;
    }

    function transfer(address to, address mintAddress, uint256 amount) public {
        address toTokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(to, mintAddress)))));
        address fromTokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, mintAddress)))));
        require(tokenAccounts[fromTokenAddress].balance >= amount, "Insufficient balance");
        require(amount == 1, "Only transferring 1 NFT at a time");
        require(tokenAccounts[fromTokenAddress].owner == msg.sender, "Not the NFT owner");
        require(!tokenAccounts[fromTokenAddress].isFrozen, "Sender token account is frozen");
        if (tokenAddresses[toTokenAddress]) {
            require(!tokenAccounts[toTokenAddress].isFrozen, "Receiver token account is frozen");
        }
        if (!tokenAddresses[toTokenAddress]) {
            tokenAccounts[toTokenAddress] = TokenAccount(mintAddress, to, 0, false);
            tokenAddresses[toTokenAddress] = true;
        }
        tokenAccounts[fromTokenAddress].balance -= amount;
        tokenAccounts[toTokenAddress].balance += amount;
        tokenAccounts[toTokenAddress].owner = to;
    }

    function freezeAccount(address owner, address mintAddress) public {
        require(mintAddresses[mintAddress], "Mint does not exist");
        require(mints[mintAddress].freezeAuthority == msg.sender, "Only the freeze authority can freeze");
        address tokenAddress = address(uint160(uint256(keccak256(abi.encodePacked(owner, mintAddress)))));
        require(tokenAddresses[tokenAddress], "Token account not found");
        tokenAccounts[tokenAddress].isFrozen = true;
    }

    function getMint(address token) public view returns (Mint memory) {
        return mints[token];
    }

    function getTokenAccount(address owner, address token) public view returns (TokenAccount memory) {
        return tokenAccounts[address(uint160(uint256(keccak256(abi.encodePacked(owner, token)))))];
    }

    function getMetadata(address mintAddress) public view returns (Metadata memory) {
        return nftMetadata[mintAddress];
    }
}
FaseEthereum (ERC-721)Solana (SPL + Metaplex)
1. Preparare il codice del tokenSolitamente si utilizzano le librerie ERC721 di OpenZeppelin. Si crea un file Solidity (ad es. MyNFT.sol).Non è necessario alcun contratto personalizzato per le funzionalità NFT di base. Il Token Program è già distribuito. Basta creare un mint account (supply = 1).
2. Compilare e distribuireCompilare e distribuire usando Hardhat/Truffle (ad es. npx hardhat run deploy.js --network ...).Utilizzare una CLI (ad es. spl-token create-token --decimals 0) o uno strumento SDK (come Candy Machine di Metaplex). Non è richiesta alcuna distribuzione di contratto separata.
3. Mintare l'NFTChiamare la funzione mint() del contratto, che assegna l'NFT a un indirizzo e solitamente imposta gli URI dei metadati del token. Le commissioni gas possono variare.Eseguire spl-token mint <MINT_ADDRESS> 1 o utilizzare le istruzioni di mint di Metaplex per creare esattamente 1 supply. Le commissioni di transazione su Solana sono tipicamente molto basse.
4. Creare il destinatarioTipicamente è solo un normale indirizzo Ethereum. Gli utenti devono aggiungere l'indirizzo del contratto NFT in molti wallet per visualizzarlo.Ogni utente ha un associated token account (ATA). I wallet come Phantom riconoscono automaticamente gli ATA per gli NFT. Nessun passaggio extra per "aggiungere" il token, anche se potrebbe apparire sotto una scheda "Collezionabili" una volta riconosciuto.
5. Verificare i risultatiVisualizzare su Etherscan o utilizzare un marketplace NFT come OpenSea. Spesso gli utenti aggiungono manualmente l'indirizzo del contratto per vedere i token in alcuni wallet.Utilizzare solana balance <ADDRESS> o spl-token accounts per vedere le partecipazioni di token. È anche possibile utilizzare un Solana Explorer o un marketplace NFT (Magic Eden, OpenSea Solana, ecc.) per confermare la presenza dell'NFT.
6. Identificatori univociIndirizzo del contratto + tokenId.Indirizzo del mint. Ogni NFT è semplicemente un token SPL con supply=1.
7. CollezioniTutti i token in un contratto tipicamente rappresentano una singola collezione.Le collezioni vengono assegnate tramite un "indirizzo di collezione" in Metaplex. Non si distribuisce un contratto dedicato.
8. Aggiornamenti del codicePer aggiornamenti importanti, si potrebbe utilizzare un pattern proxy o ridistribuire il contratto. Solo gli sviluppatori avanzati lo fanno tipicamente.Il Token Program è fisso. Se servono funzionalità avanzate (royalty, metadati dinamici, ecc.), è possibile utilizzare o costruire programmi onchain separati. Metaplex supporta anche estensioni come gli NFT programmabili.
9. Requisiti di auditOgni contratto richiede tipicamente un audit, specialmente se si aggiunge logica di minting personalizzata, un marketplace, ecc.Il Token Program principale e il Metaplex Metadata Program sono stati sottoposti ad audit molteplici volte. Per un mint standard di un NFT, tipicamente non è necessario alcun audit di contratto aggiuntivo.
solidity
function name() external view returns (string); // Returns the token collection name
jsx
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { fetchDigitalAsset, mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata";
import { PublicKey } from "@metaplex-foundation/js";
const mintAddress = new PublicKey("Token Address");

async function name() {
  try {
    const umi = createUmi("https://api.devnet.solana.com");
    umi.use(mplTokenMetadata());
    const digitalAsset = await fetchDigitalAsset(umi, mintAddress);
    return digitalAsset.metadata.name;
  } catch (error) {
    console.error("Error fetching NFT name:", error);
    return null;
  }
}

name().then(nftName => {
    console.log("NFT Name:", nftName);
  })
  .catch(error => {
    console.error("Error:", error);
  });
solidity
function symbol() external view returns (string); // Returns the token collection symbol
jsx
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { fetchDigitalAsset, mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata";
import { PublicKey } from "@metaplex-foundation/js";
const mintAddress = new PublicKey("Token Address");

async function symbol() {
  try {
    const umi = createUmi("https://api.devnet.solana.com");
    umi.use(mplTokenMetadata());
    const digitalAsset = await fetchDigitalAsset(umi, mintAddress);
    return digitalAsset.metadata.symbol;
  } catch (error) {
    console.error("Error fetching NFT symbol:", error);
    return null;
  }
}

symbol().then(nftSymbol => {
  console.log("NFT Symbol:", nftSymbol);
}).catch(error => {
  console.error("Error:", error);
});
bash
function tokenURI(uint256 _tokenId) external view returns (string); // Returns the tokenURI of the token
jsx
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { fetchDigitalAsset, mplTokenMetadata } from "@metaplex-foundation/mpl-token-metadata";
import { PublicKey } from "@metaplex-foundation/js";
const mintAddress = new PublicKey("Token Address");

async function tokenURI( /* no tokenId */ ) {
  try {
    const umi = createUmi("https://api.devnet.solana.com");
    umi.use(mplTokenMetadata());
    const digitalAsset = await fetchDigitalAsset(umi, mintAddress);
    return digitalAsset.metadata.uri;
  } catch (error) {
    console.error("Error fetching token URI:", error);
    return null;
  }
}

tokenURI()
  .then(uri => {
    console.log("Token URI:", uri);
  })
  .catch(error => {
    console.error("Error:", error);
  });
bash
function ownerOf(uint256 _tokenId) public view returns (address) // Returns the owner of the tokenId token
jsx
import { Connection, PublicKey } from "@solana/web3.js";

const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const mintAddress = new PublicKey("Token Address");

async function ownerOf( /*no tokenId*/ ){
  const largestAccounts = await connection.getTokenLargestAccounts(new PublicKey(mintAddress));
  const largestAccountInfo = await connection.getParsedAccountInfo(largestAccounts.value[0].address);
  return largestAccountInfo.value.data.parsed.info.owner;
}

ownerOf().then(owner => {
  console.log(owner);
}).catch(error => {
  console.error("Error:", error);
});
solidity
function transferFrom(address _from, address _to, uint256 _tokenId) external payable; // Transfers tokenId token from _from to _to
jsx
import { Keypair, Transaction, Connection, PublicKey } from "@solana/web3.js";
import { createTransferCheckedInstruction } from "@solana/spl-token";

const connection = new Connection("https://api.devnet.solana.com", "confirmed");

// Replace with your real private key (as a Uint8Array)
const ownerSecretkey = [];
const ownerPrivatekeypair = Keypair.fromSecretKey(new Uint8Array(ownerSecretkey));

const fromPublicKey = ownerPrivatekeypair.publicKey; // The sender's public key
const toPublicKey = new PublicKey("Receiver's Wallet Address");

const mintAddress = new PublicKey("Token Address");

// Pre-existing ATA addresses (for `_from` and `_to`)
const ownerTokenAccount = new PublicKey("Associated Token Account of _from");
const receiverTokenAccount = new PublicKey("Associated Token Account of _to");

// For an NFT, decimals = 0 and amount = 1
async function transferFrom(_from, _to) {
  try {
    const tx = new Transaction().add(
      createTransferCheckedInstruction(
        ownerTokenAccount,              // _from's ATA
        mintAddress,
        receiverTokenAccount,           // _to's ATA
        ownerPrivatekeypair.publicKey,  // Authority for `_from` (the private key must match this public key)
        1,                              // amount = 1 NFT
        0                               // decimals = 0 for NFT
      )
    );
    
    // Send transaction (simple version)
    await connection.sendTransaction(tx, [ownerPrivatekeypair]);
    return true;
  } catch (error) {
    console.error("Error in transferFrom:", error);
    return false;
  }
}

transferFrom(fromPublicKey, toPublicKey)
  .then(result => {
    console.log("transferFrom result:", result);
  })
  .catch(error => {
    console.error("Error:", error);
  });
jsx
import { Keypair, Transaction, Connection, PublicKey } from "@solana/web3.js";
import { createTransferCheckedInstruction, getOrCreateAssociatedTokenAccount } from "@solana/spl-token";

const connection = new Connection("https://api.devnet.solana.com", "confirmed");

// Insert your private key as a Uint8Array
const ownerSecretkey = [];
const ownerPrivatekeypair = Keypair.fromSecretKey(new Uint8Array(ownerSecretkey));

const fromAddress = ownerPrivatekeypair.publicKey;  // The sender's public key
const toAddress = new PublicKey("Receiver's Wallet Address");

const mintAddress = new PublicKey("Token Address");

// For an NFT: decimals = 0, amount = 1
async function transferFrom(_from, _to) {
  try {
    // Retrieve (or create if missing) the sender's ATA
    const ownerTokenAccount = await getOrCreateAssociatedTokenAccount(
      connection,
      ownerPrivatekeypair,    // Fee payer
      mintAddress,
      _from
    );

    // Retrieve (or create if missing) the receiver's ATA
    const receiverTokenAccount = await getOrCreateAssociatedTokenAccount(
      connection,
      ownerPrivatekeypair,    // Fee payer (use the receiver if needed)
      mintAddress,
      _to
    );

    // Create transaction with a transfer of 1 NFT (decimals=0)
    const tx = new Transaction().add(
      createTransferCheckedInstruction(
        ownerTokenAccount.address,
        mintAddress,
        receiverTokenAccount.address,
        ownerPrivatekeypair.publicKey,
        1, // Always transfer exactly 1 (NFT)
        0  // decimals = 0 for an NFT
      )
    );

    // Send the transaction
    await connection.sendTransaction(tx, [ownerPrivatekeypair]);
    return true;
  } catch (error) {
    console.error("Error in transferFrom:", error);
    return false;
  }
}

transferFrom(fromAddress, toAddress)
  .then(result => {
    console.log("Transaction result:", result);
  })
  .catch(error => {
    console.error("Error:", error);
  });
Guide EVM → SVM

Risorse aggiuntive

Gestito da

© 2026 Solana Foundation.
Tutti i diritti riservati.
Resta connesso
ERC-721 su Solana: NFT | Solana