Developing the Frontend Marketplace with Corresponding Functions
Dự án marketplace bao gồm các trang chính:
- Trang chính (Listed Orders): Hiển thị danh sách NFT đang được bán trên marketplace (từ smart contract).
- Trang Profile: Hiển thị NFT trong ví người dùng và NFT đang bán trên sàn.
- Trang Chi Tiết NFT (Asset): Hiển thị metadata, giá, và các action như mua, bán, update giá hoặc withdraw.
Trong hướng dẫn này, chúng ta sẽ:
- Query dữ liệu trực tiếp từ blockchain và hiển thị lên giao diện.
- Tích hợp logic off-chain để build và thực hiện giao dịch (sử dụng ví trình duyệt).
Lý do sử dụng backend cho query và build giao dịch:
- Bảo mật API key (ví dụ: Blockfrost Project ID) và tránh lộ thông tin.
- Tối ưu hiệu suất bằng caching (sử dụng Next.js cache hoặc database) để giảm thời gian query blockchain.
- Ngăn chặn người dùng can thiệp vào dữ liệu (ví dụ: thay đổi tham số giao dịch).
- Dự án Next.js đã có giao diện cơ bản (frontend với các trang listed, profile, asset).
- Thư viện cần cài: Axios (cho API calls), SWR (cho data fetching), Shadcn/UI (cho components như dialog, input, button).
- File off-chain logic từ script trước (ví dụ: mesh.js, index.js, protocol.json) – copy vào folder
contracts.
- Biến môi trường:
PROJECT_ID (Blockfrost), NEXT_PUBLIC_APP_NETWORK (mainnet/testnet).
Cài đặt thư viện:
npm install axios swr @shadcn/ui
Cấu Hình Thư Viện Và Provider
1. Cấu Hình Blockfrost Provider
Tạo folder lib và file blockfrost.js:
- Sử dụng singleton để tránh khởi tạo lặp lại:
import { Blockfrost } from '@meshsdk/core'; // Import từ contracts
let blockfrostInstance = null;
export const getBlockfrost = () => {
if (!blockfrostInstance) {
blockfrostInstance = new Blockfrost({
projectId: process.env.PROJECT_ID,
network: process.env.NEXT_PUBLIC_APP_NETWORK,
});
}
return blockfrostInstance;
};
2. Cấu Hình Axios Cho API Calls
Trong lib/axios.js:
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
});
export const get = (url, params) => api.get(url, { params });
export const post = (url, data) => api.post(url, data);
3. Cấu Hình SWR Trong Layout
Thêm SWR provider vào layout.js để quản lý data fetching:
import { SWRConfig } from 'swr';
export default function RootLayout({ children }) {
return (
<SWRConfig value={{ fetcher: (url) => get(url).then(res => res.data) }}>
{children}
</SWRConfig>
);
}
Tạo folder app/api với các endpoints sử dụng route handlers của Next.js.
1. API Listed Orders (api/listed/route.js)
Query danh sách NFT đang bán từ smart contract:
import { getBlockfrost } from '@/lib/blockfrost';
import { Marketplace } from '@/contracts'; // Từ contracts
export async function GET() {
const bf = getBlockfrost();
const marketplace = Marketplace.fromAddress(process.env.MARKETPLACE_ADDRESS);
const utxos = await bf.utxosAt(marketplace.address);
const nfts = utxos
.map(utxo => {
const datum = marketplace.redeemDatum(utxo.output().datum());
if (!datum) return null;
return {
unit: datum.unit,
seller: datum.seller,
price: datum.price,
};
})
.filter(Boolean);
return Response.json(nfts);
}
2. API Profile (api/profile/route.js)
Query NFT trong ví và NFT đang bán:
import { getBlockfrost } from '@/lib/blockfrost';
export async function GET(req) {
const { searchParams } = new URL(req.url);
const address = searchParams.get('address');
if (!address) return Response.json([]);
const bf = getBlockfrost();
const [assets, utxos] = await Promise.all([
bf.assetsByAddress(address),
bf.utxosAt(process.env.MARKETPLACE_ADDRESS),
]);
const ownedNfts = assets.filter(asset => asset.unit !== 'lovelace').map(asset => ({ unit: asset.unit }));
const listedNfts = utxos
.map(utxo => {
const datum = Marketplace.redeemDatum(utxo.output().datum());
if (datum && datum.seller === address) return { unit: datum.unit, seller: datum.seller, price: datum.price };
return null;
})
.filter(Boolean);
return Response.json({ owned: ownedNfts, listed: listedNfts });
}
3. API Asset Details (api/asset/route.js)
Query metadata và datum của NFT:
import { getBlockfrost } from '@/lib/blockfrost';
export async function GET(req) {
const { searchParams } = new URL(req.url);
const unit = searchParams.get('unit');
if (!unit) return Response.json(null);
const bf = getBlockfrost();
const metadata = await bf.assetsMetadata(unit);
const utxos = await bf.utxosAt(process.env.MARKETPLACE_ADDRESS);
const utxo = utxos.find(u => u.output().amount().has(unit)); // Lấy UTXO chứa unit
const datum = utxo ? Marketplace.redeemDatum(utxo.output().datum()) : null;
return Response.json({
metadata,
datum: datum ? { seller: datum.seller, price: datum.price / 1000000 } : null, // Chia giá cho 1e6 nếu cần
});
}
4. API Submit Transaction (api/submit/route.js)
Để submit transaction đã ký:
import { getBlockfrost } from '@/lib/blockfrost';
export async function POST(req) {
const { signedTx } = await req.json();
const bf = getBlockfrost();
const txHash = await bf.submitTx(signedTx);
return Response.json({ txHash });
}
Tích Hợp Frontend Với Data Fetching
Sử dụng SWR để fetch dữ liệu trong components.
import useSWR from 'swr';
export default function ListedPage() {
const { data: nfts } = useSWR('/api/listed');
return (
<div>
{nfts?.map(nft => (
<div key={nft.unit}>{nft.unit} - Price: {nft.price / 1000000} ADA</div>
))}
</div>
);
}
Tương tự, fetch với params address từ ví người dùng (sử dụng useWallet để lấy address).
Fetch metadata và datum, hiển thị button action dựa trên seller (update/withdraw nếu là owner, buy nếu không).
Xử Lý Giao Dịch (Transaction Actions)
Tạo component TxButton để xử lý sale, buy, update, withdraw:
- Sử dụng dialog để nhập giá (nếu cần).
- Build transaction ở backend (post đến
/api/sale, /api/buy, v.v.).
- Ký transaction với browser wallet (useWallet).
- Submit signedTx đến
/api/submit.
Ví dụ config:
const config = {
sale: { endpoint: '/sale', requirePrice: true },
buy: { endpoint: '/buy', requirePrice: false },
// ...
};
- Frontend: Chỉ xử lý ký transaction và ký message (signData). Không build hoặc submit giao dịch để tránh can thiệp.
- Backend: Build giao dịch, query dữ liệu, caching để tối ưu (sử dụng Next.js revalidate hoặc database).
- Bảo mật: Không lộ private key; sử dụng backend để quản lý datum và UTXO.
Bằng cách theo các bước trên, bạn có thể hoàn thiện frontend marketplace với đầy đủ chức năng tương tác blockchain. Trong video tiếp theo, chúng ta sẽ deploy lên production. Nếu có vấn đề, kiểm tra lỗi và debug từng bước.
Bài Tập
📝 Bài tập 1: Hiển thị danh sách NFT trên Marketplace
Tạo giao diện hiển thị danh sách NFT hiện có trong Marketplace.
- Dữ liệu NFT gồm: hình ảnh, tên, mô tả, giá.
- Sử dụng React hoặc Next.js để hiển thị danh sách từ mảng JSON tạm thời.
- Giao diện chia thành các thẻ (card) cho từng NFT.
Cách giải
Dùng map() để lặp qua mảng NFT và hiển thị từng phần tử.
const nfts = [
{ id: 1, name: "NFT #1", image: "/nft1.png", price: "100 ADA" },
{ id: 2, name: "NFT #2", image: "/nft2.png", price: "150 ADA" },
];
export default function Marketplace() {
return (
<div className="grid grid-cols-2 gap-4">
{nfts.map((nft) => (
<div key={nft.id} className="border p-4 rounded-xl text-center">
<img src={nft.image} alt={nft.name} className="w-full rounded-lg" />
<h2 className="font-bold">{nft.name}</h2>
<p>{nft.price}</p>
</div>
))}
</div>
);
}
📝 Bài tập 2: Thêm nút “Mua NFT”
Tạo nút “Mua NFT” để người dùng có thể mua một NFT trong Marketplace.
- Thêm nút
Mua ngay dưới mỗi NFT.
- Khi nhấn, hiển thị thông báo “Đang xử lý giao dịch…” và sau 3 giây hiển thị “Mua thành công!”.
Cách giải
Dùng useState để quản lý trạng thái loading.
import { useState } from "react";
function NFTCard({ nft }) {
const [status, setStatus] = useState("");
const handleBuy = () => {
setStatus("Đang xử lý giao dịch...");
setTimeout(() => setStatus("Mua thành công!"), 3000);
};
return (
<div className="border p-4 text-center rounded-xl">
<img src={nft.image} alt={nft.name} />
<h2>{nft.name}</h2>
<p>{nft.price}</p>
<button onClick={handleBuy}>Mua ngay</button>
<p>{status}</p>
</div>
);
}
📝 Bài tập 3: Xử lý kết nối ví trước khi giao dịch
Kiểm tra người dùng đã kết nối ví trước khi thực hiện giao dịch mua NFT.
- Nếu ví chưa kết nối, hiển thị thông báo “Vui lòng kết nối ví trước khi mua”.
- Nếu đã kết nối, cho phép mua bình thường.
- Biến trạng thái ví:
isConnected (true/false).
Cách giải
Sử dụng điều kiện if (isConnected) trước khi gọi hàm mua.
const isConnected = false;
function handleBuy() {
if (!isConnected) {
alert("Vui lòng kết nối ví trước khi mua.");
return;
}
alert("Đang xử lý giao dịch...");
}
📝 Bài tập 4: Thêm trang chi tiết NFT
Tạo trang hiển thị thông tin chi tiết khi người dùng nhấn vào một NFT.
- Khi click vào NFT, chuyển đến trang
/nft/[id].
- Hiển thị chi tiết gồm: hình ảnh, mô tả, giá, nút “Mua”.
Cách giải
Dùng useRouter() trong Next.js để điều hướng đến trang chi tiết.
import { useRouter } from "next/router";
export default function NFTDetail({ nft }) {
const router = useRouter();
return (
<div className="p-6">
<img src={nft.image} alt={nft.name} />
<h1>{nft.name}</h1>
<p>{nft.description}</p>
<p>Giá: {nft.price}</p>
<button>Mua ngay</button>
<button onClick={() => router.back()}>Quay lại</button>
</div>
);
}
📝 Bài tập 5: Cập nhật UI sau khi giao dịch
Khi giao dịch mua NFT thành công, loại bỏ NFT khỏi danh sách hiển thị.
- Khi mua xong, NFT biến mất khỏi danh sách.
- Dùng
useState để cập nhật danh sách NFT.
Cách giải
Sử dụng filter() để loại NFT đã mua.
const [nfts, setNfts] = useState([
{ id: 1, name: "NFT #1" },
{ id: 2, name: "NFT #2" },
]);
function handleBuy(id) {
alert("Mua thành công!");
setNfts(nfts.filter((nft) => nft.id !== id));
}