Hướng dẫn về NFT Minter
Một trong những thách thức lớn nhất đối với các nhà phát triển đến từ nền tảng Web2 là tìm ra cách kết nối hợp đồng thông minh của bạn với một dự án frontend và tương tác với nó.
Bằng cách xây dựng một NFT minter — một UI đơn giản nơi bạn có thể nhập liên kết đến tài sản kỹ thuật số, một tiêu đề và một mô tả — bạn sẽ học được cách:
- Kết nối với MetaMask thông qua dự án frontend của bạn
- Gọi các phương thức hợp đồng thông minh từ frontend của bạn
- Ký các giao dịch bằng MetaMask
Trong hướng dẫn này, chúng tôi sẽ sử dụng React (opens in a new tab) làm framework frontend. Bởi vì hướng dẫn này chủ yếu tập trung vào phát triển Web3, chúng tôi sẽ không dành nhiều thời gian để phân tích các kiến thức cơ bản về React. Thay vào đó, chúng tôi sẽ tập trung vào việc mang lại chức năng cho dự án của mình.
Là một điều kiện tiên quyết, bạn nên có một sự hiểu biết ở cấp độ người mới bắt đầu về React—biết cách các thành phần, props, useState/useEffect và cách gọi hàm cơ bản hoạt động. Nếu bạn chưa bao giờ nghe về bất kỳ thuật ngữ nào trước đây, bạn có thể muốn xem [hướng dẫn Giới thiệu về React] này(https://react.dev/learn/tutorial-tic-tac-toe (opens in a new tab)). Đối với những người học bằng hình ảnh, chúng tôi đặc biệt đề xuất chuỗi video tuyệt vời này Hướng dẫn React hiện đại đầy đủ (opens in a new tab) của Net Ninja.
Và nếu bạn chưa có, bạn chắc chắn sẽ cần một tài khoản Alchemy để hoàn thành hướng dẫn này cũng như xây dựng bất cứ thứ gì trên chuỗi khối. Đăng ký một tài khoản miễn phí tại đây (opens in a new tab).
Không chần chừ gì nữa, hãy bắt đầu nào!
Kiến thức cơ bản về tạo NFT
Trước khi chúng ta bắt đầu xem xét bất kỳ mã nào, điều quan trọng là phải hiểu cách tạo NFT hoạt động. Nó bao gồm hai bước:
Công bố một hợp đồng thông minh NFT trên chuỗi khối Ethereum
Sự khác biệt lớn nhất giữa hai tiêu chuẩn hợp đồng thông minh NFT là ERC-1155 là một tiêu chuẩn đa token và bao gồm chức năng hàng loạt, trong khi ERC-721 là một tiêu chuẩn token đơn và do đó chỉ hỗ trợ chuyển một token tại một thời điểm.
Gọi hàm đúc
Thông thường, hàm đúc này yêu cầu bạn chuyển vào hai biến làm tham số, đầu tiên là recipient, chỉ định địa chỉ sẽ nhận NFT mới đúc của bạn, và thứ hai là tokenURI của NFT, một chuỗi phân giải thành một tài liệu JSON mô tả siêu dữ liệu của NFT.
Siêu dữ liệu của NFT thực sự là thứ mang lại sức sống cho nó, cho phép nó có các thuộc tính, chẳng hạn như tên, mô tả, hình ảnh (hoặc tài sản kỹ thuật số khác) và các thuộc tính khác. Đây là một ví dụ về tokenURI (opens in a new tab), chứa siêu dữ liệu của một NFT.
Trong hướng dẫn này, chúng ta sẽ tập trung vào phần 2, gọi hàm đúc hợp đồng thông minh của một NFT hiện có bằng cách sử dụng UI React của chúng ta.
Đây là liên kết (opens in a new tab) đến hợp đồng thông minh NFT ERC-721 mà chúng ta sẽ gọi trong hướng dẫn này. Nếu bạn muốn tìm hiểu cách chúng tôi đã tạo ra nó, chúng tôi thực sự khuyên bạn nên xem hướng dẫn khác của chúng tôi, "Cách tạo một NFT" (opens in a new tab).
Tuyệt vời, bây giờ chúng ta đã hiểu cách tạo NFT hoạt động, hãy sao chép các tệp khởi đầu của chúng ta!
Sao chép các tệp khởi đầu
Đầu tiên, hãy truy cập kho lưu trữ GitHub của nft-minter-tutorial (opens in a new tab) để lấy các tệp khởi đầu cho dự án này. Sao chép kho lưu trữ này vào môi trường cục bộ của bạn.
Khi bạn mở kho lưu trữ nft-minter-tutorial đã sao chép này, bạn sẽ nhận thấy rằng nó chứa hai thư mục: minter-starter-files và nft-minter.
minter-starter-fileschứa các tệp khởi đầu (chủ yếu là UI React) cho dự án này. Trong hướng dẫn này, chúng ta sẽ làm việc trong thư mục này, khi bạn tìm hiểu cách làm cho UI này trở nên sống động bằng cách kết nối nó với ví Ethereum của bạn và một hợp đồng thông minh NFT.nft-minterchứa toàn bộ hướng dẫn đã hoàn thành và có sẵn cho bạn như một tài liệu tham khảo nếu bạn gặp khó khăn.
Tiếp theo, mở bản sao của minter-starter-files trong trình chỉnh sửa mã của bạn, và sau đó điều hướng vào thư mục src của bạn.
Tất cả mã chúng ta sẽ viết sẽ nằm trong thư mục src. Chúng ta sẽ chỉnh sửa thành phần Minter.js và viết các tệp javascript bổ sung để cung cấp cho dự án của chúng ta chức năng Web3.
Bước 2: Kiểm tra các tệp khởi đầu của chúng ta
Trước khi bắt đầu viết mã, điều quan trọng là phải kiểm tra những gì đã được cung cấp cho chúng ta trong các tệp khởi đầu.
Chạy dự án react của bạn
Hãy bắt đầu bằng cách chạy dự án React trong trình duyệt của chúng ta. Vẻ đẹp của React là một khi chúng ta có dự án đang chạy trong trình duyệt, bất kỳ thay đổi nào chúng ta lưu sẽ được cập nhật trực tiếp trong trình duyệt của chúng ta.
Để chạy dự án, điều hướng đến thư mục gốc của thư mục minter-starter-files, và chạy npm install trong terminal của bạn để cài đặt các phụ thuộc của dự án:
cd minter-starter-filesnpm installSau khi chúng đã cài đặt xong, hãy chạy npm start trong terminal của bạn:
npm startLàm như vậy sẽ mở http://localhost:3000/ (opens in a new tab) trong trình duyệt của bạn, nơi bạn sẽ thấy frontend cho dự án của chúng ta. Nó sẽ bao gồm 3 trường: một nơi để nhập liên kết đến tài sản NFT của bạn, nhập tên NFT của bạn và cung cấp một mô tả.
Nếu bạn thử nhấp vào các nút "Kết nối Ví" hoặc "Đúc NFT", bạn sẽ nhận thấy chúng không hoạt động—đó là vì chúng ta vẫn cần lập trình chức năng của chúng! :)
Thành phần Minter.js
LƯU Ý: Hãy chắc chắn rằng bạn đang ở trong thư mục minter-starter-files chứ không phải thư mục nft-minter!
Hãy quay lại thư mục src trong trình chỉnh sửa của chúng ta và mở tệp Minter.js. Việc hiểu mọi thứ trong tệp này là cực kỳ quan trọng, vì đây là thành phần React chính mà chúng ta sẽ làm việc.
Ở đầu tệp này, chúng ta có các biến trạng thái mà chúng ta sẽ cập nhật sau các sự kiện cụ thể.
1//Biến trạng thái2const [walletAddress, setWallet] = useState("")3const [status, setStatus] = useState("")4const [name, setName] = useState("")5const [description, setDescription] = useState("")6const [url, setURL] = useState("")Chưa bao giờ nghe về biến trạng thái hoặc hook trạng thái của React? Hãy xem các tài liệu này (opens in a new tab).
Đây là ý nghĩa của từng biến:
walletAddress- một chuỗi lưu trữ địa chỉ ví của người dùngstatus- một chuỗi chứa thông báo để hiển thị ở cuối UIname- một chuỗi lưu trữ tên của NFTdescription- một chuỗi lưu trữ mô tả của NFTurl- một chuỗi là liên kết đến tài sản kỹ thuật số của NFT
Sau các biến trạng thái, bạn sẽ thấy ba hàm chưa được triển khai: useEffect, connectWalletPressed và onMintPressed. Bạn sẽ nhận thấy rằng tất cả các hàm này đều là async, đó là bởi vì chúng ta sẽ thực hiện các lệnh gọi API bất đồng bộ trong chúng! Tên của chúng đồng nghĩa với chức năng của chúng:
1useEffect(async () => {2 //TODO: triển khai3}, [])45const connectWalletPressed = async () => {6 //TODO: triển khai7}89const onMintPressed = async () => {10 //TODO: triển khai11}Hiện tất cảuseEffect(opens in a new tab) - đây là một hook của React được gọi sau khi thành phần của bạn được hiển thị. Bởi vì nó có một prop mảng rỗng[]được truyền vào (xem dòng 3), nó sẽ chỉ được gọi trong lần hiển thị đầu tiên của thành phần. Ở đây, chúng ta sẽ gọi trình nghe ví của mình và một hàm ví khác để cập nhật UI của chúng ta để phản ánh xem một ví đã được kết nối hay chưa.connectWalletPressed- hàm này sẽ được gọi để kết nối ví MetaMask của người dùng với ứng dụng phi tập trung của chúng ta.onMintPressed- hàm này sẽ được gọi để đúc NFT của người dùng.
Gần cuối tệp này, chúng ta có UI của thành phần của mình. Nếu bạn xem xét kỹ mã này, bạn sẽ nhận thấy rằng chúng ta cập nhật các biến trạng thái url, name, và description của mình khi đầu vào trong các trường văn bản tương ứng của chúng thay đổi.
Bạn cũng sẽ thấy rằng connectWalletPressed và onMintPressed được gọi khi các nút có ID mintButton và walletButton được nhấp tương ứng.
1//UI của thành phần của chúng ta2return (3 <div className="Minter">4 <button id="walletButton" onClick={connectWalletPressed}>5 {walletAddress.length > 0 ? (6 "Đã kết nối: " +7 String(walletAddress).substring(0, 6) +8 "..." +9 String(walletAddress).substring(38)10 ) : (11 <span>Kết nối Ví</span>12 )}13 </button>1415 <br></br>16 <h1 id="title">🧙♂️ Alchemy NFT Minter</h1>17 <p>18 Chỉ cần thêm liên kết, tên và mô tả tài sản của bạn, sau đó nhấn "Đúc".19 </p>20 <form>21 <h2>🖼 Liên kết đến tài sản: </h2>22 <input23 type="text"24 placeholder="ví dụ: https://gateway.pinata.cloud/ipfs/<hash>"25 onChange={(event) => setURL(event.target.value)}26 />27 <h2>🤔 Tên: </h2>28 <input29 type="text"30 placeholder="ví dụ: NFT đầu tiên của tôi!"31 onChange={(event) => setName(event.target.value)}32 />33 <h2>✍️ Mô tả: </h2>34 <input35 type="text"36 placeholder="ví dụ: Thậm chí còn tuyệt hơn cả cryptokitties ;)"37 onChange={(event) => setDescription(event.target.value)}38 />39 </form>40 <button id="mintButton" onClick={onMintPressed}>41 Đúc NFT42 </button>43 <p id="status">{status}</p>44</div>45)Hiện tất cảCuối cùng, hãy xem xét nơi thành phần Minter này được thêm vào.
Nếu bạn vào tệp App.js, là thành phần chính trong React hoạt động như một vùng chứa cho tất cả các thành phần khác, bạn sẽ thấy thành phần Minter của chúng ta được chèn vào dòng 7.
Trong hướng dẫn này, chúng ta sẽ chỉ chỉnh sửa tệp Minter.js và thêm các tệp vào thư mục src của chúng ta.
Bây giờ chúng ta đã hiểu những gì chúng ta đang làm việc, hãy thiết lập ví Ethereum của chúng ta!
Thiết lập ví Ethereum của bạn
Để người dùng có thể tương tác với hợp đồng thông minh của bạn, họ sẽ cần kết nối ví Ethereum của họ với ứng dụng phi tập trung của bạn.
Tải xuống MetaMask
Trong bài hướng dẫn này, chúng ta sẽ sử dụng MetaMask, một ví ảo trong trình duyệt dùng để quản lý địa chỉ tài khoản Ethereum của bạn. Nếu bạn muốn hiểu thêm về cách thức hoạt động của các giao dịch trên Ethereum, hãy xem trang này.
Bạn có thể tải xuống và tạo tài khoản MetaMask miễn phí tại đây (opens in a new tab). Khi bạn đang tạo tài khoản, hoặc nếu bạn đã có tài khoản, hãy đảm bảo chuyển sang “Mạng thử nghiệm Ropsten” ở phía trên bên phải (để chúng ta không phải giao dịch bằng tiền thật).
Thêm ether từ một Vòi
Để đúc NFT của chúng ta (hoặc ký bất kỳ giao dịch nào trên chuỗi khối Ethereum), chúng ta sẽ cần một ít Eth giả. Để nhận Eth, bạn có thể truy cập vòi Ropsten (opens in a new tab) và nhập địa chỉ tài khoản Ropsten của bạn, sau đó nhấp vào “Gửi Ropsten Eth”. Bạn sẽ sớm thấy Eth trong tài khoản MetaMask của mình!
Kiểm tra số dư của bạn
Để kiểm tra lại số dư của chúng ta, hãy thực hiện một yêu cầu eth_getBalance (opens in a new tab) bằng cách sử dụng công cụ soạn thảo của Alchemy (opens in a new tab). Thao tác này sẽ trả về số lượng Eth trong ví của chúng ta. Sau khi bạn nhập địa chỉ tài khoản MetaMask của mình và nhấp vào “Send Request”, bạn sẽ thấy một phản hồi như sau:
1{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}LƯU Ý: Kết quả này tính bằng wei chứ không phải eth. Wei được sử dụng làm mệnh giá nhỏ nhất của ether. Việc chuyển đổi từ wei sang eth là: 1 eth = 10¹⁸ wei. Vì vậy, nếu chúng ta chuyển đổi 0xde0b6b3a7640000 sang hệ thập phân, chúng ta sẽ nhận được 1*10¹⁸, tương đương với 1 eth.
Phù! Tiền giả của chúng ta đã có đủ!
Kết nối MetaMask với UI của bạn
Bây giờ ví MetaMask của chúng ta đã được thiết lập, hãy kết nối ứng dụng phi tập trung của chúng ta với nó!
Bởi vì chúng ta muốn tuân theo mô hình MVC (opens in a new tab), chúng ta sẽ tạo một tệp riêng chứa các hàm của mình để quản lý logic, dữ liệu và các quy tắc của ứng dụng phi tập trung của chúng ta, và sau đó chuyển các hàm đó đến frontend của chúng ta (thành phần Minter.js của chúng ta).
Hàm connectWallet
Để làm điều đó, hãy tạo một thư mục mới có tên utils trong thư mục src của bạn và thêm một tệp có tên interact.js vào bên trong, tệp này sẽ chứa tất cả các hàm tương tác với ví và hợp đồng thông minh của chúng ta.
Trong tệp interact.js của chúng ta, chúng ta sẽ viết một hàm connectWallet, sau đó chúng ta sẽ nhập và gọi trong thành phần Minter.js của mình.
Trong tệp interact.js của bạn, hãy thêm những nội dung sau
1export const connectWallet = async () => {2 if (window.ethereum) {3 try {4 const addressArray = await window.ethereum.request({5 method: "eth_requestAccounts",6 })7 const obj = {8 status: "👆🏽 Viết một tin nhắn vào trường văn bản ở trên.",9 address: addressArray[0],10 }11 return obj12 } catch (err) {13 return {14 address: "",15 status: "😥 " + err.message,16 }17 }18 } else {19 return {20 address: "",21 status: (22 <span>23 <p>24 {" "}25 🦊 <a target="_blank" href={`https://metamask.io/download`}>26 Bạn phải cài đặt MetaMask, một ví Ethereum ảo, trong trình duyệt27 của bạn.28 </a>29 </p>30 </span>31 ),32 }33 }34}Hiện tất cảHãy cùng phân tích mã này làm gì:
Đầu tiên, hàm của chúng ta kiểm tra xem window.ethereum có được bật trong trình duyệt của bạn không.
window.ethereum là một API toàn cầu được chèn bởi MetaMask và các nhà cung cấp ví khác cho phép các trang web yêu cầu tài khoản Ethereum của người dùng. Nếu được chấp thuận, nó có thể đọc dữ liệu từ các chuỗi khối mà người dùng đang kết nối và đề xuất người dùng ký các tin nhắn và giao dịch. Hãy xem tài liệu MetaMask (opens in a new tab) để biết thêm thông tin!
Nếu window.ethereum không có mặt, điều đó có nghĩa là MetaMask chưa được cài đặt. Điều này dẫn đến một đối tượng JSON được trả về, trong đó address được trả về là một chuỗi rỗng và đối tượng status JSX chuyển tiếp rằng người dùng phải cài đặt MetaMask.
Hầu hết các hàm chúng ta viết sẽ trả về các đối tượng JSON mà chúng ta có thể sử dụng để cập nhật các biến trạng thái và UI của mình.
Bây giờ nếu window.ethereum có mặt, thì đó là lúc mọi thứ trở nên thú vị.
Sử dụng vòng lặp try/catch, chúng ta sẽ cố gắng kết nối với MetaMask bằng cách gọi window.ethereum.request({ method: "eth_requestAccounts" }); (opens in a new tab). Việc gọi hàm này sẽ mở MetaMask trong trình duyệt, qua đó người dùng sẽ được nhắc kết nối ví của họ với ứng dụng phi tập trung của bạn.
- Nếu người dùng chọn kết nối,
method: "eth_requestAccounts"sẽ trả về một mảng chứa tất cả các địa chỉ tài khoản của người dùng đã được kết nối với ứng dụng phi tập trung. Nói chung, hàmconnectWalletcủa chúng ta sẽ trả về một đối tượng JSON chứaaddressđầu tiên trong mảng này (xem dòng 9) và một thông báostatusnhắc người dùng viết một tin nhắn cho hợp đồng thông minh. - Nếu người dùng từ chối kết nối, thì đối tượng JSON sẽ chứa một chuỗi rỗng cho
addressđược trả về và một thông báostatusphản ánh rằng người dùng đã từ chối kết nối.
Thêm hàm connectWallet vào thành phần UI Minter.js của bạn
Bây giờ chúng ta đã viết hàm connectWallet này, hãy kết nối nó với thành phần Minter.js của chúng ta.
Đầu tiên, chúng ta sẽ phải nhập hàm của mình vào tệp Minter.js bằng cách thêm import { connectWallet } from "./utils/interact.js"; vào đầu tệp Minter.js. 11 dòng đầu tiên của Minter.js của bạn bây giờ sẽ trông như thế này:
1import { useEffect, useState } from "react";2import { connectWallet } from "./utils/interact.js";34const Minter = (props) => {56 //Biến trạng thái7 const [walletAddress, setWallet] = useState("");8 const [status, setStatus] = useState("");9 const [name, setName] = useState("");10 const [description, setDescription] = useState("");11 const [url, setURL] = useState("");Hiện tất cảSau đó, bên trong hàm connectWalletPressed của chúng ta, chúng ta sẽ gọi hàm connectWallet đã nhập, như sau:
1const connectWalletPressed = async () => {2 const walletResponse = await connectWallet()3 setStatus(walletResponse.status)4 setWallet(walletResponse.address)5}Bạn có nhận thấy hầu hết các chức năng của chúng ta được trừu tượng hóa khỏi thành phần Minter.js của chúng ta từ tệp interact.js không? Điều này là để chúng ta tuân thủ mô hình M-V-C!
Trong connectWalletPressed, chúng ta chỉ cần thực hiện một lệnh gọi await đến hàm connectWallet đã nhập của mình, và sử dụng phản hồi của nó, chúng ta cập nhật các biến status và walletAddress của mình thông qua các hook trạng thái của chúng.
Bây giờ, hãy lưu cả hai tệp Minter.js và interact.js và thử nghiệm UI của chúng ta cho đến nay.
Mở trình duyệt của bạn trên localhost:3000 và nhấn nút "Kết nối Ví" ở góc trên cùng bên phải của trang.
Nếu bạn đã cài đặt MetaMask, bạn sẽ được nhắc kết nối ví của mình với ứng dụng phi tập trung của bạn. Chấp nhận lời mời kết nối.
Bạn sẽ thấy rằng nút ví bây giờ phản ánh rằng địa chỉ của bạn đã được kết nối.
Tiếp theo, hãy thử làm mới trang... Điều này thật lạ. Nút ví của chúng ta đang nhắc chúng ta kết nối MetaMask, mặc dù nó đã được kết nối...
Tuy nhiên, đừng lo lắng! Chúng ta có thể dễ dàng khắc phục điều đó bằng cách triển khai một hàm có tên getCurrentWalletConnected, hàm này sẽ kiểm tra xem một địa chỉ đã được kết nối với ứng dụng phi tập trung của chúng ta hay chưa và cập nhật UI của chúng ta cho phù hợp!
Hàm getCurrentWalletConnected
Trong tệp interact.js của bạn, hãy thêm hàm getCurrentWalletConnected sau:
1export const getCurrentWalletConnected = async () => {2 if (window.ethereum) {3 try {4 const addressArray = await window.ethereum.request({5 method: "eth_accounts",6 })7 if (addressArray.length > 0) {8 return {9 address: addressArray[0],10 status: "👆🏽 Viết một tin nhắn vào trường văn bản ở trên.",11 }12 } else {13 return {14 address: "",15 status: "🦊 Kết nối với MetaMask bằng nút trên cùng bên phải.",16 }17 }18 } catch (err) {19 return {20 address: "",21 status: "😥 " + err.message,22 }23 }24 } else {25 return {26 address: "",27 status: (28 <span>29 <p>30 {" "}31 🦊 <a target="_blank" href={`https://metamask.io/download`}>32 Bạn phải cài đặt MetaMask, một ví Ethereum ảo, trong trình duyệt33 của bạn.34 </a>35 </p>36 </span>37 ),38 }39 }40}Hiện tất cảMã này rất tương tự với hàm connectWallet mà chúng ta vừa viết trước đó.
Sự khác biệt chính là thay vì gọi phương thức eth_requestAccounts, phương thức này sẽ mở MetaMask để người dùng kết nối ví của họ, ở đây chúng ta gọi phương thức eth_accounts, phương thức này chỉ đơn giản trả về một mảng chứa các địa chỉ MetaMask hiện đang được kết nối với ứng dụng phi tập trung của chúng ta.
Để xem hàm này hoạt động, hãy gọi nó trong hàm useEffect của thành phần Minter.js của chúng ta.
Giống như chúng ta đã làm với connectWallet, chúng ta phải nhập hàm này từ tệp interact.js vào tệp Minter.js của mình như sau:
1import { useEffect, useState } from "react"2import {3 connectWallet,4 getCurrentWalletConnected, //nhập tại đây5} from "./utils/interact.js"Bây giờ, chúng ta chỉ cần gọi nó trong hàm useEffect của mình:
1useEffect(async () => {2 const { address, status } = await getCurrentWalletConnected()3 setWallet(address)4 setStatus(status)5}, [])Lưu ý, chúng ta sử dụng phản hồi của lệnh gọi đến getCurrentWalletConnected để cập nhật các biến trạng thái walletAddress và status của mình.
Sau khi bạn đã thêm mã này, hãy thử làm mới cửa sổ trình duyệt của chúng ta. Nút sẽ hiển thị rằng bạn đã kết nối và hiển thị bản xem trước địa chỉ ví đã kết nối của bạn - ngay cả sau khi bạn làm mới!
Triển khai addWalletListener
Bước cuối cùng trong quá trình thiết lập ví ứng dụng phi tập trung của chúng ta là triển khai trình nghe ví để UI của chúng ta cập nhật khi trạng thái ví thay đổi, chẳng hạn như khi người dùng ngắt kết nối hoặc chuyển đổi tài khoản.
Trong tệp Minter.js của bạn, hãy thêm một hàm addWalletListener trông như sau:
1function addWalletListener() {2 if (window.ethereum) {3 window.ethereum.on("accountsChanged", (accounts) => {4 if (accounts.length > 0) {5 setWallet(accounts[0])6 setStatus("👆🏽 Viết một tin nhắn vào trường văn bản ở trên.")7 } else {8 setWallet("")9 setStatus("🦊 Kết nối với MetaMask bằng nút trên cùng bên phải.")10 }11 })12 } else {13 setStatus(14 <p>15 {" "}16 🦊 <a target="_blank" href={`https://metamask.io/download`}>17 Bạn phải cài đặt MetaMask, một ví Ethereum ảo, trong trình duyệt của bạn.18 </a>19 </p>20 )21 }22}Hiện tất cảHãy nhanh chóng phân tích những gì đang xảy ra ở đây:
- Đầu tiên, hàm của chúng ta kiểm tra xem
window.ethereumcó được bật hay không (tức là MetaMask đã được cài đặt).- Nếu không, chúng ta chỉ cần thiết lập biến trạng thái
statuscủa mình thành một chuỗi JSX nhắc người dùng cài đặt MetaMask. - Nếu nó được bật, chúng ta sẽ thiết lập trình nghe
window.ethereum.on("accountsChanged")trên dòng 3 để lắng nghe các thay đổi trạng thái trong ví MetaMask, bao gồm khi người dùng kết nối thêm một tài khoản vào ứng dụng phi tập trung, chuyển đổi tài khoản hoặc ngắt kết nối một tài khoản. Nếu có ít nhất một tài khoản được kết nối, biến trạng tháiwalletAddresssẽ được cập nhật thành tài khoản đầu tiên trong mảngaccountsđược trả về bởi trình nghe. Ngược lại,walletAddressđược đặt thành một chuỗi rỗng.
- Nếu không, chúng ta chỉ cần thiết lập biến trạng thái
Cuối cùng, chúng ta phải gọi nó trong hàm useEffect của mình:
1useEffect(async () => {2 const { address, status } = await getCurrentWalletConnected()3 setWallet(address)4 setStatus(status)56 addWalletListener()7}, [])Và thế là xong! Chúng ta đã hoàn thành việc lập trình tất cả các chức năng ví của mình! Bây giờ ví của chúng ta đã được thiết lập, hãy cùng tìm hiểu cách đúc NFT của chúng ta!
Kiến thức cơ bản về siêu dữ liệu NFT
Vì vậy, hãy nhớ lại siêu dữ liệu NFT mà chúng ta vừa nói đến trong Bước 0 của hướng dẫn này—nó mang lại sức sống cho NFT, cho phép nó có các thuộc tính, chẳng hạn như tài sản kỹ thuật số, tên, mô tả và các thuộc tính khác.
Chúng ta sẽ cần phải cấu hình siêu dữ liệu này dưới dạng một đối tượng JSON và lưu trữ nó, để chúng ta có thể chuyển nó vào làm tham số tokenURI khi gọi hàm mintNFT của hợp đồng thông minh của chúng ta.
Văn bản trong các trường "Liên kết đến tài sản", "Tên", "Mô tả" sẽ bao gồm các thuộc tính khác nhau của siêu dữ liệu NFT của chúng ta. Chúng ta sẽ định dạng siêu dữ liệu này dưới dạng một đối tượng JSON, nhưng có một vài tùy chọn về nơi chúng ta có thể lưu trữ đối tượng JSON này:
- Chúng ta có thể lưu trữ nó trên chuỗi khối Ethereum; tuy nhiên, làm như vậy sẽ rất tốn kém.
- Chúng ta có thể lưu trữ nó trên một máy chủ tập trung, như AWS hoặc Firebase. Nhưng điều đó sẽ đi ngược lại với đặc tính phi tập trung của chúng ta.
- Chúng ta có thể sử dụng IPFS, một giao thức phi tập trung và mạng ngang hàng để lưu trữ và chia sẻ dữ liệu trong một hệ thống tệp phân tán. Vì giao thức này là phi tập trung và miễn phí, nó là lựa chọn tốt nhất của chúng ta!
Để lưu trữ siêu dữ liệu của chúng ta trên IPFS, chúng ta sẽ sử dụng Pinata (opens in a new tab), một API và bộ công cụ IPFS tiện lợi. Trong bước tiếp theo, chúng ta sẽ giải thích chính xác cách thực hiện điều này!
Sử dụng Pinata để ghim siêu dữ liệu của bạn vào IPFS
Nếu bạn chưa có tài khoản Pinata (opens in a new tab), hãy đăng ký một tài khoản miễn phí tại đây (opens in a new tab) và hoàn thành các bước để xác minh email và tài khoản của bạn.
Tạo khóa API Pinata của bạn
Điều hướng đến trang https://pinata.cloud/keys (opens in a new tab), sau đó chọn nút "New Key" ở trên cùng, đặt widget Admin thành bật và đặt tên cho khóa của bạn.
Sau đó, bạn sẽ thấy một cửa sổ bật lên với thông tin API của mình. Hãy chắc chắn rằng bạn đặt nó ở một nơi an toàn.
Bây giờ khóa của chúng ta đã được thiết lập, hãy thêm nó vào dự án của chúng ta để có thể sử dụng.
Tạo tệp .env
Chúng ta có thể lưu trữ khóa và mã bí mật Pinata của mình một cách an toàn trong một tệp môi trường. Hãy cài đặt gói dotenv (opens in a new tab) trong thư mục dự án của bạn.
Mở một tab mới trong terminal của bạn (riêng biệt với tab đang chạy local host) và đảm bảo bạn đang ở trong thư mục minter-starter-files, sau đó chạy lệnh sau trong terminal của bạn:
1npm install dotenv --saveTiếp theo, tạo tệp .env trong thư mục gốc của minter-starter-files bằng cách nhập nội dung sau vào dòng lệnh của bạn:
1vim.envThao tác này sẽ mở tệp .env của bạn trong vim (một trình soạn thảo văn bản). Để lưu nó, hãy nhấn "esc" + ":" + "q" trên bàn phím của bạn theo thứ tự đó.
Tiếp theo, trong VSCode, điều hướng đến tệp .env của bạn và thêm khóa API và mã bí mật API Pinata của bạn vào đó, như sau:
1REACT_APP_PINATA_KEY = <pinata-api-key>2REACT_APP_PINATA_SECRET = <pinata-api-secret>Lưu tệp, và sau đó bạn đã sẵn sàng để bắt đầu viết hàm để tải siêu dữ liệu JSON của mình lên IPFS!
Triển khai pinJSONToIPFS
May mắn cho chúng ta, Pinata có một API dành riêng cho việc tải dữ liệu JSON lên IPFS (opens in a new tab) và một ví dụ JavaScript tiện lợi với axios mà chúng ta có thể sử dụng, với một số sửa đổi nhỏ.
Trong thư mục utils của bạn, hãy tạo một tệp khác có tên pinata.js và sau đó nhập mã bí mật và khóa Pinata của chúng ta từ tệp .env như sau:
1require("dotenv").config()2const key = process.env.REACT_APP_PINATA_KEY3const secret = process.env.REACT_APP_PINATA_SECRETTiếp theo, dán mã bổ sung từ bên dưới vào tệp pinata.js của bạn. Đừng lo lắng, chúng ta sẽ phân tích ý nghĩa của mọi thứ!
1require("dotenv").config()2const key = process.env.REACT_APP_PINATA_KEY3const secret = process.env.REACT_APP_PINATA_SECRET45const axios = require("axios")67export const pinJSONToIPFS = async (JSONBody) => {8 const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`9 //making axios POST request to Pinata ⬇️10 return axios11 .post(url, JSONBody, {12 headers: {13 pinata_api_key: key,14 pinata_secret_api_key: secret,15 },16 })17 .then(function (response) {18 return {19 success: true,20 pinataUrl:21 "https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash,22 }23 })24 .catch(function (error) {25 console.log(error)26 return {27 success: false,28 message: error.message,29 }30 })31}Hiện tất cảVậy mã này làm gì chính xác?
Đầu tiên, nó nhập axios (opens in a new tab), một ứng dụng HTTP dựa trên promise cho trình duyệt và node.js, mà chúng ta sẽ sử dụng để thực hiện một yêu cầu đến Pinata.
Sau đó, chúng ta có hàm bất đồng bộ pinJSONToIPFS, nhận JSONBody làm đầu vào và khóa và mã bí mật api Pinata trong tiêu đề của nó, tất cả để thực hiện một yêu cầu POST đến API pinJSONToIPFS của họ.
- Nếu yêu cầu POST này thành công, thì hàm của chúng ta sẽ trả về một đối tượng JSON với boolean
successlà true vàpinataUrlnơi siêu dữ liệu của chúng ta được ghim. Chúng ta sẽ sử dụngpinataUrlđược trả về này làm đầu vàotokenURIcho hàm đúc của hợp đồng thông minh của chúng ta. - Nếu yêu cầu post này không thành công, thì hàm của chúng ta sẽ trả về một đối tượng JSON với boolean
successlà false và một chuỗimessagechuyển tiếp lỗi của chúng ta.
Như với các loại trả về của hàm connectWallet của chúng ta, chúng ta đang trả về các đối tượng JSON để có thể sử dụng các tham số của chúng để cập nhật các biến trạng thái và UI của mình.
Tải hợp đồng thông minh của bạn
Bây giờ chúng ta đã có cách để tải siêu dữ liệu NFT của mình lên IPFS thông qua hàm pinJSONToIPFS, chúng ta sẽ cần một cách để tải một phiên bản của hợp đồng thông minh của mình để có thể gọi hàm mintNFT của nó.
Như chúng ta đã đề cập trước đó, trong hướng dẫn này, chúng ta sẽ sử dụng hợp đồng thông minh NFT hiện có này (opens in a new tab); tuy nhiên, nếu bạn muốn tìm hiểu cách chúng tôi đã tạo ra nó, hoặc tự tạo một cái, chúng tôi thực sự khuyên bạn nên xem hướng dẫn khác của chúng tôi, "Cách tạo một NFT." (opens in a new tab).
Giao diện nhị phân ứng dụng hợp đồng
Nếu bạn đã kiểm tra kỹ các tệp của chúng ta, bạn sẽ nhận thấy rằng trong thư mục src của mình, có một tệp contract-abi.json. Một Giao diện nhị phân ứng dụng là cần thiết để chỉ định hàm nào mà một hợp đồng sẽ gọi cũng như đảm bảo rằng hàm sẽ trả về dữ liệu ở định dạng bạn đang mong đợi.
Chúng ta cũng sẽ cần một khóa Giao diện Lập trình Ứng dụng Alchemy và Giao diện Lập trình Ứng dụng Alchemy Web3 để kết nối với chuỗi khối Ethereum và tải hợp đồng thông minh của mình.
Tạo khóa Giao diện Lập trình Ứng dụng Alchemy của bạn
Nếu bạn chưa có tài khoản Alchemy, hãy đăng ký miễn phí tại đây. (opens in a new tab)
Khi bạn đã tạo tài khoản Alchemy, bạn có thể tạo một khoá API bằng cách tạo một ứng dụng. Điều này sẽ cho phép chúng ta tạo các yêu cầu gửi đến mạng thử nghiệm Ropsten.
Điều hướng đến trang “Create App” trong bảng điều khiển Alchemy của bạn bằng cách di chuột qua “Apps” trong thanh điều hướng và nhấp vào “Create App”.
Đặt tên cho ứng dụng của bạn, chúng tôi đã chọn "My First NFT!", cung cấp một mô tả ngắn, chọn “Staging” cho Môi trường được sử dụng để lưu trữ ứng dụng của bạn và chọn “Ropsten” cho mạng của bạn.
Nhấp vào "Create app" và thế là xong! Ứng dụng của bạn sẽ xuất hiện trong bảng dưới đây.
Tuyệt vời, bây giờ chúng ta đã tạo URL API HTTP Alchemy của mình, hãy sao chép nó vào clipboard của bạn...
…và sau đó hãy thêm nó vào tệp .env của chúng ta. Nói chung, tệp .env của bạn sẽ trông như thế này:
1REACT_APP_PINATA_KEY = <pinata-key>2REACT_APP_PINATA_SECRET = <pinata-secret>3REACT_APP_ALCHEMY_KEY = https://eth-ropsten.alchemyapi.io/v2/<alchemy-key>Bây giờ chúng ta đã có Giao diện nhị phân ứng dụng hợp đồng và khóa Giao diện Lập trình Ứng dụng Alchemy của mình, chúng ta đã sẵn sàng để tải hợp đồng thông minh của mình bằng Alchemy Web3 (opens in a new tab).
Thiết lập điểm cuối và hợp đồng Alchemy Web3 của bạn
Đầu tiên, nếu bạn chưa có, bạn sẽ cần cài đặt Alchemy Web3 (opens in a new tab) bằng cách điều hướng đến thư mục Trang chủ: nft-minter-tutorial trong terminal:
1cd ..2npm install @alch/alchemy-web3Tiếp theo, hãy quay lại tệp interact.js của chúng ta. Ở đầu tệp, hãy thêm mã sau để nhập khóa Alchemy của bạn từ tệp .env và thiết lập điểm cuối Alchemy Web3 của bạn:
1require("dotenv").config()2const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY3const { createAlchemyWeb3 } = require("@alch/alchemy-web3")4const web3 = createAlchemyWeb3(alchemyKey)Alchemy Web3 (opens in a new tab) là một trình bao bọc xung quanh Web3.js (opens in a new tab), cung cấp các phương thức Giao diện Lập trình Ứng dụng nâng cao và các lợi ích quan trọng khác để giúp cuộc sống của bạn với tư cách là một nhà phát triển web3 dễ dàng hơn. Nó được thiết kế để yêu cầu cấu hình tối thiểu để bạn có thể bắt đầu sử dụng nó trong ứng dụng của mình ngay lập tức!
Tiếp theo, hãy thêm Giao diện nhị phân ứng dụng hợp đồng và địa chỉ hợp đồng của chúng ta vào tệp.
1require("dotenv").config()2const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY3const { createAlchemyWeb3 } = require("@alch/alchemy-web3")4const web3 = createAlchemyWeb3(alchemyKey)56const contractABI = require("../contract-abi.json")7const contractAddress = "0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE"Một khi chúng ta có cả hai, chúng ta đã sẵn sàng để bắt đầu viết mã hàm đúc của mình!
Triển khai hàm mintNFT
Bên trong tệp interact.js của bạn, hãy xác định hàm mintNFT, hàm này sẽ đúc NFT của chúng ta.
Bởi vì chúng ta sẽ thực hiện nhiều lệnh gọi bất đồng bộ (đến Pinata để ghim siêu dữ liệu của chúng ta vào IPFS, Alchemy Web3 để tải hợp đồng thông minh của chúng ta, và MetaMask để ký các giao dịch của chúng ta), hàm của chúng ta cũng sẽ là bất đồng bộ.
Ba đầu vào cho hàm của chúng ta sẽ là url của tài sản kỹ thuật số, name, và description. Thêm chữ ký hàm sau bên dưới hàm connectWallet:
1export const mintNFT = async (url, name, description) => {}Xử lý lỗi đầu vào
Đương nhiên, việc có một số loại xử lý lỗi đầu vào ở đầu hàm là hợp lý, để chúng ta thoát khỏi hàm này nếu các tham số đầu vào của chúng ta không chính xác. Bên trong hàm của chúng ta, hãy thêm mã sau:
1export const mintNFT = async (url, name, description) => {2 //xử lý lỗi3 if (url.trim() == "" || name.trim() == "" || description.trim() == "") {4 return {5 success: false,6 status: "❗Vui lòng đảm bảo tất cả các trường đã được hoàn thành trước khi đúc.",7 }8 }9}Hiện tất cảVề cơ bản, nếu bất kỳ tham số đầu vào nào là một chuỗi rỗng, thì chúng ta sẽ trả về một đối tượng JSON trong đó boolean success là false, và chuỗi status chuyển tiếp rằng tất cả các trường trong UI của chúng ta phải được hoàn thành.
Tải siêu dữ liệu lên IPFS
Một khi chúng ta biết siêu dữ liệu của mình đã được định dạng đúng cách, bước tiếp theo là gói nó vào một đối tượng JSON và tải nó lên IPFS thông qua pinJSONToIPFS mà chúng ta đã viết!
Để làm điều đó, trước tiên chúng ta cần nhập hàm pinJSONToIPFS vào tệp interact.js của mình. Ở đầu tệp interact.js, hãy thêm:
1import { pinJSONToIPFS } from "./pinata.js"Hãy nhớ lại rằng pinJSONToIPFS nhận vào một thân JSON. Vì vậy, trước khi chúng ta gọi nó, chúng ta sẽ cần định dạng các tham số url, name, và description của mình thành một đối tượng JSON.
Hãy cập nhật mã của chúng ta để tạo một đối tượng JSON có tên metadata và sau đó thực hiện một lệnh gọi đến pinJSONToIPFS với tham số metadata này:
1export const mintNFT = async (url, name, description) => {2 //xử lý lỗi3 if (url.trim() == "" || name.trim() == "" || description.trim() == "") {4 return {5 success: false,6 status: "❗Vui lòng đảm bảo tất cả các trường đã được hoàn thành trước khi đúc.",7 }8 }910 //tạo siêu dữ liệu11 const metadata = new Object()12 metadata.name = name13 metadata.image = url14 metadata.description = description1516 //thực hiện lệnh gọi pinata17 const pinataResponse = await pinJSONToIPFS(metadata)18 if (!pinataResponse.success) {19 return {20 success: false,21 status: "😢 Đã xảy ra lỗi khi tải lên tokenURI của bạn.",22 }23 }24 const tokenURI = pinataResponse.pinataUrl25}Hiện tất cảLưu ý, chúng ta lưu trữ phản hồi của lệnh gọi đến pinJSONToIPFS(metadata) trong đối tượng pinataResponse. Sau đó, chúng ta phân tích đối tượng này để tìm bất kỳ lỗi nào.
Nếu có lỗi, chúng ta sẽ trả về một đối tượng JSON trong đó boolean success là false và chuỗi status của chúng ta chuyển tiếp rằng lệnh gọi của chúng ta đã thất bại. Ngược lại, chúng ta trích xuất pinataURL từ pinataResponse và lưu trữ nó làm biến tokenURI của mình.
Bây giờ là lúc tải hợp đồng thông minh của chúng ta bằng Giao diện Lập trình Ứng dụng Alchemy Web3 mà chúng ta đã khởi tạo ở đầu tệp của mình. Thêm dòng mã sau vào cuối hàm mintNFT để thiết lập hợp đồng tại biến toàn cục window.contract:
1window.contract = await new web3.eth.Contract(contractABI, contractAddress)Điều cuối cùng cần thêm vào hàm mintNFT của chúng ta là giao dịch Ethereum của chúng ta:
1//thiết lập giao dịch Ethereum của bạn2const transactionParameters = {3 to: contractAddress, // Bắt buộc ngoại trừ trong quá trình xuất bản hợp đồng.4 from: window.ethereum.selectedAddress, // phải khớp với địa chỉ đang hoạt động của người dùng.5 data: window.contract.methods6 .mintNFT(window.ethereum.selectedAddress, tokenURI)7 .encodeABI(), //thực hiện lệnh gọi đến hợp đồng thông minh NFT8}910//ký giao dịch qua MetaMask11try {12 const txHash = await window.ethereum.request({13 method: "eth_sendTransaction",14 params: [transactionParameters],15 })16 return {17 success: true,18 status:19 "✅ Kiểm tra giao dịch của bạn trên Etherscan: https://ropsten.etherscan.io/tx/" +20 txHash,21 }22} catch (error) {23 return {24 success: false,25 status: "😥 Đã xảy ra lỗi: " + error.message,26 }27}Hiện tất cảNếu bạn đã quen thuộc với các giao dịch Ethereum, bạn sẽ nhận thấy rằng cấu trúc khá tương tự với những gì bạn đã thấy.
- Đầu tiên, chúng ta thiết lập các tham số giao dịch của mình.
tochỉ định địa chỉ người nhận (hợp đồng thông minh của chúng ta)fromchỉ định người ký giao dịch (địa chỉ được kết nối của người dùng với MetaMask:window.ethereum.selectedAddress)datachứa lệnh gọi đến phương thứcmintNFTcủa hợp đồng thông minh của chúng ta, phương thức này nhậntokenURIvà địa chỉ ví của người dùng,window.ethereum.selectedAddress, làm đầu vào
- Sau đó, chúng ta thực hiện một lệnh gọi await,
window.ethereum.request,trong đó chúng ta yêu cầu MetaMask ký giao dịch. Lưu ý, trong yêu cầu này, chúng ta đang chỉ định phương thức eth của mình (eth_SentTransaction) và chuyển vào cáctransactionParameterscủa chúng ta. Tại thời điểm này, MetaMask sẽ mở ra trong trình duyệt và nhắc người dùng ký hoặc từ chối giao dịch.- Nếu giao dịch thành công, hàm sẽ trả về một đối tượng JSON trong đó boolean
successđược thiết lập là true và chuỗistatusnhắc người dùng kiểm tra Etherscan để biết thêm thông tin về giao dịch của họ. - Nếu giao dịch không thành công, hàm sẽ trả về một đối tượng JSON trong đó boolean
successđược thiết lập là false và chuỗistatuschuyển tiếp thông điệp lỗi.
- Nếu giao dịch thành công, hàm sẽ trả về một đối tượng JSON trong đó boolean
Nói chung, hàm mintNFT của chúng ta sẽ trông như thế này:
1export const mintNFT = async (url, name, description) => {2 //xử lý lỗi3 if (url.trim() == "" || name.trim() == "" || description.trim() == "") {4 return {5 success: false,6 status: "❗Vui lòng đảm bảo tất cả các trường đã được hoàn thành trước khi đúc.",7 }8 }910 //tạo siêu dữ liệu11 const metadata = new Object()12 metadata.name = name13 metadata.image = url14 metadata.description = description1516 //yêu cầu ghim pinata17 const pinataResponse = await pinJSONToIPFS(metadata)18 if (!pinataResponse.success) {19 return {20 success: false,21 status: "😢 Đã xảy ra lỗi khi tải lên tokenURI của bạn.",22 }23 }24 const tokenURI = pinataResponse.pinataUrl2526 //tải hợp đồng thông minh27 window.contract = await new web3.eth.Contract(contractABI, contractAddress) //loadContract();2829 //thiết lập giao dịch Ethereum của bạn30 const transactionParameters = {31 to: contractAddress, // Bắt buộc ngoại trừ trong quá trình xuất bản hợp đồng.32 from: window.ethereum.selectedAddress, // phải khớp với địa chỉ đang hoạt động của người dùng.33 data: window.contract.methods34 .mintNFT(window.ethereum.selectedAddress, tokenURI)35 .encodeABI(), //thực hiện lệnh gọi đến hợp đồng thông minh NFT36 }3738 //ký giao dịch qua MetaMask39 try {40 const txHash = await window.ethereum.request({41 method: "eth_sendTransaction",42 params: [transactionParameters],43 })44 return {45 success: true,46 status:47 "✅ Kiểm tra giao dịch của bạn trên Etherscan: https://ropsten.etherscan.io/tx/" +48 txHash,49 }50 } catch (error) {51 return {52 success: false,53 status: "😥 Đã xảy ra lỗi: " + error.message,54 }55 }56}Hiện tất cảĐó là một hàm khổng lồ! Bây giờ, chúng ta chỉ cần kết nối hàm mintNFT của mình với thành phần Minter.js...
Kết nối mintNFT với frontend Minter.js của chúng ta
Mở tệp Minter.js của bạn và cập nhật dòng import { connectWallet, getCurrentWalletConnected } from "./utils/interact.js"; ở trên cùng thành:
1import {2 connectWallet,3 getCurrentWalletConnected,4 mintNFT,5} from "./utils/interact.js"Cuối cùng, triển khai hàm onMintPressed để thực hiện lệnh gọi await đến hàm mintNFT đã nhập của bạn và cập nhật biến trạng thái status để phản ánh xem giao dịch của chúng ta có thành công hay không:
1const onMintPressed = async () => {2 const { status } = await mintNFT(url, name, description)3 setStatus(status)4}Triển khai NFT của bạn lên một trang web trực tiếp
Sẵn sàng đưa dự án của bạn lên mạng để người dùng tương tác chưa? Hãy xem hướng dẫn này (opens in a new tab) để triển khai Minter của bạn lên một trang web trực tiếp.
Một bước cuối cùng...
Khuấy đảo thế giới blockchain
Chỉ đùa thôi, bạn đã hoàn thành hướng dẫn rồi!
Tóm lại, bằng cách xây dựng một NFT minter, bạn đã học thành công cách:
- Kết nối với MetaMask thông qua dự án frontend của bạn
- Gọi các phương thức hợp đồng thông minh từ frontend của bạn
- Ký các giao dịch bằng MetaMask
Có lẽ, bạn muốn có thể khoe các NFT được đúc thông qua ứng dụng phi tập trung của mình trong ví của bạn — vì vậy hãy chắc chắn xem hướng dẫn nhanh của chúng tôi Cách xem NFT của bạn trong Ví (opens in a new tab)!
Và, như mọi khi, nếu bạn có bất kỳ câu hỏi nào, chúng tôi ở đây để Trợ giúp trong Alchemy Discord (opens in a new tab). Chúng tôi rất nóng lòng được xem bạn áp dụng các khái niệm từ hướng dẫn này vào các dự án trong tương lai của mình như thế nào!
Lần cập nhật trang lần cuối: 25 tháng 2, 2026