
Making Transaction
Bài viết này hướng dẫn cách tạo giao dịch chuyển tiền trên blockchain Cardano, sử dụng thư viện MeshJS trong một dự án Next.js. Nội dung được thiết kế cho các nhà phát triển xây dựng ứng dụng phi tập trung (dApps) trên Cardano, bao gồm cả việc xây dựng giao dịch ở phía client và server. Bài viết cung cấp giao diện mẫu, logic xử lý giao dịch, và giải thích lý do nên ưu tiên xây dựng giao dịch trên server trong các ứng dụng thực tế.
Yêu Cầu Chuẩn Bị
Trước khi bắt đầu, hãy đảm bảo bạn có:
- Node.js và npm: Cài đặt Node.js (khuyến nghị phiên bản 16 trở lên) và npm để quản lý phụ thuộc JavaScript.
- Trình Soạn Thảo Mã: Sử dụng trình soạn thảo như Visual Studio Code.
- Ví Cardano: Thiết lập ví Cardano (như Eternl) với một lượng test ADA (tADA) trên mạng thử nghiệm Cardano testnet.
- Tài Khoản Blockfrost (cho server-side): Đăng ký tại Blockfrost Dashboard để nhận Project ID nếu xây dựng giao dịch ở phía server.
- Kiến Thức Cơ Bản: Hiểu biết về JavaScript, API REST, và các khái niệm blockchain như UTXO, giao dịch, và ký giao dịch.
Tổng Quan Về Giao Dịch Cardano
Giao dịch trên Cardano là quá trình chuyển tài sản (như ADA) từ một ví sang ví khác, được thực hiện thông qua các bước:
- Xây Dựng Giao Dịch: Tạo giao dịch với các thông tin như địa chỉ người nhận, số lượng ADA, và UTXO (unspent transaction outputs) từ ví người gửi.
- Ký Giao Dịch: Sử dụng ví để ký giao dịch, đảm bảo tính xác thực.
- Gửi Giao Dịch: Submit giao dịch lên blockchain để các validator xác nhận và ghi vào sổ cái.
Giao dịch có thể được xây dựng ở:
- Phía Client: Phù hợp cho các ứng dụng đơn giản, nhưng kém bảo mật vì logic giao dịch có thể bị lộ.
- Phía Server: An toàn hơn, đặc biệt với các giao dịch phức tạp như đa chữ ký (multisig), vì logic được xử lý trên server và không bị lộ cho người dùng.
Thiết Lập Giao Diện Người Dùng (UI)
Chúng ta sẽ tạo một giao diện đơn giản để người dùng nhập địa chỉ nhận và số lượng ADA, sau đó thực hiện giao dịch. Giao diện bao gồm:
- Nút Connect Wallet: Kết nối với ví Cardano (như Eternl) để lấy thông tin ví và ký giao dịch.
- Form Nhập Dữ Liệu: Ô input cho địa chỉ người nhận và số lượng ADA.
- Nút Tạo Giao Dịch: Thực hiện xây dựng, ký, và gửi giao dịch.
Bước 1: Cài Đặt Dự Án Next.js
Giả sử bạn đã tạo một dự án Next.js (nếu chưa, chạy lệnh npx create-next-app@latest). Cài đặt MeshJS:
npm install @meshsdk/core@1.8.14
Cập nhật next.config.js để hỗ trợ MeshJS với App Router:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: ['@meshsdk/core'],
};
module.exports = nextConfig;
Bước 2: Tạo Thành Phần Connect Wallet
Tạo tệp app/components/WalletConnect.jsx để xử lý kết nối ví:
import { useState, useEffect } from 'react';
import { BrowserWallet } from '@meshsdk/core';
export default function WalletConnect({ setWallet }) {
const [address, setAddress] = useState('');
const [balance, setBalance] = useState(0);
const connectWallet = async () => {
try {
const wallets = BrowserWallet.getInstalledWallets();
if (wallets.length === 0) {
alert('No wallet found. Please install a Cardano wallet (e.g., Eternl).');
return;
}
const selectedWallet = await BrowserWallet.enable('eternl');
setWallet(selectedWallet);
const addresses = await selectedWallet.getUsedAddresses();
setAddress(addresses[0]);
const balance = await selectedWallet.getBalance();
setBalance(balance.find(asset => asset.unit === 'lovelace').quantity / 1000000); // Convert lovelace to ADA
} catch (error) {
console.error('Error connecting wallet:', error);
alert('Failed to connect wallet.');
}
};
const disconnectWallet = () => {
setWallet(null);
setAddress('');
setBalance(0);
};
return (
<div>
{address ? (
<div>
<p>Connected: {address.slice(0, 8)}...{address.slice(-8)}</p>
<p>Balance: {balance} ADA</p>
<button onClick={disconnectWallet}>Disconnect Wallet</button>
</div>
) : (
<button onClick={connectWallet}>Connect Wallet</button>
)}
</div>
);
}
Bước 3: Tạo Trang Gửi Giao Dịch
Tạo tệp app/send/page.jsx cho trang /send với form nhập dữ liệu và logic giao dịch:
'use client';
import { useState, useEffect } from 'react';
import { Transaction, BrowserWallet } from '@meshsdk/core';
import WalletConnect from '../components/WalletConnect';
export default function Send() {
const [wallet, setWallet] = useState(null);
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [balance, setBalance] = useState(0);
useEffect(() => {
const getBalance = async () => {
if (wallet) {
try {
const balance = await wallet.getBalance();
setBalance(balance.find(asset => asset.unit === 'lovelace').quantity / 1000000);
} catch (error) {
console.error('Error fetching balance:', error);
}
} else {
setBalance(0);
}
};
getBalance();
}, [wallet]);
const createTransaction = async () => {
if (!wallet) {
alert('Please connect a wallet first.');
return;
}
if (!recipient || !amount) {
alert('Please fill in recipient address and amount.');
return;
}
try {
const amountInLovelace = String(Number(amount) * 1000000); // Convert ADA to lovelace
const tx = new Transaction({ initiator: wallet });
tx.sendValue(
{ lovelace: amountInLovelace },
recipient
);
const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
alert(`Transaction submitted: ${txHash}`);
console.log('Transaction Hash:', txHash);
} catch (error) {
console.error('Error creating transaction:', error);
alert('Failed to create transaction.');
}
};
return (
<main>
<h1>Send ADA</h1>
<WalletConnect setWallet={setWallet} />
<div>
<h2>Recipient Information</h2>
<div>
<label>Recipient Address:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Enter recipient address"
/>
</div>
<div>
<label>Amount (ADA):</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Enter amount in ADA"
/>
</div>
<button onClick={createTransaction}>Create Transaction</button>
</div>
<p>Balance: {balance} ADA</p>
</main>
);
}
Giải Thích Mã:
- State Management: Sử dụng
useStateđể lưu trữ địa chỉ người nhận (recipient) và số lượng ADA (amount). - Wallet Balance:
useEffecttự động cập nhật số dư ví khiwalletthay đổi. - Transaction Logic:
- Kiểm tra ví đã kết nối và thông tin đầu vào đầy đủ.
- Chuyển đổi số lượng ADA thành lovelace (1 ADA = 1,000,000 lovelace).
- Sử dụng
Transactiontừ MeshJS để xây dựng giao dịch, gửi ADA đến địa chỉ người nhận. build()tạo giao dịch chưa ký (unsignedTx).signTx()yêu cầu ví ký giao dịch (hiển thị pop-up để nhập mật khẩu).submitTx()gửi giao dịch lên blockchain, trả về hash giao dịch.
- Error Handling: Hiển thị thông báo lỗi nếu thiếu ví hoặc thông tin đầu vào.
Kiểm Tra Giao Dịch:
- Chạy dự án:
npm run dev. - Truy cập
http://localhost:3000/send. - Kết nối ví Eternl, nhập địa chỉ người nhận và số lượng ADA (ví dụ: 1000 ADA), nhấn “Create Transaction”.
- Kiểm tra hash giao dịch trên Cardano Testnet Explorer.
Xây Dựng Giao Dịch Ở Phía Server
Xây dựng giao dịch ở phía client đơn giản nhưng có hạn chế:
- Bảo Mật: Logic giao dịch có thể bị lộ, cho phép người dùng chỉnh sửa hoặc tạo giao dịch giả mạo.
- Hạn Chế Chức Năng: Không hỗ trợ các giao dịch phức tạp như đa chữ ký (multisig).
Xây dựng ở phía server an toàn hơn và phù hợp với các ứng dụng thực tế.
Bước 4: Tạo API Route Cho Giao Dịch
Tạo tệp app/api/cardano/send/route.js để xử lý giao dịch trên server:
import { NextResponse } from 'next/server';
import { Transaction } from '@meshsdk/core';
export async function POST(request) {
try {
const { sender, receiver, amount } = await request.json();
if (!sender || !receiver || !amount) {
return NextResponse.json({ error: 'Missing sender, receiver, or amount' }, { status: 400 });
}
// Lấy UTXO từ Blockfrost
const projectId = 'preprodYourProjectIdHere'; // Thay bằng Project ID của bạn
const response = await fetch(`https://cardano-preprod.blockfrost.io/api/v0/addresses/${sender}/utxos`, {
headers: { project_id: projectId },
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const utxos = await response.json();
if (utxos.length === 0) {
return NextResponse.json({ error: 'No UTXOs found for sender address' }, { status: 400 });
}
// Format UTXO cho MeshJS
const formattedUtxos = utxos.map(utxo => ({
input: {
outputIndex: utxo.output_index,
txHash: utxo.tx_hash,
},
output: {
address: sender,
amount: utxo.amount.map(asset => ({
unit: asset.unit,
quantity: asset.quantity,
})),
},
}));
// Xây dựng giao dịch
const tx = new Transaction({ initiator: null }); // Không cần ví trên server
tx.setTxInputs(formattedUtxos);
tx.sendValue(
{ lovelace: String(Number(amount) * 1000000) }, // Convert ADA to lovelace
receiver
);
const unsignedTx = await tx.build();
return NextResponse.json({ unsignedTx });
} catch (error) {
console.error('Error building transaction:', error);
return NextResponse.json({ error: 'Failed to build transaction' }, { status: 500 });
}
}
Bước 5: Tích Hợp API Vào Trang Send
Cập nhật app/send/page.jsx để gọi API server thay vì xây dựng giao dịch trực tiếp:
'use client';
import { useState, useEffect } from 'react';
import { BrowserWallet } from '@meshsdk/core';
import WalletConnect from '../components/WalletConnect';
export default function Send() {
const [wallet, setWallet] = useState(null);
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const [balance, setBalance] = useState(0);
const [sender, setSender] = useState('');
useEffect(() => {
const getBalance = async () => {
if (wallet) {
try {
const balance = await wallet.getBalance();
setBalance(balance.find(asset => asset.unit === 'lovelace').quantity / 1000000);
const addresses = await wallet.getUsedAddresses();
setSender(addresses[0]);
} catch (error) {
console.error('Error fetching balance:', error);
}
} else {
setBalance(0);
setSender('');
}
};
getBalance();
}, [wallet]);
const createTransaction = async () => {
if (!wallet || !sender) {
alert('Please connect a wallet first.');
return;
}
if (!recipient || !amount) {
alert('Please fill in recipient address and amount.');
return;
}
try {
// Gửi yêu cầu đến server
const response = await fetch('/api/cardano/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sender, receiver: recipient, amount }),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const { unsignedTx } = await response.json();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
alert(`Transaction submitted: ${txHash}`);
console.log('Transaction Hash:', txHash);
} catch (error) {
console.error('Error creating transaction:', error);
alert('Failed to create transaction.');
}
};
return (
<main>
<h1>Send ADA</h1>
<WalletConnect setWallet={setWallet} />
<div>
<h2>Recipient Information</h2>
<div>
<label>Recipient Address:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="Enter recipient address"
/>
</div>
<div>
<label>Amount (ADA):</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Enter amount in ADA"
/>
</div>
<button onClick={createTransaction}>Create Transaction</button>
</div>
<p>Balance: {balance} ADA</p>
</main>
);
}
Giải Thích Mã:
- Server-Side:
- API route
/api/cardano/sendnhận thông tinsender,receiver, vàamounttừ client. - Sử dụng Blockfrost để lấy UTXO của địa chỉ người gửi.
- Format UTXO để tương thích với MeshJS (Blockfrost trả về định dạng khác với
wallet.getUtxos()). - Xây dựng giao dịch chưa ký (
unsignedTx) và trả về cho client.
- API route
- Client-Side:
- Gửi yêu cầu POST đến API với thông tin giao dịch.
- Nhận
unsignedTx, ký bằng ví (signTx), và gửi lên blockchain (submitTx).
- Lợi Ích:
- Logic giao dịch được xử lý trên server, tăng bảo mật.
- Hỗ trợ các giao dịch phức tạp như đa chữ ký bằng cách trả về
unsignedTxcho nhiều ví ký. - Ngăn người dùng can thiệp vào logic giao dịch.
Kiểm Tra Giao Dịch:
- Nhập địa chỉ người nhận (ví dụ: một địa chỉ testnet khác) và số lượng ADA (ví dụ: 250 ADA).
- Kiểm tra tab Network trong DevTools để xác nhận yêu cầu POST và phản hồi
unsignedTx. - Kiểm tra hash giao dịch trên Cardano Testnet Explorer.
Lợi Ích Của Xây Dựng Giao Dịch Ở Server
-
Bảo Mật:
- Logic giao dịch được ẩn trên server, ngăn người dùng đọc hoặc chỉnh sửa.
- Tránh lộ thông tin nhạy cảm như cách UTXO được chọn.
-
Hỗ Trợ Multisig:
- Giao dịch đa chữ ký yêu cầu nhiều bên ký. Server tạo
unsignedTx, sau đó mỗi bên ký riêng, đảm bảo tính linh hoạt.
- Giao dịch đa chữ ký yêu cầu nhiều bên ký. Server tạo
-
Tối Ưu Hiệu Suất:
- Server có thể sử dụng Blockfrost để lấy UTXO, đảm bảo dữ liệu chính xác và giảm tải cho client.
-
Kiểm Soát:
- Ngăn chặn việc tạo giao dịch giả mạo hoặc khai thác lỗ hổng.
Nhược điểm:
- Phụ thuộc vào server và Blockfrost.
- Cần thêm bước gọi API, có thể tăng độ trễ nhỏ.
Tài Liệu Tham Khảo
- MeshJS Documentation: Hướng dẫn sử dụng Transaction và BrowserWallet.
- Blockfrost API Documentation: Endpoint để lấy UTXO và gửi giao dịch.
- Cardano Developer Portal: Công cụ và tài liệu phát triển Cardano.
- Cardano Testnet Explorer: Kiểm tra giao dịch.
Kết Luận
Bài viết đã hướng dẫn cách tạo giao dịch chuyển tiền trên Cardano, từ xây dựng giao diện người dùng đến xử lý logic giao dịch ở cả phía client và server. Xây dựng giao dịch ở phía server được khuyến nghị cho các ứng dụng thực tế vì tính bảo mật và khả năng hỗ trợ các giao dịch phức tạp. Bạn có thể mở rộng bằng cách thêm hỗ trợ đa chữ ký hoặc tích hợp các tính năng khác như mint token, tham khảo tài liệu MeshJS và Blockfrost.
Bài Tập
Tạo một giao diện form trong Next.js để nhập địa chỉ ví nhận và số lượng ADA cần gửi.
- Tạo trang
/sendvới form chứa hai input: địa chỉ ví nhận và số lượng ADA. - Sử dụng state để quản lý dữ liệu nhập vào.
- Hiển thị thông báo lỗi nếu input trống khi nhấn nút gửi.
- Định dạng giao diện bằng CSS.
Cách giải
- Tạo trang
/send:- Tạo file
app/send/page.tsxđể chứa form. - Sử dụng
useStateđể quản lý địa chỉ ví nhận và số lượng ADA.
- Tạo file
- Xử lý input và lỗi:
- Thêm sự kiện
onChangecho input để cập nhật state. - Kiểm tra input trống khi nhấn nút gửi và hiển thị thông báo lỗi.
- Thêm sự kiện
- Định dạng giao diện:
- Sử dụng inline CSS hoặc file CSS riêng để tạo giao diện đẹp.
Tạo file app/send/page.tsx:
"use client";
import { useState } from "react";
export default function Send() {
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [error, setError] = useState("");
const handleSubmit = () => {
if (!recipient || !amount) {
setError("Vui lòng nhập đầy đủ địa chỉ ví và số lượng ADA");
return;
}
setError("");
// Logic gửi ADA sẽ được thêm ở bài tập sau
console.log("Recipient:", recipient, "Amount:", amount);
};
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<h1>Gửi ADA</h1>
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
<div style={{ marginBottom: "10px" }}>
<label>Địa chỉ ví nhận:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<div style={{ marginBottom: "10px" }}>
<label>Số lượng ADA:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<button
onClick={handleSubmit}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
}}
>
Gửi ADA
</button>
{error && <p style={{ color: "red", marginTop: "10px" }}>{error}</p>}
</div>
</div>
);
}
Chạy npm run dev, truy cập http://localhost:3000/send, nhập địa chỉ ví và số lượng ADA, nhấn nút “Gửi ADA” để kiểm tra console log và thông báo lỗi nếu input trống.
Tích hợp ví Cardano vào trang /send để hiển thị số dư ADA sau khi kết nối.
- Sử dụng MeshJS để kết nối ví (như Eternl).
- Hiển thị số dư ADA của ví sau khi kết nối.
- Hiển thị thông báo lỗi nếu ví chưa kết nối.
- Định dạng giao diện số dư.
Cách giải
- Cài đặt MeshJS:
- Cài đặt
@meshsdk/corevà@meshsdk/react.
- Cài đặt
- Tích hợp ví:
- Sử dụng hook
useWalletđể kết nối ví và lấy số dư. - Thêm nút “Connect Wallet” và hiển thị số dư sau khi kết nối.
- Sử dụng hook
- Xử lý lỗi:
- Kiểm tra trạng thái kết nối ví trước khi lấy số dư.
- Định dạng:
- Sử dụng inline CSS để hiển thị số dư.
Cài đặt:
npm install @meshsdk/core @meshsdk/react
Sửa file app/send/page.tsx:
"use client";
import { useState, useEffect } from "react";
import { useWallet } from "@meshsdk/react";
export default function Send() {
const { connect, wallet, connected } = useWallet();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [error, setError] = useState("");
const [balance, setBalance] = useState("");
useEffect(() => {
if (connected) {
async function fetchBalance() {
try {
const balance = await wallet.getBalance();
const ada =
balance.find((asset) => asset.unit === "lovelace")?.quantity || "0";
setBalance(`${parseInt(ada) / 1000000} ADA`);
} catch (err) {
setBalance("Lỗi khi lấy số dư");
}
}
fetchBalance();
}
}, [connected, wallet]);
const handleSubmit = () => {
if (!connected) {
setError("Vui lòng kết nối ví!");
return;
}
if (!recipient || !amount) {
setError("Vui lòng nhập đầy đủ địa chỉ ví và số lượng ADA");
return;
}
setError("");
console.log("Recipient:", recipient, "Amount:", amount);
};
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<h1>Gửi ADA</h1>
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
{!connected ? (
<button
onClick={() => connect("eternl")}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
marginBottom: "20px",
}}
>
Connect Wallet
</button>
) : (
<p style={{ marginBottom: "20px" }}>Số dư: {balance}</p>
)}
<div style={{ marginBottom: "10px" }}>
<label>Địa chỉ ví nhận:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<div style={{ marginBottom: "10px" }}>
<label>Số lượng ADA:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<button
onClick={handleSubmit}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
}}
>
Gửi ADA
</button>
{error && <p style={{ color: "red", marginTop: "10px" }}>{error}</p>}
</div>
</div>
);
}
Chạy npm run dev, truy cập http://localhost:3000/send, nhấn “Connect Wallet” để kết nối ví Eternl và hiển thị số dư ADA.
Tạo giao dịch gửi ADA trên client-side sử dụng MeshJS.
- Sử dụng MeshJS để xây dựng và ký giao dịch gửi ADA từ form
/send. - Kiểm tra ví đã kết nối và input hợp lệ trước khi tạo giao dịch.
- Hiển thị Tx Hash sau khi giao dịch thành công.
- Xử lý lỗi nếu giao dịch thất bại.
Cách giải
- Xây dựng giao dịch:
- Sử dụng
Transactiontừ@meshsdk/coređể tạo giao dịch. - Lấy địa chỉ ví nhận và số lượng ADA từ state.
- Sử dụng
- Ký và gửi giao dịch:
- Ký giao dịch bằng ví trình duyệt và submit lên blockchain.
- Xử lý lỗi:
- Kiểm tra kết nối ví và input, hiển thị thông báo lỗi nếu cần.
- Hiển thị Tx Hash:
- Hiển thị Tx Hash trong giao diện sau khi submit thành công.
Sửa file app/send/page.tsx:
"use client";
import { useState, useEffect } from "react";
import { useWallet } from "@meshsdk/react";
import { Transaction } from "@meshsdk/core";
export default function Send() {
const { connect, wallet, connected } = useWallet();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [error, setError] = useState("");
const [balance, setBalance] = useState("");
const [txHash, setTxHash] = useState("");
useEffect(() => {
if (connected) {
async function fetchBalance() {
try {
const balance = await wallet.getBalance();
const ada =
balance.find((asset) => asset.unit === "lovelace")?.quantity || "0";
setBalance(`${parseInt(ada) / 1000000} ADA`);
} catch (err) {
setBalance("Lỗi khi lấy số dư");
}
}
fetchBalance();
}
}, [connected, wallet]);
const handleSubmit = async () => {
if (!connected) {
setError("Vui lòng kết nối ví!");
return;
}
if (!recipient || !amount) {
setError("Vui lòng nhập đầy đủ địa chỉ ví và số lượng ADA");
return;
}
try {
const tx = new Transaction({ initiator: wallet });
tx.sendAssets(
{ address: recipient },
[{ unit: "lovelace", quantity: `${Number(amount) * 1000000}` }] // ADA to lovelace
);
const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
setTxHash(txHash);
setError("");
} catch (err) {
setError(`Lỗi: ${err.message}`);
}
};
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<h1>Gửi ADA</h1>
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
{!connected ? (
<button
onClick={() => connect("eternl")}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
marginBottom: "20px",
}}
>
Connect Wallet
</button>
) : (
<p style={{ marginBottom: "20px" }}>Số dư: {balance}</p>
)}
<div style={{ marginBottom: "10px" }}>
<label>Địa chỉ ví nhận:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<div style={{ marginBottom: "10px" }}>
<label>Số lượng ADA:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<button
onClick={handleSubmit}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
}}
>
Gửi ADA
</button>
{error && <p style={{ color: "red", marginTop: "10px" }}>{error}</p>}
{txHash && (
<p style={{ color: "green", marginTop: "10px" }}>
Giao dịch thành công! Tx Hash:{" "}
<a
href={`https://preprod.cardanoscan.io/transaction/${txHash}`}
target="_blank"
>
{txHash}
</a>
</p>
)}
</div>
</div>
);
}
Chạy npm run dev, truy cập http://localhost:3000/send, kết nối ví, nhập địa chỉ ví nhận và số lượng ADA, nhấn “Gửi ADA” để gửi giao dịch và xem Tx Hash trên CardanoScan.
Tạo giao dịch gửi ADA trên server-side sử dụng MeshJS và Blockfrost.
- Tạo API route
/api/cardano/sendđể xây dựng giao dịch unsigned. - Gửi thông tin người nhận, số lượng ADA, và địa chỉ người gửi từ client.
- Ký và submit giao dịch trên client-side.
- Hiển thị Tx Hash sau khi giao dịch thành công.
Cách giải
- Tạo API route:
- Tạo file
app/api/cardano/send/route.tsđể xây dựng giao dịch unsigned bằng MeshJS và Blockfrost. - Lấy UTxO từ Blockfrost dựa trên địa chỉ người gửi.
- Tạo file
- Gửi request từ client:
- Sửa
app/send/page.tsxđể gửi POST request đến API route với thông tin người gửi, người nhận, và số lượng ADA.
- Sửa
- Ký và submit:
- Nhận unsigned transaction từ server, ký bằng ví trên client, và submit.
- Hiển thị kết quả:
- Hiển thị Tx Hash hoặc lỗi trong giao diện.
Tạo file app/api/cardano/send/route.ts:
import { NextResponse } from "next/server";
import { Transaction } from "@meshsdk/core";
import { BlockFrostAPI } from "@blockfrost/blockfrost-js";
export async function POST(request: Request) {
try {
const { sender, recipient, amount } = await request.json();
if (!sender || !recipient || !amount) {
return NextResponse.json(
{ error: "Thiếu thông tin người gửi, người nhận hoặc số lượng ADA" },
{ status: 400 }
);
}
const api = new BlockFrostAPI({ projectId: "preprodYourProjectIdHere" }); // Thay bằng project ID của bạn
const utxos = await api.addressesUtxos(sender);
const formattedUtxos = utxos.map((utxo) => ({
input: { outputIndex: utxo.output_index, txHash: utxo.tx_hash },
output: { address: utxo.address, amount: utxo.amount },
}));
const tx = new Transaction();
tx.sendAssets({ address: recipient }, [
{ unit: "lovelace", quantity: `${Number(amount) * 1000000}` },
]);
tx.setTxInputs(formattedUtxos);
const unsignedTx = await tx.build();
return NextResponse.json({ unsignedTx });
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
Sửa file app/send/page.tsx:
"use client";
import { useState, useEffect } from "react";
import { useWallet } from "@meshsdk/react";
export default function Send() {
const { connect, wallet, connected, walletAddress } = useWallet();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [error, setError] = useState("");
const [balance, setBalance] = useState("");
const [txHash, setTxHash] = useState("");
useEffect(() => {
if (connected) {
async function fetchBalance() {
try {
const balance = await wallet.getBalance();
const ada =
balance.find((asset) => asset.unit === "lovelace")?.quantity || "0";
setBalance(`${parseInt(ada) / 1000000} ADA`);
} catch (err) {
setBalance("Lỗi khi lấy số dư");
}
}
fetchBalance();
}
}, [connected, wallet]);
const handleSubmit = async () => {
if (!connected) {
setError("Vui lòng kết nối ví!");
return;
}
if (!recipient || !amount) {
setError("Vui lòng nhập đầy đủ địa chỉ ví và số lượng ADA");
return;
}
try {
const response = await fetch("/api/cardano/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sender: walletAddress, recipient, amount }),
});
const { unsignedTx, error } = await response.json();
if (error) throw new Error(error);
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
setTxHash(txHash);
setError("");
} catch (err) {
setError(`Lỗi: ${err.message}`);
}
};
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<h1>Gửi ADA</h1>
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
{!connected ? (
<button
onClick={() => connect("eternl")}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
marginBottom: "20px",
}}
>
Connect Wallet
</button>
) : (
<p style={{ marginBottom: "20px" }}>Số dư: {balance}</p>
)}
<div style={{ marginBottom: "10px" }}>
<label>Địa chỉ ví nhận:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<div style={{ marginBottom: "10px" }}>
<label>Số lượng ADA:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<button
onClick={handleSubmit}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
}}
>
Gửi ADA
</button>
{error && <p style={{ color: "red", marginTop: "10px" }}>{error}</p>}
{txHash && (
<p style={{ color: "green", marginTop: "10px" }}>
Giao dịch thành công! Tx Hash:{" "}
<a
href={`https://preprod.cardanoscan.io/transaction/${txHash}`}
target="_blank"
>
{txHash}
</a>
</p>
)}
</div>
</div>
);
}
Chạy npm run dev, truy cập http://localhost:3000/send, kết nối ví, nhập địa chỉ ví nhận và số lượng ADA, nhấn “Gửi ADA” để gửi giao dịch qua server-side. Kiểm tra Tx Hash trên CardanoScan.
So sánh việc tạo giao dịch trên client-side và server-side, triển khai một giao diện để chuyển đổi giữa hai phương thức.
- Tạo trang
/sendvới tùy chọn chuyển đổi giữa client-side và server-side. - Hiển thị thời gian thực thi giao dịch cho mỗi phương thức.
- Liệt kê ưu/nhược điểm của mỗi phương thức.
- Đảm bảo giao diện hiển thị Tx Hash và lỗi.
Cách giải
- Tạo giao diện:
- Sửa
app/send/page.tsxđể thêm dropdown chọn phương thức (client-side/server-side). - Thêm biến state để theo dõi thời gian thực thi.
- Sửa
- Triển khai hai phương thức:
- Client-side: Sử dụng logic từ Bài tập 3.
- Server-side: Sử dụng logic từ Bài tập 4.
- Đo thời gian thực thi:
- Sử dụng
Date.now()để tính thời gian trước và sau khi thực hiện giao dịch.
- Sử dụng
- So sánh ưu/nhược điểm:
- Client-side: Nhanh hơn nhưng lộ logic.
- Server-side: Bảo mật hơn nhưng chậm hơn do request server.
Sửa file app/send/page.tsx:
"use client";
import { useState, useEffect } from "react";
import { useWallet } from "@meshsdk/react";
import { Transaction } from "@meshsdk/core";
export default function Send() {
const { connect, wallet, connected, walletAddress } = useWallet();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [error, setError] = useState("");
const [balance, setBalance] = useState("");
const [txHash, setTxHash] = useState("");
const [method, setMethod] = useState("client");
const [executionTime, setExecutionTime] = useState("");
useEffect(() => {
if (connected) {
async function fetchBalance() {
try {
const balance = await wallet.getBalance();
const ada =
balance.find((asset) => asset.unit === "lovelace")?.quantity || "0";
setBalance(`${parseInt(ada) / 1000000} ADA`);
} catch (err) {
setBalance("Lỗi khi lấy số dư");
}
}
fetchBalance();
}
}, [connected, wallet]);
const handleSubmit = async () => {
if (!connected) {
setError("Vui lòng kết nối ví!");
return;
}
if (!recipient || !amount) {
setError("Vui lòng nhập đầy đủ địa chỉ ví và số lượng ADA");
return;
}
const startTime = Date.now();
try {
if (method === "client") {
const tx = new Transaction({ initiator: wallet });
tx.sendAssets({ address: recipient }, [
{ unit: "lovelace", quantity: `${Number(amount) * 1000000}` },
]);
const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
setTxHash(txHash);
setError("");
} else {
const response = await fetch("/api/cardano/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sender: walletAddress, recipient, amount }),
});
const { unsignedTx, error } = await response.json();
if (error) throw new Error(error);
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
setTxHash(txHash);
setError("");
}
const endTime = Date.now();
setExecutionTime(`${endTime - startTime} ms`);
} catch (err) {
setError(`Lỗi: ${err.message}`);
}
};
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<h1>Gửi ADA</h1>
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
{!connected ? (
<button
onClick={() => connect("eternl")}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
marginBottom: "20px",
}}
>
Connect Wallet
</button>
) : (
<p style={{ marginBottom: "20px" }}>Số dư: {balance}</p>
)}
<div style={{ marginBottom: "10px" }}>
<label>Phương thức:</label>
<select
value={method}
onChange={(e) => setMethod(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
>
<option value="client">Client-side</option>
<option value="server">Server-side</option>
</select>
</div>
<div style={{ marginBottom: "10px" }}>
<label>Địa chỉ ví nhận:</label>
<input
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<div style={{ marginBottom: "10px" }}>
<label>Số lượng ADA:</label>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
style={{ width: "100%", padding: "8px", marginTop: "5px" }}
/>
</div>
<button
onClick={handleSubmit}
style={{
padding: "10px 20px",
backgroundColor: "#0070f3",
color: "white",
border: "none",
borderRadius: "5px",
}}
>
Gửi ADA
</button>
{error && <p style={{ color: "red", marginTop: "10px" }}>{error}</p>}
{txHash && (
<p style={{ color: "green", marginTop: "10px" }}>
Giao dịch thành công! Tx Hash:{" "}
<a
href={`https://preprod.cardanoscan.io/transaction/${txHash}`}
target="_blank"
>
{txHash}
</a>
</p>
)}
{executionTime && (
<p style={{ marginTop: "10px" }}>
Thời gian thực thi: {executionTime}
</p>
)}
</div>
<div style={{ marginTop: "20px" }}>
<h3>Ưu/Nhược điểm</h3>
<p>
<strong>Client-side:</strong> Nhanh hơn, không cần server request.
Nhược điểm: Lộ logic giao dịch, không phù hợp với giao dịch phức tạp
như đa chữ ký.
</p>
<p>
<strong>Server-side:</strong> Bảo mật hơn, hỗ trợ giao dịch phức tạp.
Nhược điểm: Chậm hơn do request server, phụ thuộc vào server.
</p>
</div>
</div>
);
}
Ưu/Nhược điểm:
- Client-side:
- Ưu điểm: Nhanh hơn, không cần gửi request đến server.
- Nhược điểm: Lộ logic giao dịch, không phù hợp với giao dịch phức tạp như đa chữ ký.
- Server-side:
- Ưu điểm: Bảo mật hơn, hỗ trợ logic phức tạp, không lộ thông tin giao dịch.
- Nhược điểm: Chậm hơn do request server, phụ thuộc vào server.
Chạy npm run dev, truy cập http://localhost:3000/send, chọn phương thức (client/server), nhập địa chỉ ví nhận và số lượng ADA, nhấn “Gửi ADA” để kiểm tra thời gian thực thi và Tx Hash.
Link Source Code: https://github.com/htlabs-xyz/Cardano-App-Development-Course/tree/main/Code/Video_06
Link Bài Tập: https://github.com/htlabs-xyz/Cardano-App-Development-Course/blob/main/Exercises/Video_06.md
