ERC-721 estandariza los tokens no fungibles (NFT) en Ethereum: cada token tiene un identificador único y sus propios metadatos, a diferencia de las unidades intercambiables de ERC-20.
pallet-uniques
EmpoorioChain usa pallet-uniques, el pallet estándar de NFT de Substrate, para los tokens no fungibles. En lugar de desplegar un nuevo contrato por colección, creas un nuevo ID de colección en el pallet y luego acuñas ítems (IDs únicos) dentro de ella. Los metadatos a nivel de ítem (nombre, URI de imagen, atributos) se almacenan directamente en el almacenamiento de metadatos propio del pallet, sin necesidad de un programa de metadatos independiente.
Esto refleja el mismo planteamiento arquitectónico que los tokens fungibles: un único pallet nativo y auditado gestiona todas las colecciones de NFT de la cadena, en lugar de un nuevo contrato por proyecto.
Los NFT se identifican por (ID de colección, ID de ítem), no por una dirección de contrato
En Ethereum, un NFT se identifica por la dirección de contrato de su colección más un tokenId. En EmpoorioChain, un NFT se identifica mediante un par (ID de colección, ID de ítem) dentro de pallet-uniques — no hay una dirección de contrato por colección, porque la colección es solo una entrada en el almacenamiento propio del pallet.
Sin flujo de aprobación independiente para transferencias básicas
En Ethereum, ERC-721 a menudo depende de approve o setApprovalForAll para que los marketplaces puedan mover tus NFT. En EmpoorioChain, la propiedad se rastrea directamente por cada (colección, ítem) bajo el AccountId32 propietario, y el pallet expone su propio extrínseco de aprobación delegada cuando se necesita, en lugar de un patrón específico por colección.
¿Cómo sería un equivalente a ERC-721 en Solidity?
A modo de comparación, así sería aproximadamente el comportamiento equivalente como un único contrato reutilizable en Solidity (ilustrativo — la lógica real reside en pallet-uniques, escrito en Rust):
// 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];
}
}
Al igual que con los tokens fungibles, la lógica de NFT de EmpoorioChain se distribuye una sola vez como parte del runtime; cada colección es un dato dentro de ese único pallet, en lugar de su propio contrato desplegado.
¿Dónde están los metadatos del NFT?
Los metadatos (nombre, símbolo, atributos y una URI off-chain si se usa) se almacenan directamente en el almacenamiento de metadatos propio de pallet-uniques, por ítem y por colección, establecidos mediante extrínsecos dedicados en el momento de la acuñación — no hay un programa de metadatos independiente que desplegar.
Compensaciones de cada enfoque
ERC-721
- Altamente personalizable por colección
- Nuevo despliegue de contrato por cada colección
- La indexación requiere rastrear cada dirección de contrato
- La gestión de metadatos varía de un contrato a otro
pallet-uniques
- Un único pallet gestiona todas las colecciones
- Las nuevas colecciones/ítems se crean mediante extrínsecos, no con nuevos contratos
- Más fácil de indexar — un pallet, una estructura de almacenamiento coherente
- La lógica avanzada por colección requiere una extensión del pallet, no un redespliegue rápido
Comparación general
A continuación se muestra una comparación de los pasos básicos para crear una colección de NFT en Ethereum frente a EmpoorioChain.
| Paso | Ethereum (ERC-721) | EmpoorioChain (pallet-uniques) |
|---|---|---|
| 1. Preparar el código | Escribir/importar un contrato en Solidity (p. ej. el ERC721 de OpenZeppelin). | No se necesita código personalizado — el pallet ya forma parte del runtime. |
| 2. Compilar y desplegar | Compilar y desplegar con Hardhat/Foundry. | Crear un ID de colección mediante un extrínseco — sin despliegue independiente. |
| 3. Acuñar | Llamar a la función mint() del contrato. | Llamar al extrínseco de acuñación del pallet para un ID de ítem determinado dentro de la colección. |
| 4. Destinatario | Una dirección de Ethereum simple; las wallets a menudo añaden el contrato manualmente para mostrarlo. | Cualquier AccountId32 puede poseer un ítem directamente. |
| 5. Comprobar resultados | Ver en Etherscan/marketplace por dirección de contrato. | Consultar vía EmpooScan o api.query.uniques.asset por ID de colección/ítem. |
| 6. Identificadores únicos | Dirección de contrato + tokenId. | Par (ID de colección, ID de ítem). |
| 7. Actualizaciones de código | Patrón proxy o redespliegue. | Los cambios en la lógica del pallet requieren una actualización de runtime gobernada. |
¿Quieres acuñar tu propio NFT en EmpoorioChain? Consulta la referencia de pallets y la documentación de emp-cli.
Cómo interactuar con pallet-uniques
Los ejemplos siguientes usan @polkadot/api. Como las colecciones de NFT en EmpoorioChain no están vinculadas a una dirección de contrato por colección, los ítems se buscan por (ID de colección, ID de ítem) en lugar de un simple tokenId frente a un contrato.
Leer los metadatos de colección/ítem
function name() external view returns (string); // Returns the token collection nameSe lee mediante api.query.uniques.instanceMetadataOf(collectionId, itemId) o la consulta de metadatos a nivel de colección, según lo que necesites.
// EmpoorioChain: query on-chain asset metadata via @polkadot/api
// against the pallet_emp_assets metadata storage for the given assetId.
// See the EmpoorioChain SDK docs for the exact metadata query shape.
import { ApiPromise, WsProvider } from "@polkadot/api";
async function name(assetId) {
const api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.testnet.empooriochain.org") });
const metadata = await api.query.assets.metadata(assetId);
return metadata.toHuman();
}
name().then(nftName => {
console.log("NFT Name:", nftName);
})
.catch(error => {
console.error("Error:", error);
});
Comparación con la interfaz ERC-721
function symbol() external view returns (string); // Returns the token collection symbolEthereum expone name()/symbol() como funciones de vista del contrato; EmpoorioChain expone el equivalente como lecturas de almacenamiento contra el pallet.
// EmpoorioChain: query on-chain asset metadata via @polkadot/api
// against the pallet_emp_assets metadata storage for the given assetId.
// See the EmpoorioChain SDK docs for the exact metadata query shape.
import { ApiPromise, WsProvider } from "@polkadot/api";
async function symbol(assetId) {
const api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.testnet.empooriochain.org") });
const metadata = await api.query.assets.metadata(assetId);
return metadata.toHuman();
}
symbol().then(nftSymbol => {
console.log("NFT Symbol:", nftSymbol);
}).catch(error => {
console.error("Error:", error);
});
Leer la URI/atributos de un ítem
function tokenURI(uint256 _tokenId) external view returns (string); // Returns the tokenURI of the tokenSe almacena en la entrada de metadatos del ítem, establecida en el momento de la acuñación mediante el extrínseco de metadatos del pallet.
import { createUmi } from "@polkadot/api";
import { fetchDigitalAsset, mplTokenMetadata } from "@polkadot/api";
import { PublicKey } from "@polkadot/api";
const mintAddress = new PublicKey("Token Address");
async function tokenURI( /* no tokenId */ ) {
try {
const umi = new WsProvider("wss://rpc.testnet.empooriochain.org");
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);
});
Cómo comprobar la propiedad
function ownerOf(uint256 _tokenId) public view returns (address) // Returns the owner of the tokenId tokenComprobamos el propietario de un ítem mediante api.query.uniques.asset(collectionId, itemId), usando el par (colección, ítem) en lugar de un simple ID de token.
import { ApiPromise, WsProvider } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
async function ownerOf(collectionId, itemId) {
const item = await api.query.uniques.asset(collectionId, itemId);
return item.isSome ? item.unwrap().owner.toString() : null;
}
ownerOf(1, 1).then(owner => {
console.log(owner);
}).catch(error => {
console.error("Error:", error);
});
Cómo transferir un ítem
function transferFrom(address _from, address _to, uint256 _tokenId) external payable; // Transfers tokenId token from _from to _toEn EmpoorioChain, cada ítem pertenece directamente a un AccountId32 — no hay que buscar primero una cuenta de token derivada. Un único extrínseco uniques.transfer transfiere la propiedad.
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
const keyring = new Keyring({ type: "sr25519" });
const owner = keyring.addFromUri("//your-seed-here");
// pallet_uniques identifies NFTs by (collectionId, itemId) rather than a
// single contract address + tokenId — there is no separate "associated
// token account" to derive first, the recipient's AccountId32 receives it.
async function transferFrom(collectionId, itemId, to) {
const tx = api.tx.uniques.transfer(collectionId, itemId, to);
const hash = await tx.signAndSend(owner);
return hash.toString();
}
transferFrom(1, 1, "5FHneW...")
.then(result => {
console.log("transferFrom result:", result);
})
.catch(error => {
console.error("Error:", error);
});
Las transferencias con comisión patrocinada (pagadas por un relayer), en las que un marketplace o una app quiere cubrir la comisión, se gestionan mediante el pallet paymaster nativo de EmpoorioChain, en lugar de con un patrón específico.
// A relayer can submit a user-signed transfer and cover the fee itself —
// EmpoorioChain does not require the receiver to pre-create a token
// account before an NFT can be sent to them.
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
const keyring = new Keyring({ type: "sr25519" });
const relayer = keyring.addFromUri("//relayer-seed-here");
async function transferFromSponsored(collectionId, itemId, to, signedPayload) {
const tx = api.tx.uniques.transfer(collectionId, itemId, to);
const hash = await tx.signAndSend(relayer, { payload: signedPayload });
return hash.toString();
}
¿Quieres explorar más? Consulta la referencia de pallets para ver la lista completa de extrínsecos de pallet-uniques.


