Ứng dụng mint NFT trên Sui blockchain với 2 loại NFT: Random Memory NFT và Self Introduction NFT.
- Random Memory NFT: Mint NFT ngẫu nhiên từ templates có sẵn
- Self Introduction NFT: Tạo NFT với thông tin tùy chỉnh
- Real-time Transaction: Hiển thị kết quả transaction ngay lập tức
- Wallet Integration: Tích hợp với Sui wallet
Trước khi bắt đầu, đảm bảo bạn có:
- Node.js (v18+)
- pnpm hoặc npm
- Sui wallet (Sui Wallet, Suiet, hoặc các wallet khác)
- SUI testnet tokens
# Tạo project React với Vite
pnpm create vite simple-sui-app --template react-ts
cd simple-sui-app
# Cài đặt dependencies
pnpm install# Cài đặt Sui SDK và dapp-kit
pnpm add @mysten/sui @mysten/dapp-kit @tanstack/react-query
# Cài đặt Tailwind CSS
pnpm add tailwindcss @tailwindcss/viteTạo file tailwind.config.ts:
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
tailwindcss(),
],
})Cập nhật src/index.css:
@import "tailwindcss";Cập nhật src/main.tsx:
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import {
createNetworkConfig,
SuiClientProvider,
WalletProvider,
} from "@mysten/dapp-kit";
import { getFullnodeUrl } from "@mysten/sui/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Create Sui client for testnet
const { networkConfig } = createNetworkConfig({
testnet: { url: getFullnodeUrl("testnet") },
});
const queryClient = new QueryClient();
createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<SuiClientProvider networks={networkConfig} defaultNetwork="testnet">
<WalletProvider>
<App />
</WalletProvider>
</SuiClientProvider>
</QueryClientProvider>
);mkdir src/componentsinterface TransactionResultProps {
isLoading: boolean;
mintResult: any;
}
export function TransactionResult({ isLoading, mintResult }: TransactionResultProps) {
return (
<>
{/* Loading State */}
{isLoading && (
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<span className="ml-3 text-blue-700 font-medium">Minting your NFT...</span>
</div>
</div>
)}
{/* Success Result */}
{mintResult && !mintResult.error && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center mb-2">
<svg
className="w-5 h-5 text-green-500 mr-2"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
<h3 className="text-green-800 font-semibold">NFT Minted Successfully!</h3>
</div>
<div className="text-sm text-green-700">
<p className="mb-2">
<strong>Transaction Digest:</strong>
</p>
<code className="bg-green-100 px-2 py-1 rounded text-xs break-all">
{mintResult.digest}
</code>
<p className="mt-3 text-xs">
<strong>Status:</strong> Confirmed
</p>
</div>
</div>
)}
{/* Error Result */}
{mintResult && mintResult.error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center mb-2">
<svg
className="w-5 h-5 text-red-500 mr-2"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
<h3 className="text-red-800 font-semibold">Minting Failed</h3>
</div>
<p className="text-red-700 text-sm">
{mintResult.error.message || "An error occurred while minting the NFT."}
</p>
</div>
)}
</>
);
}import { useCurrentAccount, useSignAndExecuteTransaction } from "@mysten/dapp-kit";
import { Transaction } from "@mysten/sui/transactions";
interface RandomMemoryNFTProps {
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
mintResult: any;
setMintResult: (result: any) => void;
}
export function RandomMemoryNFT({ isLoading, setIsLoading, mintResult, setMintResult }: RandomMemoryNFTProps) {
const account = useCurrentAccount();
const signAndExecuteTransaction = useSignAndExecuteTransaction();
async function handleMintRandomMemoryNFT() {
if (!account) return;
setIsLoading(true);
setMintResult(null);
// define a programmable transaction
const tx = new Transaction();
const packageObjectId =
"0x489563cb7a99e87528b871f6f5df62100e96374d7cfc9432af7907f119049151";
// MemoryTemplateStore object ID trên testnet
const memoryTemplateStoreId =
"0x0b8391f4a847b3c9b1ec9a4820939906c8520714dcf5f1b4b503f8ab3c33f4c0";
tx.moveCall({
target: `${packageObjectId}::my_nft_collection::mint_random_memory_nft`,
arguments: [
tx.object(memoryTemplateStoreId), // MemoryTemplateStore reference
],
});
try {
// execute the programmable transaction
const resData = await signAndExecuteTransaction.mutateAsync({
transaction: tx,
});
console.log("Memory NFT minted successfully!", resData);
setMintResult(resData);
} catch (e) {
console.error("Memory NFT mint failed", e);
setMintResult({ error: e });
} finally {
setIsLoading(false);
}
}
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Mint Random Memory NFT</h2>
<p className="text-gray-600 mb-6">
Mint a random memory NFT from the available templates in the collection.
</p>
<button
onClick={handleMintRandomMemoryNFT}
disabled={!account || isLoading}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
{isLoading
? "Minting..."
: account
? "Mint Random Memory NFT"
: "Connect Wallet First"}
</button>
</div>
);
}import { useCurrentAccount, useSignAndExecuteTransaction } from "@mysten/dapp-kit";
import { Transaction } from "@mysten/sui/transactions";
import { useState } from "react";
interface SelfIntroductionNFTProps {
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
mintResult: any;
setMintResult: (result: any) => void;
}
export function SelfIntroductionNFT({ isLoading, setIsLoading, mintResult, setMintResult }: SelfIntroductionNFTProps) {
const account = useCurrentAccount();
const signAndExecuteTransaction = useSignAndExecuteTransaction();
const [selfIntroForm, setSelfIntroForm] = useState({
name: "",
description: "",
imageUrl: "",
slogan: "",
});
async function handleMintSelfIntroductionNFT() {
if (!account) return;
// Validate form
if (
!selfIntroForm.name ||
!selfIntroForm.description ||
!selfIntroForm.imageUrl ||
!selfIntroForm.slogan
) {
alert("Please fill in all fields");
return;
}
setIsLoading(true);
setMintResult(null);
// define a programmable transaction
const tx = new Transaction();
const packageObjectId =
"0x489563cb7a99e87528b871f6f5df62100e96374d7cfc9432af7907f119049151";
// @ts-ignore - SDK version compatibility issue
tx.moveCall({
target: `${packageObjectId}::my_nft_collection::mint_self_introduction_nft`,
arguments: [
tx.pure.string(selfIntroForm.name),
tx.pure.string(selfIntroForm.description),
tx.pure.string(selfIntroForm.imageUrl),
tx.pure.string(selfIntroForm.slogan),
],
});
try {
// execute the programmable transaction
const resData = await signAndExecuteTransaction.mutateAsync({
transaction: tx,
});
console.log("Self Introduction NFT minted successfully!", resData);
setMintResult(resData);
// Reset form
setSelfIntroForm({
name: "",
description: "",
imageUrl: "",
slogan: "",
});
} catch (e) {
console.error("Self Introduction NFT mint failed", e);
setMintResult({ error: e });
} finally {
setIsLoading(false);
}
}
return (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Mint Self Introduction NFT</h2>
<p className="text-gray-600 mb-6">
Create your own self introduction NFT with custom details.
</p>
<div className="space-y-4 mb-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<input
type="text"
value={selfIntroForm.name}
onChange={(e) =>
setSelfIntroForm({ ...selfIntroForm, name: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter your name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={selfIntroForm.description}
onChange={(e) =>
setSelfIntroForm({
...selfIntroForm,
description: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter your description"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Image URL
</label>
<input
type="url"
value={selfIntroForm.imageUrl}
onChange={(e) =>
setSelfIntroForm({
...selfIntroForm,
imageUrl: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://example.com/image.jpg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Slogan
</label>
<input
type="text"
value={selfIntroForm.slogan}
onChange={(e) =>
setSelfIntroForm({
...selfIntroForm,
slogan: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter your slogan"
/>
</div>
</div>
<button
onClick={handleMintSelfIntroductionNFT}
disabled={!account || isLoading}
className="w-full bg-green-500 hover:bg-green-600 disabled:bg-gray-300 text-white font-medium py-2 px-4 rounded-lg transition-colors"
>
{isLoading
? "Minting..."
: account
? "Mint Self Introduction NFT"
: "Connect Wallet First"}
</button>
</div>
);
}Cập nhật src/App.tsx:
import { ConnectButton } from "@mysten/dapp-kit";
import { useState } from "react";
import { RandomMemoryNFT } from "./components/RandomMemoryNFT";
import { SelfIntroductionNFT } from "./components/SelfIntroductionNFT";
import { TransactionResult } from "./components/TransactionResult";
function App() {
const [mintResult, setMintResult] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
return (
<div className="min-h-screen bg-gray-50">
<header className="flex justify-between items-center p-4 bg-white shadow-sm">
<h1 className="text-2xl font-bold">Memory NFT Collection</h1>
<ConnectButton />
</header>
<main className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Random Memory NFT Section */}
<RandomMemoryNFT
isLoading={isLoading}
setIsLoading={setIsLoading}
mintResult={mintResult}
setMintResult={setMintResult}
/>
{/* Self Introduction NFT Section */}
<SelfIntroductionNFT
isLoading={isLoading}
setIsLoading={setIsLoading}
mintResult={mintResult}
setMintResult={setMintResult}
/>
</div>
{/* Global Transaction Result */}
<div className="max-w-4xl mx-auto mt-8">
<TransactionResult isLoading={isLoading} mintResult={mintResult} />
</div>
</main>
</div>
);
}
export default App;# Chạy development server
pnpm devTruy cập http://localhost:5173 để xem ứng dụng.
- Package ID:
0x489563cb7a99e87528b871f6f5df62100e96374d7cfc9432af7907f119049151 - MemoryTemplateStore:
0x0b8391f4a847b3c9b1ec9a4820939906c8520714dcf5f1b4b503f8ab3c33f4c0 - Network: Sui Testnet
mint_random_memory_nft: Mint NFT ngẫu nhiên từ templatesmint_self_introduction_nft: Mint NFT với thông tin tùy chỉnh
- Wallet connection với @mysten/dapp-kit
- Random Memory NFT minting
- Self Introduction NFT minting với form
- Real-time transaction status
- Loading states và error handling
- Responsive design với Tailwind CSS
- Component-based architecture
- TypeScript support
- Dual NFT Types: Hỗ trợ 2 loại NFT khác nhau
- Form Validation: Kiểm tra input trước khi mint
- Transaction Feedback: Hiển thị kết quả ngay lập tức
- Error Handling: Xử lý lỗi gracefully
- Responsive UI: Hoạt động tốt trên mobile và desktop
- Wallet không kết nối: Đảm bảo wallet đang ở testnet
- Transaction failed: Kiểm tra SUI balance trên testnet
- TypeScript errors: Có thể bỏ qua các warning về SDK compatibility
# Xem logs trong browser console
F12 > Console
# Kiểm tra network requests
F12 > Network- Fork repository
- Tạo feature branch
- Commit changes
- Push to branch
- Tạo Pull Request
MIT License - xem file LICENSE để biết thêm chi tiết.
Happy Minting! 🎉