Xây dựng giao diện người dùng cho hợp đồng của bạn
Bạn đã tìm thấy một tính năng mà chúng ta cần trong hệ sinh thái Ethereum. Bạn đã viết các hợp đồng thông minh để triển khai nó, và thậm chí có thể là một số mã liên quan chạy ngoài chuỗi. Điều này thật tuyệt! Thật không may, nếu không có giao diện người dùng, bạn sẽ không có bất kỳ người dùng nào, và lần cuối cùng bạn viết một trang web là khi mọi người còn sử dụng modem quay số và JavaScript vẫn còn mới mẻ.
Bài viết này là dành cho bạn. Tôi giả định rằng bạn biết lập trình, và có thể là một chút JavaScript và HTML, nhưng kỹ năng làm giao diện người dùng của bạn đã bị mai một và lỗi thời. Cùng nhau, chúng ta sẽ xem xét một ứng dụng hiện đại đơn giản để bạn thấy cách mọi thứ được thực hiện ngày nay.
Tại sao điều này lại quan trọng
Về lý thuyết, bạn có thể chỉ cần yêu cầu mọi người sử dụng Etherscan (opens in a new tab) hoặc Blockscout (opens in a new tab) để tương tác với các hợp đồng của bạn. Điều đó thật tuyệt đối với những người dùng Ethereum có kinh nghiệm. Nhưng chúng ta đang cố gắng phục vụ một tỷ người khác (opens in a new tab). Điều này sẽ không xảy ra nếu không có trải nghiệm người dùng tuyệt vời, và một giao diện người dùng thân thiện là một phần lớn trong đó.
Ứng dụng Greeter
Có rất nhiều lý thuyết đằng sau cách hoạt động của giao diện người dùng hiện đại, và rất nhiều trang web hay (opens in a new tab) giải thích về nó (opens in a new tab). Thay vì lặp lại công việc tuyệt vời mà các trang web đó đã làm, tôi sẽ giả định rằng bạn thích học qua thực hành hơn và bắt đầu với một ứng dụng mà bạn có thể thử nghiệm. Bạn vẫn cần lý thuyết để hoàn thành công việc, và chúng ta sẽ đi đến phần đó - chúng ta sẽ chỉ đi qua từng tệp mã nguồn một, và thảo luận về mọi thứ khi chúng ta gặp chúng.
Cài đặt
-
Ứng dụng sử dụng mạng lưới thử nghiệm Sepolia (opens in a new tab). Nếu cần, hãy nhận ETH thử nghiệm Sepolia và thêm Sepolia vào Ví của bạn (opens in a new tab).
-
Sao chép kho lưu trữ GitHub và cài đặt các gói cần thiết.
git clone https://github.com/qbzzt/260301-modern-ui-web3.git cd 260301-modern-ui-web3 npm install -
Ứng dụng sử dụng các điểm truy cập miễn phí, vốn có những hạn chế về hiệu suất. Nếu bạn muốn sử dụng một nhà cung cấp Nút dưới dạng dịch vụ (Node as a service), hãy thay thế các URL trong
src/wagmi.ts. -
Khởi chạy ứng dụng.
npm run dev -
Duyệt đến URL được hiển thị bởi ứng dụng. Trong hầu hết các trường hợp, đó là http://localhost:5173/ (opens in a new tab).
-
Bạn có thể xem mã nguồn hợp đồng, một phiên bản sửa đổi của Greeter từ Hardhat, trên một trình khám phá Chuỗi khối (opens in a new tab).
Hướng dẫn chi tiết từng tệp
index.html
Tệp này là một mẫu HTML tiêu chuẩn ngoại trừ dòng này, dùng để nhập tệp tập lệnh.
<script type="module" src="/src/main.tsx"></script>
src/main.tsx
Phần mở rộng tệp chỉ ra rằng đây là một thành phần React (opens in a new tab) được viết bằng TypeScript (opens in a new tab), một phần mở rộng của JavaScript hỗ trợ kiểm tra kiểu (opens in a new tab). TypeScript được biên dịch sang JavaScript, vì vậy chúng ta có thể sử dụng nó ở phía máy khách.
Tệp này chủ yếu được giải thích trong trường hợp bạn quan tâm. Thông thường bạn không sửa đổi tệp này, mà là src/App.tsx và các tệp mà nó nhập vào.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
Nhập mã Thư viện mà chúng ta cần.
import App from './App.tsx'
Nhập thành phần React triển khai ứng dụng (xem bên dưới).
import { config } from './wagmi.ts'
Nhập cấu hình Wagmi (opens in a new tab), bao gồm cấu hình Chuỗi khối.
const queryClient = new QueryClient()
Tạo một phiên bản mới của trình quản lý bộ nhớ đệm của React Query (opens in a new tab). Đối tượng này sẽ lưu trữ:
- Các lệnh gọi RPC được lưu trong bộ nhớ đệm
- Các lần đọc hợp đồng
- Trạng thái tìm nạp lại trong nền
Chúng ta cần trình quản lý bộ nhớ đệm vì Wagmi v3 sử dụng React Query ở bên trong.
ReactDOM.createRoot(document.getElementById('root')!).render(
Tạo thành phần React gốc. Tham số cho render là JSX (opens in a new tab), một ngôn ngữ mở rộng sử dụng cả HTML và JavaScript/TypeScript. Dấu chấm than ở đây nói với thành phần TypeScript rằng: "bạn không biết rằng document.getElementById('root') sẽ là một tham số hợp lệ cho ReactDOM.createRoot, nhưng đừng lo - tôi là nhà phát triển và tôi đang nói với bạn rằng nó sẽ hợp lệ".
<React.StrictMode>
Ứng dụng sẽ nằm bên trong một thành phần React.StrictMode (opens in a new tab). Thành phần này yêu cầu Thư viện React chèn thêm các kiểm tra gỡ lỗi, điều này rất hữu ích trong quá trình phát triển.
<WagmiProvider config={config}>
Ứng dụng cũng nằm bên trong một thành phần WagmiProvider (opens in a new tab). Thư viện Wagmi (chúng ta sẽ tạo nó) (opens in a new tab) kết nối các định nghĩa giao diện người dùng React với Thư viện Viem (opens in a new tab) để viết một ứng dụng phi tập trung (dapp) Ethereum.
<QueryClientProvider client={queryClient}>
Và cuối cùng, thêm một nhà cung cấp React Query để bất kỳ thành phần ứng dụng nào cũng có thể sử dụng các truy vấn được lưu trong bộ nhớ đệm.
<App />
Bây giờ chúng ta có thể có thành phần cho ứng dụng, nơi thực sự triển khai giao diện người dùng. /> ở cuối thành phần cho React biết rằng thành phần này không có bất kỳ định nghĩa nào bên trong nó, theo tiêu chuẩn XML.
</QueryClientProvider>
</WagmiProvider>
</React.StrictMode>,
)
Tất nhiên, chúng ta phải đóng các thành phần khác lại.
src/App.tsx
import {
useConnect,
useConnection,
useDisconnect,
useSwitchChain
} from 'wagmi'
import { useEffect } from 'react'
import { Greeter } from './Greeter'
Nhập các Thư viện chúng ta cần, cũng như thành phần Greeter.
const SEPOLIA_CHAIN_ID = 11155111
ID Chuỗi của Sepolia.
function App() {
Đây là cách tiêu chuẩn để tạo một thành phần React: định nghĩa một hàm được gọi bất cứ khi nào nó cần được hiển thị. Hàm này thường chứa mã TypeScript hoặc JavaScript, theo sau là một câu lệnh return trả về mã JSX.
const connection = useConnection()
Sử dụng useConnection (opens in a new tab) để lấy thông tin liên quan đến kết nối hiện tại, chẳng hạn như Địa chỉ và chainId.
Theo quy ước, trong React các hàm được gọi là use... là các hook (opens in a new tab). Các hàm này không chỉ trả về dữ liệu cho thành phần; chúng còn đảm bảo nó được hiển thị lại (hàm thành phần được thực thi lại và đầu ra của nó thay thế đầu ra trước đó trong HTML) khi dữ liệu đó thay đổi.
const { connectors, connect, status, error } = useConnect()
Sử dụng useConnect (opens in a new tab) để lấy thông tin về kết nối Ví.
const { disconnect } = useDisconnect()
Hook này (opens in a new tab) cung cấp cho chúng ta hàm để ngắt kết nối khỏi Ví.
const { switchChain } = useSwitchChain()
Hook này (opens in a new tab) cho phép chúng ta chuyển đổi Chuỗi.
useEffect(() => {
Hook React useEffect (opens in a new tab) cho phép bạn chạy một hàm bất cứ khi nào giá trị của một biến thay đổi để đồng bộ hóa một hệ thống bên ngoài.
if (connection.status === 'connected' &&
connection.chainId !== SEPOLIA_CHAIN_ID
) {
switchChain({ chainId: SEPOLIA_CHAIN_ID })
}
Nếu chúng ta đã kết nối, nhưng không phải với Chuỗi khối Sepolia, hãy chuyển sang Sepolia.
}, [connection.status, connection.chainId])
Chạy lại hàm mỗi khi trạng thái kết nối hoặc chainId của kết nối thay đổi.
return (
<>
JSX của một thành phần React phải trả về một thành phần HTML duy nhất. Khi chúng ta có nhiều thành phần và không cần một vùng chứa để bọc tất cả chúng lại, chúng ta sử dụng một thành phần trống (<> ... </>) để kết hợp chúng thành một thành phần duy nhất.
<h2>Connection</h2>
<div>
status: {connection.status}
<br />
addresses: {JSON.stringify(connection.addresses)}
<br />
chainId: {connection.chainId}
</div>
Cung cấp thông tin về kết nối hiện tại. Trong JSX, {<expression>} có nghĩa là đánh giá biểu thức dưới dạng JavaScript.
{connection.status === 'connected' && (
Cú pháp {<condition> && <value>} means "if the condition is true, evaluate to the value; if it isn't, evaluate to false`".
Đây là cách tiêu chuẩn để đặt các câu lệnh if bên trong JSX.
<div>
<Greeter />
<hr />
JSX tuân theo tiêu chuẩn XML, nghiêm ngặt hơn HTML. Nếu một thẻ không có thẻ đóng tương ứng, nó phải có một dấu gạch chéo (/) ở cuối để kết thúc nó.
Ở đây chúng ta có hai thẻ như vậy, <Greeter /> (thực sự chứa mã HTML giao tiếp với hợp đồng) và <hr /> cho một đường ngang (opens in a new tab).
<button type="button" onClick={disconnect}>
Disconnect
</button>
</div>
)}
Nếu người dùng nhấp vào nút này, hãy gọi hàm disconnect.
{connection.status !== 'connected' && (
Nếu chúng ta chưa kết nối, hãy hiển thị các tùy chọn cần thiết để kết nối với Ví.
<div>
<h2>Connect</h2>
{connectors.map((connector) => (
Trong connectors chúng ta có một danh sách các trình kết nối. Chúng ta sử dụng map (opens in a new tab) để biến nó thành một danh sách các nút JSX để hiển thị.
<button
key={connector.uid}
Trong JSX, các thẻ "anh em" (các thẻ có cùng một thẻ cha) cần phải có các định danh khác nhau.
onClick={() => connect({ connector })}
type="button"
>
{connector.name}
</button>
))}
Các nút trình kết nối.
<div>{status}</div>
<div>{error?.message}</div>
</div>
)}
Cung cấp thông tin bổ sung. Cú pháp biểu thức <variable>?.<field> cho JavaScript biết rằng nếu biến được định nghĩa, hãy đánh giá trường đó. Nếu biến không được định nghĩa, thì biểu thức này đánh giá thành undefined.
Biểu thức error.message, khi không có lỗi, sẽ gây ra một ngoại lệ. Sử dụng error?.message cho phép chúng ta tránh được vấn đề này.
src/Greeter.tsx
Tệp này chứa hầu hết các chức năng của giao diện người dùng. Nó bao gồm các định nghĩa mà thông thường sẽ nằm trong nhiều tệp, nhưng vì đây là một hướng dẫn, chương trình được tối ưu hóa để dễ hiểu trong lần đầu tiên, thay vì hiệu suất hoặc tính dễ bảo trì.
import {
useState,
useEffect,
} from 'react'
import { useChainId,
useAccount,
useReadContract,
useWriteContract,
useWatchContractEvent,
useSimulateContract
} from 'wagmi'
Chúng ta sử dụng các hàm Thư viện này. Một lần nữa, chúng được giải thích bên dưới ở nơi chúng được sử dụng.
import { AddressType } from 'abitype'
Thư viện abitype (opens in a new tab) cung cấp cho chúng ta các định nghĩa TypeScript cho các kiểu dữ liệu Ethereum khác nhau, chẳng hạn như AddressType (opens in a new tab).
let greeterABI = [
{ "type": "function", "name": "greet", ... },
{ "type": "function", "name": "setGreeting", ... },
{ "type": "event", "name": "SetGreeting", ... },
] as const // greeterABI
ABI cho hợp đồng Greeter.
Nếu bạn đang phát triển các hợp đồng và giao diện người dùng cùng một lúc, bạn thường sẽ đặt chúng trong cùng một kho lưu trữ và sử dụng ABI được tạo bởi trình biên dịch Solidity dưới dạng một tệp trong ứng dụng của bạn. Tuy nhiên, điều này không cần thiết ở đây vì hợp đồng đã được phát triển và sẽ không thay đổi.
Chúng ta sử dụng as const (opens in a new tab) để nói với TypeScript rằng đây là một hằng số thực sự. Thông thường, khi bạn chỉ định trong JavaScript const x = {"a": 1}, bạn có thể thay đổi giá trị trong x, bạn chỉ không thể gán lại cho nó.
type AddressPerBlockchainType = {
[key: number]: AddressType
}
TypeScript được định kiểu mạnh. Chúng ta sử dụng định nghĩa này để chỉ định Địa chỉ nơi hợp đồng Greeter được triển khai trên các Chuỗi khác nhau. Khóa là một số (chainId), và giá trị là một AddressType (một Địa chỉ).
const contractAddrs : AddressPerBlockchainType = {
// Sepolia
11155111: '0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA'
}
Địa chỉ của hợp đồng trên Sepolia (opens in a new tab).
Thành phần Timer
Thành phần Timer hiển thị số giây kể từ một thời điểm nhất định. Điều này rất quan trọng cho mục đích khả năng sử dụng. Khi người dùng làm điều gì đó, họ mong đợi một phản ứng ngay lập tức. Trong các Chuỗi khối, điều này thường là không thể vì không có gì xảy ra cho đến khi một giao dịch được đưa vào một khối. Một giải pháp là hiển thị thời gian đã trôi qua kể từ khi người dùng thực hiện hành động, để người dùng có thể quyết định xem thời gian yêu cầu có hợp lý hay không.
type TimerProps = {
lastUpdate: Date
}
Thành phần Timer nhận một tham số, lastUpdate, là thời gian của hành động cuối cùng.
const Timer = ({ lastUpdate }: TimerProps) => {
const [_, setNow] = useState(new Date())
Chúng ta cần có trạng thái (một biến gắn liền với thành phần) và cập nhật nó để thành phần hoạt động chính xác. Nhưng chúng ta không bao giờ cần đọc nó, vì vậy đừng bận tâm đến việc tạo một biến.
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000)
return () => clearInterval(id)
}, [])
Hàm setInterval (opens in a new tab) cho phép chúng ta lên lịch để một hàm chạy định kỳ. Trong trường hợp này, mỗi giây. Hàm gọi setNow để cập nhật trạng thái, vì vậy thành phần Timer sẽ được hiển thị lại. Chúng ta bọc nó bên trong useEffect (opens in a new tab) với một danh sách phụ thuộc trống để nó chỉ xảy ra một lần, thay vì mỗi lần thành phần được hiển thị.
const secondsSinceUpdate = Math.floor(
(Date.now() - lastUpdate.getTime()) / 1000
)
return (
<span>{secondsSinceUpdate} seconds ago</span>
)
}
Tính số giây kể từ lần cập nhật cuối cùng và trả về nó.
Thành phần Greeter
const Greeter = () => {
Cuối cùng, chúng ta bắt đầu định nghĩa thành phần.
const chainId = useChainId()
const account = useAccount()
Thông tin về Chuỗi và tài khoản chúng ta đang sử dụng, được cung cấp bởi Wagmi (opens in a new tab). Vì đây là một hook (use...), thành phần sẽ được hiển thị lại bất cứ khi nào thông tin này thay đổi.
const greeterAddr = chainId && contractAddrs[chainId]
Địa chỉ của hợp đồng Greeter, sẽ là undefined nếu chúng ta không có thông tin Chuỗi, hoặc chúng ta đang ở trên một Chuỗi không có hợp đồng đó.
const readResults = useReadContract({
address: greeterAddr,
abi: greeterABI,
functionName: "greet", // Không có đối số
})
Hook useReadContract (opens in a new tab) gọi hàm greet của hợp đồng (opens in a new tab).
const [ currentGreeting, setCurrentGreeting ] =
useState("Please wait while we fetch the greeting from the blockchain...")
const [ newGreeting, setNewGreeting ] = useState("")
Hook useState (opens in a new tab) của React cho phép chúng ta chỉ định một biến trạng thái, giá trị của nó được duy trì từ lần hiển thị này sang lần hiển thị khác của thành phần. Giá trị ban đầu là tham số, trong trường hợp này là chuỗi rỗng.
Hook useState trả về một danh sách với hai giá trị:
- Giá trị hiện tại của biến trạng thái.
- Một hàm để sửa đổi biến trạng thái khi cần. Vì đây là một hook, mỗi khi nó được gọi, thành phần sẽ được hiển thị lại.
Trong trường hợp này, chúng ta đang sử dụng một biến trạng thái cho lời chào mới mà người dùng muốn thiết lập.
const [ lastSetterAddress, setLastSetterAddress ] = useState("")
Nếu nhiều người dùng đang sử dụng cùng một hợp đồng cùng một lúc, họ có thể ghi đè lên lời chào của nhau. Điều này sẽ khiến người dùng có vẻ như ứng dụng đang bị lỗi. Nếu ứng dụng hiển thị ai là người thiết lập lời chào cuối cùng, người dùng sẽ biết đó là người khác và ứng dụng đang hoạt động chính xác.
const [ status, setStatus ] = useState("")
const [ statusTime, setStatusTime ] = useState(new Date())
Người dùng thích thấy rằng hành động của họ có hiệu lực ngay lập tức. Tuy nhiên, trên một Chuỗi khối, điều này không xảy ra. Các biến trạng thái này cho phép chúng ta ít nhất hiển thị một cái gì đó cho người dùng để họ biết hành động của họ đang được tiến hành.
useEffect(() => {
if (readResults.data) {
setCurrentGreeting(readResults.data)
setStatus("Greeting fetched from blockchain")
}
}, [readResults.data])
Nếu readResults ở trên thay đổi dữ liệu và nó không được đặt thành giá trị sai (ví dụ: undefined), hãy cập nhật lời chào hiện tại thành lời chào được đọc từ Chuỗi khối. Đồng thời, cập nhật trạng thái.
useWatchContractEvent({
address: greeterAddr,
abi: greeterABI,
eventName: 'SetGreeting',
chainId,
Lắng nghe các sự kiện SetGreeting.
enabled: !!greeterAddr,
!!<value> có nghĩa là nếu giá trị là false, hoặc một giá trị được đánh giá là sai, chẳng hạn như undefined, 0, hoặc một chuỗi rỗng, thì toàn bộ biểu thức là false. Đối với bất kỳ giá trị nào khác, nó là true. Đó là một cách để chuyển đổi các giá trị thành boolean, bởi vì nếu không có greeterAddr, chúng ta không muốn lắng nghe các sự kiện.
onLogs: logs => {
const greetingFromContract = logs[0].args.greeting
setCurrentGreeting(greetingFromContract)
setLastSetterAddress(logs[0].args.sender)
updateStatus("Greeting updated by event")
},
})
Khi chúng ta thấy nhật ký (điều này xảy ra khi chúng ta thấy một sự kiện mới), điều đó có nghĩa là lời chào đã được sửa đổi. Trong trường hợp đó, chúng ta có thể cập nhật currentGreeting và lastSetterAddress thành các giá trị mới. Đồng thời, chúng ta muốn cập nhật hiển thị trạng thái.
const updateStatus = (newStatus: string) => {
setStatus(newStatus)
setStatusTime(new Date())
}
Khi chúng ta cập nhật trạng thái, chúng ta muốn làm hai việc:
- Cập nhật chuỗi trạng thái (
status) - Cập nhật thời gian của lần cập nhật trạng thái cuối cùng (
statusTime) thành hiện tại.
const greetingChange = (evt) =>
setNewGreeting(evt.target.value)
Đây là trình xử lý sự kiện cho các thay đổi đối với trường nhập lời chào mới. Chúng ta có thể chỉ định kiểu của tham số evt, nhưng TypeScript là một ngôn ngữ tùy chọn kiểu. Vì hàm này chỉ được gọi một lần, trong một trình xử lý sự kiện HTML, tôi không nghĩ điều đó là cần thiết.
const { writeContractAsync } = useWriteContract()
Hàm để ghi vào một hợp đồng. Nó tương tự như writeContracts (opens in a new tab), nhưng cho phép cập nhật trạng thái tốt hơn.
const simulation = useSimulateContract({
address: greeterAddr,
abi: greeterABI,
functionName: 'setGreeting',
args: [newGreeting],
account: account.address
})
Đây là quá trình để gửi một giao dịch Chuỗi khối từ góc độ máy khách:
- Gửi giao dịch đến một nút trong Chuỗi khối bằng cách sử dụng
eth_estimateGas(opens in a new tab). - Chờ phản hồi từ nút.
- Khi nhận được phản hồi, yêu cầu người dùng ký giao dịch thông qua Ví. Bước này phải xảy ra sau khi nhận được phản hồi của nút vì người dùng sẽ được hiển thị chi phí gas của giao dịch trước khi ký nó.
- Chờ người dùng phê duyệt.
- Gửi lại giao dịch, lần này sử dụng
eth_sendRawTransaction(opens in a new tab).
Bước 2 có thể mất một khoảng thời gian đáng kể, trong thời gian đó người dùng có thể tự hỏi liệu lệnh của họ đã được giao diện người dùng nhận hay chưa và tại sao họ vẫn chưa được yêu cầu ký giao dịch. Điều đó tạo ra trải nghiệm người dùng (UX) kém.
Một giải pháp là gửi eth_estimateGas mỗi khi một tham số thay đổi. Sau đó, khi người dùng thực sự muốn gửi giao dịch (trong trường hợp này bằng cách nhấn Cập nhật lời chào), chi phí gas đã được biết và người dùng có thể thấy trang Ví ngay lập tức.
return (
Bây giờ cuối cùng chúng ta có thể tạo HTML thực tế để trả về.
<>
<h2>Greeter</h2>
{currentGreeting}
Hiển thị lời chào hiện tại.
{lastSetterAddress && (
<p>Last updated by {
lastSetterAddress === account.address ? "you" : lastSetterAddress
}</p>
)}
Nếu chúng ta biết ai là người thiết lập lời chào cuối cùng, hãy hiển thị thông tin đó. Greeter không theo dõi thông tin này và chúng ta không muốn xem lại các sự kiện SetGreeting, vì vậy chúng ta chỉ nhận được nó khi lời chào bị thay đổi trong khi chúng ta đang chạy.
<hr />
<input type="text"
value={newGreeting}
onChange={greetingChange}
/>
<br />
Đây là trường văn bản đầu vào nơi người dùng có thể thiết lập một lời chào mới. Mỗi khi người dùng nhấn một phím, chúng ta gọi greetingChange, hàm này sẽ gọi setNewGreeting. Vì setNewGreeting đến từ useState, nó khiến thành phần Greeter được hiển thị lại. Điều này có nghĩa là:
- Chúng ta cần chỉ định
valueđể giữ giá trị của lời chào mới, bởi vì nếu không nó sẽ quay trở lại mặc định, là chuỗi rỗng. simulationcũng được cập nhật mỗi khinewGreetingthay đổi, điều đó có nghĩa là chúng ta sẽ nhận được một mô phỏng với lời chào chính xác. Điều này có thể liên quan vì chi phí gas phụ thuộc vào kích thước của dữ liệu cuộc gọi, vốn phụ thuộc vào độ dài của chuỗi.
<button disabled={!simulation.data}
Chỉ bật nút khi chúng ta có thông tin cần thiết để gửi giao dịch.
onClick={async () => {
updateStatus("Please confirm in wallet...")
Cập nhật trạng thái. Tại thời điểm này, người dùng cần xác nhận trong Ví.
await writeContractAsync(simulation.data.request)
updateStatus("Transaction sent, waiting for greeting to change...")
}}
>
Update greeting
</button>
writeContractAsync chỉ trả về sau khi giao dịch thực sự được gửi. Điều này cho phép chúng ta hiển thị cho người dùng biết giao dịch đã chờ bao lâu để được đưa vào Chuỗi khối.
<h4>Status: {status}</h4>
<p>Updated <Timer lastUpdate={statusTime} /> </p>
</>
)
}
Hiển thị trạng thái và thời gian đã trôi qua kể từ khi nó được cập nhật.
export {Greeter}
Xuất thành phần.
src/wagmi.ts
Cuối cùng, các định nghĩa khác nhau liên quan đến Wagmi nằm trong src/wagmi.ts. Tôi sẽ không giải thích mọi thứ ở đây, bởi vì hầu hết nó là mã mẫu mà bạn không có khả năng cần phải thay đổi.
import { http, webSocket, createConfig, fallback } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'
export const config = createConfig({
chains: [sepolia],
Cấu hình Wagmi bao gồm các Chuỗi được ứng dụng này hỗ trợ. Bạn có thể xem danh sách các Chuỗi có sẵn (opens in a new tab).
connectors: [
injected(),
],
Trình kết nối này (opens in a new tab) cho phép chúng ta giao tiếp với một Ví được cài đặt trong trình duyệt.
transports: {
[sepolia.id]: http()
Điểm cuối HTTP mặc định đi kèm với Viem là đủ tốt. Nếu chúng ta muốn một URL khác, chúng ta có thể sử dụng http("https:// hostname ") hoặc webSocket("wss:// hostname ").
},
multiInjectedProviderDiscovery: false,
})
Thêm một Chuỗi khối khác
Ngày nay có rất nhiều giải pháp mở rộng quy mô L2 (opens in a new tab), và bạn có thể muốn hỗ trợ một số giải pháp mà Viem chưa hỗ trợ. Để làm điều đó, bạn sửa đổi src/wagmi.ts. Các hướng dẫn này giải thích cách thêm Optimism Sepolia (opens in a new tab).
-
Chỉnh sửa
src/wagmi.tsA. Nhập kiểu
defineChaintừ Viem.import { defineChain } from 'viem'B. Thêm định nghĩa mạng lưới. Bạn không thực sự cần làm điều này cho Optimism Sepolia, nó đã có trong
viem(opens in a new tab), nhưng bằng cách này bạn học được cách thêm một Chuỗi khối không có trongviem.const optimismSepolia = defineChain({ id: 11_155_420, name: 'OP Sepolia', nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://sepolia.optimism.io'], webSocket: ['wss://optimism-sepolia.drpc.org'], }, }, blockExplorers: { default: { name: 'Blockscout', url: 'https://optimism-sepolia.blockscout.com', apiUrl: 'https://optimism-sepolia.blockscout.com/api', } }, })C. Thêm Chuỗi mới vào lệnh gọi
createConfig.export const config = createConfig({ chains: [sepolia, optimismSepolia], connectors: [ injected(), ], transports: { [optimismSepolia.id]: http(), [sepolia.id]: http() }, multiInjectedProviderDiscovery: false, }) -
Chỉnh sửa
src/App.tsxđể nhận xét (comment out) việc tự động chuyển sang Sepolia. Trên một hệ thống sản xuất, bạn có thể sẽ hiển thị các nút có liên kết đến từng Chuỗi khối mà bạn hỗ trợ./* useEffect(() => { if (connection.status === 'connected' && connection.chainId !== SEPOLIA_CHAIN_ID ) { switchChain({ chainId: SEPOLIA_CHAIN_ID }) } }, [connection.status, connection.chainId]) */ -
Chỉnh sửa
src/Greeter.tsxđể đảm bảo rằng ứng dụng biết Địa chỉ cho các hợp đồng của bạn trên mạng lưới mới.const contractAddrs: AddressPerBlockchainType = { // Optimism Sepolia 11155420: "0x4dd85791923E9294E934271522f63875EAe5806f", // Sepolia 11155111: "0x7143d5c190F048C8d19fe325b748b081903E3BF0", } -
Trong trình duyệt của bạn.
A. Duyệt đến ChainList (opens in a new tab) và nhấp vào một trong các nút ở bên phải của bảng để thêm Chuỗi vào Ví của bạn.
B. Trong ứng dụng, Ngắt kết nối (Disconnect) và sau đó kết nối lại để thay đổi Chuỗi khối. Có những cách tốt hơn để xử lý việc này, nhưng chúng sẽ yêu cầu thay đổi ứng dụng.
Kết luận
Tất nhiên, bạn không thực sự quan tâm đến việc cung cấp giao diện người dùng cho Greeter. Bạn muốn tạo một giao diện người dùng cho các hợp đồng của riêng mình. Để tạo ứng dụng của riêng bạn, hãy chạy các bước sau:
-
Chỉ định để tạo một ứng dụng Wagmi.
npm create wagmi -
Nhập
yđể tiếp tục. -
Đặt tên cho ứng dụng.
-
Chọn framework React.
-
Chọn biến thể Vite.
Bây giờ hãy đi và làm cho các hợp đồng của bạn có thể sử dụng được cho toàn thế giới.