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.
// 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];
}
}
| Fase | Ethereum (ERC-721) | Solana (SPL + Metaplex) |
|---|---|---|
| 1. Preparare il codice del token | Solitamente 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 distribuire | Compilare 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'NFT | Chiamare 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 destinatario | Tipicamente è 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 risultati | Visualizzare 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 univoci | Indirizzo del contratto + tokenId. | Indirizzo del mint. Ogni NFT è semplicemente un token SPL con supply=1. |
| 7. Collezioni | Tutti 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 codice | Per 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 audit | Ogni 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. |
function name() external view returns (string); // Returns the token collection nameimport { 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);
});
function symbol() external view returns (string); // Returns the token collection symbolimport { 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);
});
function tokenURI(uint256 _tokenId) external view returns (string); // Returns the tokenURI of the tokenimport { 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);
});
function ownerOf(uint256 _tokenId) public view returns (address) // Returns the owner of the tokenId tokenimport { 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);
});
function transferFrom(address _from, address _to, uint256 _tokenId) external payable; // Transfers tokenId token from _from to _toimport { 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);
});
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);
});