7. Build a web3 web with ubuntu

Tree folder

fhe-tax-dapp/                   Tên folder dự án của bạn

├── package.json                Tạo tự động bởi Vite
├── vite.config.js              Tạo tự động
├── index.html                  UI siêu đẹp của bạn (giữ gần nguyên bản)
├── public/                     (tùy chọn) favicon, logo...

└── src/
    └── main.js                 Toàn bộ logic FHE + Relayer SDK mới (mình viết sẵn cho bạn)
mkdir fhe-tax-dapp && cd fhe-tax-dapp
npm create vite@latest . -- --template vanilla

( chose yes all )

npm install
npm install @zama-fhe/[email protected] [email protected]
nano src/main.js
import { createInstance, SepoliaConfig } from '@zama-fhe/relayer-sdk';
import { ethers } from 'ethers';

const CONTRACT_ADDRESS = "0x11408744D57DfC18a170789B28F6F2d6F58A37d1";
const CONTRACT_ABI = [
  "function declare(externalEuint32 input, bytes proof)",
  "function getDeclaration() view returns (euint32)",
  "event Declared(address indexed user)"
];

let provider, signer, contract, userAddress, fhe;

async function init() {
  try {
    fhe = await createInstance(SepoliaConfig);
    document.getElementById("result").textContent = "FHE sẵn sàng! Kết nối ví để tiếp tục.";
  } catch (e) {
    document.getElementById("error").textContent = "Lỗi FHE: " + e.message;
  }
}
init();

window.connectWallet = async () => {
  if (!window.ethereum) return alert("Cài MetaMask!");
  try {
    await ethereum.request({ method: 'eth_requestAccounts' });
    provider = new ethers.providers.Web3Provider(window.ethereum);
    signer = provider.getSigner();
    userAddress = await signer.getAddress();
    contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);

    document.getElementById("walletInfo").innerHTML = `<strong>Đã kết nối:</strong> ${userAddress.slice(0,6)}...${userAddress.slice(-4)}`;
    document.getElementById("connectBtn").textContent = "NGẮT KẾT NỐI";
    document.getElementById("connectBtn").classList.add("connected");
    document.getElementById("declareBtn").disabled = false;
    loadData();
  } catch (e) { document.getElementById("error").textContent = e.message; }
};

async function loadData() {
  try {
    const enc = await contract.getDeclaration();
    if (enc !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
      const val = await fhe.decryptEuint32(enc, userAddress);
      document.getElementById("amount").value = (val / 100).toFixed(2);
    }
  } catch (e) { console.log("Chưa có dữ liệu"); }
}

window.declareOnChain = async () => {
  const amount = Math.round(parseFloat(document.getElementById("amount").value) * 100);
  if (!amount) return;

  document.getElementById("result").innerHTML = `<span class="loading">Đang mã hóa & gửi tx...</span>`;

  try {
    const input = fhe.createEncryptedInput(CONTRACT_ADDRESS, userAddress);
    input.add32(amount);
    const { handles, inputProof } = await input.encrypt();

    const tx = await contract.declare(handles[0], inputProof);
    await tx.wait();

    document.getElementById("result").textContent = "Đã ghi thành công! Bạn là tỷ phú rồi";
    fireConfetti();
  } catch (e) {
    document.getElementById("error").textContent = e.message.includes("rejected") ? "Bạn từ chối tx" : e.message;
  }
};

function fireConfetti() {
  const duration = 5 * 1000;
  const end = Date.now() + duration;
  (function frame() {
    confetti({ particleCount: 10, spread: 70, origin: { x: 0 } });
    confetti({ particleCount: 10, spread: 70, origin: { x: 1 } });
    if (Date.now() < end) requestAnimationFrame(frame);
  }());
}

document.getElementById("connectBtn").onclick = () => {
  if (document.getElementById("connectBtn").classList.contains("connected")) location.reload();
  else connectWallet();
};

document.getElementById("declareBtn").onclick = declareOnChain;
nano index.html
<!DOCTYPE html>
<html lang="vi">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>FHE TAX</title>
  <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=Inter:wght@400;600&display=swap" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/ethers.umd.min.js"></script>
  <!-- THÊM FHEVM.JS ĐỂ MÃ HÓA -->
  <script src="https://cdn.jsdelivr.net/npm/@fhevm/[email protected]/dist/fhevm.min.js"></script>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Inter', sans-serif;
      /* Giữ gradient vàng nhạt làm nền chính */
      background: 
        linear-gradient(135deg, #fff8e1 0%, #ffecb3 50%, #ffe082 100%),
        url('https://sf-static.upanhlaylink.com/img/image_2025112678dd783a8345ae4a8afb47c30ffb8065.jpg') center/cover no-repeat fixed;
  
      /* Làm ảnh mờ nhẹ để không lấn chữ + khung trắng */
      background-blend-mode: overlay;
      color: #3c2f00;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
  
      /* Tùy chọn: làm ảnh trong suốt hơn */
      position: relative;
    }
    .container {
      background: rgba(255, 255, 255, 0.85);
      backdrop-filter: blur(15px);
      border: 2px solid #ffd700;
      border-radius: 24px;
      padding: 50px 35px;
      width: 100%;
      max-width: 540px;
      box-shadow: 
        0 20px 50px rgba(255, 193, 7, 0.3),
        0 0 40px rgba(255, 215, 0, 0.5),
        inset 0 0 30px rgba(255, 255, 255, 0.6);
      text-align: center;
    }
    h1 {
      font-family: 'Orbitron', sans-serif;
      font-size: 2.5rem;
      color: #b8860b;
      text-shadow: 
        0 0 20px #ffd700,
        0 0 40px #ffaa00;
      margin-bottom: 12px;
      animation: pulseGlowGold 3s ease-in-out infinite alternate;
    }
    p.subtitle {
      color: #5c4000;
      margin-bottom: 20px 0 35px;
      font-size: 1.05rem;
      font-weight: 500;
    }
    .connect-btn {
      background: linear-gradient(45deg, #ffd700, #ffaa00);
      color: #000;
      padding: 14px 28px;
      border: none;
      border-radius: 16px;
      font-weight: 700;
      cursor: pointer;
      margin-bottom: 25px;
      transition: all 0.3s;
      box-shadow: 0 8px 20px rgba(255, 215, 0, 0.4);
    }
    .connect-btn:hover { transform: scale(1.05); }
    .connect-btn.connected { background: #51cf66; }
    input {
      width: 100%; 
      padding: 18px 24px;                 /* to hơn tí cho đẹp */
      font-size: 1.3rem;
      background: rgba(255, 255, 255, 0.92);   /* trắng trong suốt nhẹ → hợp nền vàng */
      border: 3px solid #ffd700;              /* viền vàng đậm nổi bật */
      border-radius: 20px;
      color: #b8860b;                         /* chữ vàng đậm sang trọng */
      outline: none;
      font-weight: 700;
      text-align: center;
      box-shadow: 0 8px 30px rgba(255, 193, 7, 0.4);
      transition: all 0.4s;
    }
    input:focus { 
      border-color: #ffaa00;
      box-shadow: 0 0 35px rgba(255, 215, 0, 0.7);
      background: #ffffff;
      transform: scale(1.02);
    }
    button {
      background: linear-gradient(45deg, #ffd700, #ffaa00); color: #000; font-weight: 600;
      padding: 14px 32px; border: none; border-radius: 12px; font-size: 1.1rem;
      cursor: pointer; margin-top: 10px; box-shadow: 0 5px 15px rgba(255, 215, 0, 0.4);
    }
    button:hover { transform: translateY(-3px); }
    button:disabled { opacity: 0.5; cursor: not-allowed; }
    .result {
      margin-top: 25px; padding: 20px; background: rgba(30, 30, 46, 0.7);
      border-radius: 12px; border-left: 5px solid #ffd700; font-size: 1.1rem; line-height: 2.2;
      overflow: hidden;
    }
    .result-item {
      opacity: 0;
      transform: translateX(-50px);
      margin: 14px 0;
      padding: 8px 0;
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-weight: 600;
    }
    .status {
      font-weight: 800;
      padding: 14px 28px;
      border-radius: 20px;
      background: linear-gradient(45deg, #b8860b, #ffd700);
      color: #000;
      box-shadow: 0 0 30px rgba(255, 215, 0, 0.7);
    }
    .poor { background: #718096; color: #fff; }
    .normal { background: #63b3ed; color: #fff; }
    .rich { background: #9f7aea; color: #fff; }
    .super-rich { background: #f687b3; color: #fff; }
    .millionaire { background: #f6ad55; color: #000; }
    .billionaire { background: #fc8181; color: #fff; }
    .error { color: #ff6b6b; margin-top: 15px; font-weight: 600; }
    .tx-hash { font-size: 0.85rem; color: #a0aec0; word-break: break-all; margin-top: 10px; }
    .loading { color: #ffd700; font-style: italic; }
    .etherscan-link { margin-top: 10px; }
    .etherscan-link a { color: #4fd1c5; text-decoration: underline; font-size: 0.9rem; }
    footer { 
      margin-top: 50px; 
      font-size: 0.9rem; 
      color: #5c4000; 
      font-weight: 600;
    }

    /* GLOW CỰC MẠNH CHO SỐ */
    .glow-number {
      font-weight: 800;
      font-size: 1.3rem;
      letter-spacing: 1px;
    }
    .glow-gold {
      color: #ffd700;
      text-shadow: 
        0 0 10px #ffd700,
        0 0 20px #ffd700,
        0 0 30px #ffd700,
        0 0 50px #ffaa00,
        0 0 70px #ffaa00;
      animation: pulseGlowGold 2s infinite alternate;
    }
    .glow-red {
      color: #ff6b6b;
      text-shadow: 
        0 0 10px #ff6b6b,
        0 0 20px #ff6b6b,
        0 0 30px #ff6b6b,
        0 0 50px #ff4444,
        0 0 70px #ff4444;
      animation: pulseGlowRed 1.8s infinite alternate;
    }
    .glow-green {
      color: #51cf66;
      text-shadow: 
        0 0 10px #51cf66,
        0 0 20px #51cf66,
        0 0 30px #51cf66,
        0 0 50px #40b558,
        0 0 70px #40b558;
      animation: pulseGlowGreen 2.2s infinite alternate;
    }

    @keyframes pulseGlowGold {
      from { text-shadow: 0 0 10px #ffd700, 0 0 20px #ffd700; }
      to { text-shadow: 0 0 20px #ffd700, 0 0 40px #ffd700, 0 0 60px #ffaa00; }
    }
    @keyframes pulseGlowRed {
      from { text-shadow: 0 0 10px #ff6b6b; }
      to { text-shadow: 0 0 20px #ff6b6b, 0 0 40px #ff6b6b; }
    }
    @keyframes pulseGlowGreen {
      from { text-shadow: 0 0 10px #51cf66; }
      to { text-shadow: 0 0 20px #51cf66, 0 0 40px #51cf66; }
    }

    @keyframes slideInLeft {
      from { opacity: 0; transform: translateX(-100px); }
      to { opacity: 1; transform: translateX(0); }
    }
    @keyframes pulseRed {
      0%, 100% { transform: scale(1); }
      50% { transform: scale(1.05); }
    }
    @keyframes bounceGreen {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-8px); }
    }
    @keyframes fadeInScale {
      from { opacity: 0; transform: scale(0.5); }
      to { opacity: 1; transform: scale(1); }
    }
    @keyframes float {
      0%, 100% { transform: translateY(0); }
      50% { transform: translateY(-12px); }
    }

    #characterImage img {
      animation: float 3s ease-in-out infinite;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>FHE TAX</h1>
    <p class="subtitle">FHE Encrypted Data – No One Sees Your Money!</p>
   
    <button class="connect-btn" id="connectBtn">CONNECT WALLET</button>
    <div class="wallet-info" id="walletInfo"></div>

    <div class="input-group">
      <input type="number" id="amount" placeholder="Enter the amount (USD)" step="0.1" min="0" />
    </div>

    <button onclick="declareOnChain()" id="declareBtn" disabled>SIGN & WRITE ON BLOCKCHAIN (FHE)</button>

    <div class="result" id="result">Connect wallet and enter amount...</div>
    <div class="error" id="error"></div>
    <div class="tx-hash" id="txHash"></div>
    <div class="etherscan-link" id="contractLink"></div>

    <footer>© 2025 Canhsiro – DApp FHE On-Chain</footer>
  </div>

  <script>
    // CẬP NHẬT ĐỊA CHỈ HỢP ĐỒNG FHE MỚI SAU KHI DEPLOY
    const CONTRACT_ADDRESS = "0x11408744D57DfC18a170789B28F6F2d6F58A37d1";
    const CONTRACT_ABI = [
      "function declare(externalEuint32 input, bytes proof)",
      "function getDeclaration() view returns (euint32)",
      "event Declared(address indexed user)"
    ];

    let provider, signer, contract, userAddress;
    let isConnected = false;

    // Cập nhật link Etherscan
    document.getElementById("contractLink").innerHTML = `
      <a href="https://sepolia.etherscan.io/address/${CONTRACT_ADDRESS}" target="_blank">
        View FHE contract on Sepolia Etherscan
      </a>
    `;

    document.getElementById("connectBtn").onclick = () => {
      if (isConnected) {
        disconnectWallet();
      } else {
        connectWallet();
      }
    };

    async function connectWallet() {
      if (!window.ethereum) {
        document.getElementById("error").innerText = "Install MetaMask: https://metamask.io";
        return;
      }

      try {
        await ethereum.request({ method: 'eth_requestAccounts' });
        provider = new ethers.providers.Web3Provider(window.ethereum, "any");
        signer = provider.getSigner();
        const address = await signer.getAddress();

        userAddress = address;
        contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);

        const short = `${userAddress.slice(0,6)}...${userAddress.slice(-4)}`;
        document.getElementById("walletInfo").innerHTML = `<strong>Đã kết nối:</strong> ${short}`;
        document.getElementById("connectBtn").innerText = "DISCONNECT";
        document.getElementById("connectBtn").classList.add("connected");
        document.getElementById("declareBtn").disabled = false;
        document.getElementById("error").innerText = "";

        isConnected = true;
        loadUserData();

      } catch (err) {
        document.getElementById("error").innerText = "Connection failed: " + (err.message || "Unknown error");
      }
    }

    function disconnectWallet() {
      provider = signer = contract = userAddress = null;
      isConnected = false;
      document.getElementById("walletInfo").innerHTML = "";
      document.getElementById("connectBtn").innerText = "CONNECT EVM WALLET";
      document.getElementById("connectBtn").classList.remove("connected");
      document.getElementById("declareBtn").disabled = true;
      document.getElementById("amount").value = "";
      document.getElementById("result").innerHTML = "Connect wallet and enter amount...";
      document.getElementById("error").innerText = "";
      document.getElementById("txHash").innerText = "";
    }

    // TẢI DỮ LIỆU ĐÃ MÃ HÓA → GIẢI MÃ
    async function loadUserData() {
      if (!contract || !userAddress) return;
      try {
        const encrypted = await contract.getDeclaration();
        const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
        if (encrypted !== zeroHash) {
          const decrypted = await fhevm.userDecryptEuint32(encrypted, CONTRACT_ADDRESS, userAddress);
          const usd = decrypted / 100;
          document.getElementById("amount").value = usd;
          calculateTax(usd);
        }
      } catch (err) {
        console.log("No data or decoding error");
      }
    }

    // GHI DỮ LIỆU MÃ HÓA LÊN CHAIN
    async function declareOnChain() {
      const amountInput = document.getElementById("amount").value;
      const resultDiv = document.getElementById("result");
      const errorDiv = document.getElementById("error");
      const txHashDiv = document.getElementById("txHash");

      if (!amountInput || amountInput <= 0) {
        errorDiv.innerText = "Enter valid amount!";
        return;
      }

      const amountUSD = Math.round(parseFloat(amountInput) * 100);
      resultDiv.innerHTML = `<span class="loading">Encrypting data...</span>`;
      errorDiv.innerText = "";
      txHashDiv.innerText = "";

      try {
        // MÃ HÓA DỮ LIỆU
        const input = fhevm.createEncryptedInput(CONTRACT_ADDRESS, userAddress);
        input.add32(amountUSD);
        const encryptedInput = await input.encrypt();

        // GỬI GIAO DỊCH
        const tx = await contract.declare(
          encryptedInput.handles[0],
          encryptedInput.inputProof
        );

        resultDiv.innerHTML = `<span class="loading">Writing to blockchain...</span>`;
        const receipt = await tx.wait();

        txHashDiv.innerHTML = `
          Tx: <a href="https://sepolia.etherscan.io/tx/${receipt.transactionHash}" target="_blank">
            ${receipt.transactionHash}
          </a>
        `;

        // GIẢI MÃ ĐỂ HIỂN THỊ
        const encryptedCount = await contract.getDeclaration();
        const decrypted = await fhevm.userDecryptEuint32(encryptedCount, CONTRACT_ADDRESS, userAddress);
        const usd = decrypted / 100;

        animateResults(usd);
        fireConfetti();

      } catch (err) {
        console.error(err);
        errorDiv.innerText = 
          err.message.includes("user rejected") ? "You refuse to sign!" 
          : err.message.includes("proof") ? "Encryption proof error!" 
          : "Error transaction: " + err.message;
        resultDiv.innerHTML = "";
      }
    }

    // TÍNH THUẾ VÀ HIỂN THỊ
    function calculateTax(usd) {
      animateResults(usd);
    }

    // === HIỆU ỨNG KẾT QUẢ ===
    function animateResults(amount) {
      const resultDiv = document.getElementById("result");
      let category = "", taxRate = 0, statusClass = "";

      if (amount <= 10000) {
        category = "Bạn thuộc hạng nghèo, không cần đóng thuế"; taxRate = 0; statusClass = "poor";
      } else if (amount <= 50000) {
        category = "Thuộc hàng bình thường, thuế 1%"; taxRate = 1; statusClass = "normal";
      } else if (amount <= 500000) {
        category = "Thuộc khá giả, đóng thuế 5%"; taxRate = 5; statusClass = "rich";
      } else if (amount < 1000000) {
        category = "Bạn thuộc hạng giàu, đóng thuế 10%"; taxRate = 10; statusClass = "super-rich";
      } else if (amount < 1000000000) {
        category = "Bạn thuộc triệu phú đô la, đóng thuế 20%"; taxRate = 20; statusClass = "millionaire";
      } else {
        category = "Bạn thuộc tỷ phú, sáng ngang với tạp chí Forbes, đóng thuế 30%"; taxRate = 30; statusClass = "billionaire";
      }

      const tax = (amount * taxRate / 100).toFixed(2);
      const net = (amount - tax).toFixed(2);

      resultDiv.innerHTML = `
        <div class="result-item" id="assetLine">
          <strong>Tài sản:</strong> 
          <span class="glow-number glow-gold">$${amount.toLocaleString()}</span>
        </div>
        <div class="result-item" id="taxLine">
          <strong>Thuế (${taxRate}%):</strong> 
          <span class="glow-number glow-red">$${tax}</span>
        </div>
        <div class="result-item" id="netLine">
          <strong>Còn lại:</strong> 
          <span class="glow-number glow-green">$${net}</span>
        </div>
        <div class="status ${statusClass}" id=" |statusLine">${category}</div>
        <div id="characterImage" style="margin-top: 20px; opacity: 0; transition: opacity 1s ease;"></div>
      `;

      setTimeout(() => animateIn("#assetLine", "slideInLeft"), 600);
      setTimeout(() => animateIn("#taxLine", "pulseRed"), 1400);
      setTimeout(() => animateIn("#netLine", "bounceGreen"), 2200);
      setTimeout(() => {
        animateIn("#statusLine", "fadeInScale");
        showCharacterImage(statusClass);
      }, 3000);
    }

    function animateIn(selector, animationName) {
      const el = document.querySelector(selector);
      if (!el) return;
      el.style.opacity = 1;
      el.style.transform = "translateX(0) scale(1)";
      el.style.animation = `${animationName} 0.8s ease forwards`;
    }

    function showCharacterImage(statusClass) {
      const imgDiv = document.getElementById("characterImage");
      const images = {
        poor:        "https://i.imgur.com/8bF3v2J.png",
        normal:      "https://i.imgur.com/5k3jP9m.png",
        rich:        "https://i.imgur.com/2fX9kLm.png",
        "super-rich": "https://i.imgur.com/7vP8nXc.png",
        millionaire: "https://i.imgur.com/Qw5kR9v.png",
        billionaire: "https://i.imgur.com/3kLmN2P.png"
      };

      const imgUrl = images[statusClass];
      if (imgUrl) {
        imgDiv.innerHTML = `
          <img src="${imgUrl}" alt="Character" 
               style="width: 180px; height: auto; border-radius: 16px; 
                      box-shadow: 0 0 20px rgba(255,215,0,0.6);">
        `;
        setTimeout(() => { imgDiv.style.opacity = 1; }, 200);
      }
    }

    function fireConfetti() {
      const duration = 4 * 1000, end = Date.now() + duration;
      (function frame() {
        confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 } });
        confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 } });
        if (Date.now() < end) requestAnimationFrame(frame);
      }());
    }

    // Enter để ghi
    document.getElementById("amount").addEventListener("keypress", (e) => {
      if (e.key === "Enter" && !document.getElementById("declareBtn").disabled) {
        declareOnChain();
      }
    });
  </script>
</body>
</html>
npm run dev -- --host

Last updated