Xây dựng dapp trên cardano từ con số không
About Lesson

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:

  1. 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.
  2. Ký Giao Dịch: Sử dụng ví để ký giao dịch, đảm bảo tính xác thực.
  3. 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 BalanceuseEffect tự động cập nhật số dư ví khi wallet thay đổ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 Transaction từ 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/send nhận thông tin senderreceiver, và amount từ 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.
  • 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ề unsignedTx cho 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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

📝 Bài tập 1: Tạo giao diện Form gửi ADA

Đề bài

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.

Yêu cầu

  • Tạo trang /send vớ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
  1. 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.
  2. Xử lý input và lỗi:
    • Thêm sự kiện onChange cho 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.
  3. Định dạng giao diện:
    • Sử dụng inline CSS hoặc file CSS riêng để tạo giao diện đẹp.

Đáp án

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.


📝 Bài tập 2: Kết nối ví và hiển thị số dư

Đề bài

Tích hợp ví Cardano vào trang /send để hiển thị số dư ADA sau khi kết nối.

Yêu cầu

  • 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
  1. Cài đặt MeshJS:
    • Cài đặt @meshsdk/core và @meshsdk/react.
  2. 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.
  3. Xử lý lỗi:
    • Kiểm tra trạng thái kết nối ví trước khi lấy số dư.
  4. Định dạng:
    • Sử dụng inline CSS để hiển thị số dư.

Đáp án

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.


📝 Bài tập 3: Tạo giao dịch trên Client-side

Đề bài

Tạo giao dịch gửi ADA trên client-side sử dụng MeshJS.

Yêu cầu

  • 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
  1. Xây dựng giao dịch:
    • Sử dụng Transaction từ @meshsdk/core để tạo giao dịch.
    • Lấy địa chỉ ví nhận và số lượng ADA từ state.
  2. Ký và gửi giao dịch:
    • Ký giao dịch bằng ví trình duyệt và submit lên blockchain.
  3. 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.
  4. Hiển thị Tx Hash:
    • Hiển thị Tx Hash trong giao diện sau khi submit thành công.

Đáp án

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.


📝 Bài tập 4: Tạo giao dịch trên Server-side

Đề bài

Tạo giao dịch gửi ADA trên server-side sử dụng MeshJS và Blockfrost.

Yêu cầu

  • 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
  1. 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.
  2. 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.
  3. Ký và submit:
    • Nhận unsigned transaction từ server, ký bằng ví trên client, và submit.
  4. Hiển thị kết quả:
    • Hiển thị Tx Hash hoặc lỗi trong giao diện.

Đáp á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.


📝 Bài tập 5: So sánh Client-side và Server-side cho Giao dịch

Đề bài

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.

Yêu cầu

  • Tạo trang /send vớ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
  1. 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.
  2. 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.
  3. Đ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.
  4. 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.

Đáp án

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