原文作者:PaperMoon团队
⚠️ PolkaVM 的以太坊兼容智能合约仍处于早期开发阶段,可能存在不稳定或功能不完整的情况。
去中心化应用(dApp)已经成为 Web3 生态系统的核心组成部分,它允许开发者创建能够直接与区块链网络交互的应用程序。Polkadot Hub 是一条支持智能合约功能的区块链,为 dApp 的部署和交互提供了一个非常理想的平台。
在本教程中,你将构建一个完整的 dApp,用于与部署在 Polkadot Hub 测试网 上的智能合约进行交互。该应用将使用 Ethers.js 作为区块链交互库,并使用 Next.js 作为前端框架。
完成本教程后,你将拥有一个功能齐全的 dApp,支持:
• 连接用户钱包
• 从区块链读取数据
• 发送交易并修改链上状态
前置条件
在开始之前,请确保你已经具备以下条件:
• 本地已安装 Node.js v16 或更高版本
• 一个加密钱包(如 MetaMask),并且钱包中有一些测试代币
👉 详情可参考 Connect to Polkadot 指南
• 对 React 和 JavaScript 有基本了解
• 对 区块链概念和 Solidity 有一定了解(非必须,但会有帮助)
项目概览
该 dApp 将与一个简单的 Storage 合约进行交互。你可以参考 Create Contracts 教程了解如何一步步创建该合约。
该合约支持以下功能:
• 从区块链读取一个存储的数字
• 使用新值更新该存储数字
该合约已经部署在 Polkadot Hub 测试网,可直接用于测试:
0x58053f0e8ede1a47a1af53e43368cd04ddcaf66f
如果你希望自行部署合约,可以参考 Deploying Contracts 章节。
下面是你将要构建的应用的简化结构示意图:

项目目录结构
项目最终的目录结构如下所示:
ethers-dapp
├── abis
│ └── Storage.json
└── app
├── components
│ ├── ReadContract.js
│ ├── WalletConnect.js
│ └── WriteContract.js
├── favicon.ico
├── globals.css
├── layout.js
├── page.js
└── utils
├── contract.js
└── ethers.js
项目初始化
首先,创建一个新的 Next.js 项目:
npx create-next-app ethers-dapp --js --eslint --tailwind --app --yes
cd ethers-dapp
接下来,安装所需的依赖:
npm install ethers@6.13.5
连接到 Polkadot Hub
为了与 Polkadot Hub 交互,你需要使用 Ethers.js Provider 连接到区块链网络。本示例中我们使用 Polkadot Hub 测试网,以便安全地进行实验。
新建文件 app/utils/ethers.js,并添加如下代码:
app/utils/ethers.js
import { JsonRpcProvider } from 'ethers';
export const PASSET_HUB_CONFIG = {
name: 'Passet Hub',
rpc: 'https://testnet-passet-hub-eth-rpc.polkadot.io/', // Passet Hub testnet RPC
chainId: 420420422, // Passet Hub testnet chainId
blockExplorer: 'https://blockscout-passet-hub.parity-testnet.parity.io/',
};
export const getProvider = () => {
return new JsonRpcProvider(PASSET_HUB_CONFIG.rpc, {
chainId: PASSET_HUB_CONFIG.chainId,
name: PASSET_HUB_CONFIG.name,
});
};
// Helper to get a signer from a provider
export const getSigner = async (provider) => {
if (window.ethereum) {
await window.ethereum.request({ method: 'eth_requestAccounts' });
const ethersProvider = new ethers.BrowserProvider(window.ethereum);
return ethersProvider.getSigner();
}
throw new Error('No Ethereum browser provider detected');
};
该文件完成了以下工作:
• 建立与 Polkadot Hub 测试网 的 RPC 连接
• 提供获取 Provider(只读) 与 Signer(可写) 的辅助方法
设置智能合约接口
本 dApp 使用的是一个已经部署好的 Storage 合约,因此我们需要其 ABI(应用二进制接口)。
1️⃣ 创建 ABI 文件
在项目根目录下创建 abis 文件夹,并新建 Storage.json 文件,粘贴以下 ABI 内容:
abis/Storage.json
[
{
"inputs": [
{
"internalType": "uint256",
"name": "_newNumber",
"type": "uint256"
}
],
"name": "setNumber",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "storedNumber",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]
2️⃣ 创建合约工具文件
新建文件 app/utils/contract.js:
app/utils/contract.js
import { Contract } from 'ethers';
import { getProvider } from './ethers';
import StorageABI from '../../abis/Storage.json';
export const CONTRACT_ADDRESS = '0x58053f0e8ede1a47a1af53e43368cd04ddcaf66f';
export const CONTRACT_ABI = StorageABI;
export const getContract = () => {
const provider = getProvider();
return new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, provider);
};
export const getSignedContract = async (signer) => {
return new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);
};
该文件定义了:
• 合约地址
• ABI
• 只读合约实例(Provider)
• 可写合约实例(Signer)
创建钱包连接组件
接下来,我们创建一个用于连接钱包的组件。
新建文件 app/components/WalletConnect.js:
app/components/WalletConnect.js
'use client';
import React, { useState, useEffect } from 'react';
import { PASSET_HUB_CONFIG } from '../utils/ethers';
const WalletConnect = ({ onConnect }) => {
const [account, setAccount] = useState(null);
const [chainId, setChainId] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
// Check if user already has an authorized wallet connection
const checkConnection = async () => {
if (window.ethereum) {
try {
// eth_accounts doesn't trigger the wallet popup
const accounts = await window.ethereum.request({
method: 'eth_accounts',
});
if (accounts.length > 0) {
setAccount(accounts[0]);
const chainIdHex = await window.ethereum.request({
method: 'eth_chainId',
});
setChainId(parseInt(chainIdHex, 16));
}
} catch (err) {
console.error('Error checking connection:', err);
setError('Failed to check wallet connection');
}
}
};
checkConnection();
if (window.ethereum) {
// Setup wallet event listeners
window.ethereum.on('accountsChanged', (accounts) => {
setAccount(accounts[0] || null);
if (accounts[0] && onConnect) onConnect(accounts[0]);
});
window.ethereum.on('chainChanged', (chainIdHex) => {
setChainId(parseInt(chainIdHex, 16));
});
}
return () => {
// Cleanup event listeners
if (window.ethereum) {
window.ethereum.removeListener('accountsChanged', () => {});
window.ethereum.removeListener('chainChanged', () => {});
}
};
}, [onConnect]);
const connectWallet = async () => {
if (!window.ethereum) {
setError(
'MetaMask not detected! Please install MetaMask to use this dApp.'
);
return;
}
try {
// eth_requestAccounts triggers the wallet popup
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
setAccount(accounts[0]);
const chainIdHex = await window.ethereum.request({
method: 'eth_chainId',
});
const currentChainId = parseInt(chainIdHex, 16);
setChainId(currentChainId);
// Prompt user to switch networks if needed
if (currentChainId !== PASSET_HUB_CONFIG.chainId) {
await switchNetwork();
}
if (onConnect) onConnect(accounts[0]);
} catch (err) {
console.error('Error connecting to wallet:', err);
setError('Failed to connect wallet');
}
};
const switchNetwork = async () => {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${PASSET_HUB_CONFIG.chainId.toString(16)}` }],
});
} catch (switchError) {
// Error 4902 means the chain hasn't been added to MetaMask
if (switchError.code === 4902) {
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: `0x${PASSET_HUB_CONFIG.chainId.toString(16)}`,
chainName: PASSET_HUB_CONFIG.name,
rpcUrls: [PASSET_HUB_CONFIG.rpc],
blockExplorerUrls: [PASSET_HUB_CONFIG.blockExplorer],
},
],
});
} catch (addError) {
setError('Failed to add network to wallet');
}
} else {
setError('Failed to switch network');
}
}
};
// UI-only disconnection - MetaMask doesn't support programmatic disconnection
const disconnectWallet = () => {
setAccount(null);
};
return (
<div className="border border-pink-500 rounded-lg p-4 shadow-md bg-white text-pink-500 max-w-sm mx-auto">
{error && <p className="text-red-500 text-sm mb-2">{error}</p>}
{!account ? (
<button
onClick={connectWallet}
className="w-full bg-pink-500 hover:bg-pink-600 text-white font-bold py-2 px-4 rounded-lg transition"
>
Connect Wallet
</button>
) : (
<div className="flex flex-col items-center">
<span className="text-sm font-mono bg-pink-100 px-2 py-1 rounded-md text-pink-700">
{`${account.substring(0, 6)}...${account.substring(38)}`}
</span>
<button
onClick={disconnectWallet}
className="mt-3 w-full bg-gray-200 hover:bg-gray-300 text-pink-500 py-2 px-4 rounded-lg transition"
>
Disconnect
</button>
{chainId !== PASSET_HUB_CONFIG.chainId && (
<button
onClick={switchNetwork}
className="mt-3 w-full bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded-lg transition"
>
Switch to Passet Hub
</button>
)}
</div>
)}
</div>
);
};
export default WalletConnect;
将组件集成到页面中
为了将钱包连接组件集成到你的 dApp 中,需要覆盖 app/page.js 中原有的 Next.js 模板代码,替换为如下内容:
app/page.js
'use client';
import { useState } from 'react';
import WalletConnect from './components/WalletConnect';
export default function Home() {
const [account, setAccount] = useState(null);
const handleConnect = (connectedAccount) => {
setAccount(connectedAccount);
};
return (
<section className="min-h-screen bg-white text-black flex flex-col justify-center items-center gap-4 py-10">
<h1 className="text-2xl font-semibold text-center">
Ethers.js dApp - Passet Hub Smart Contracts
</h1>
<WalletConnect onConnect={handleConnect} />
</section>
);
}
在终端中运行以下命令启动项目:
npm run dev
此时你将看到如下界面:

从区块链读取数据
现在,我们来创建一个组件,用于从智能合约中读取数据。
新建文件 app/components/ReadContract.js:
app/components/ReadContract.js
'use client';
import React, { useState, useEffect } from 'react';
import { getContract } from '../utils/contract';
const ReadContract = () => {
const [storedNumber, setStoredNumber] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Function to read data from the blockchain
const fetchData = async () => {
try {
setLoading(true);
const contract = getContract();
// Call the smart contract's storedNumber function
const number = await contract.storedNumber();
setStoredNumber(number.toString());
setError(null);
} catch (err) {
console.error('Error fetching stored number:', err);
setError('Failed to fetch data from the contract');
} finally {
setLoading(false);
}
};
fetchData();
// Poll for updates every 10 seconds to keep UI in sync with blockchain
const interval = setInterval(fetchData, 10000);
// Clean up interval on component unmount
return () => clearInterval(interval);
}, []);
return (
<div className="border border-pink-500 rounded-lg p-4 shadow-md bg-white text-pink-500 max-w-sm mx-auto">
<h2 className="text-lg font-bold text-center mb-4">Contract Data</h2>
{loading ? (
<div className="flex justify-center my-4">
<div className="w-6 h-6 border-4 border-pink-500 border-t-transparent rounded-full animate-spin"></div>
</div>
) : error ? (
<p className="text-red-500 text-center">{error}</p>
) : (
<div className="text-center">
<p className="text-sm font-mono bg-pink-100 px-2 py-1 rounded-md text-pink-700">
<strong>Stored Number:</strong> {storedNumber}
</p>
</div>
)}
</div>
);
};
export default ReadContract;
该组件会调用合约中的 storedNumber 函数,并将返回值展示给用户。同时,它还设置了一个 轮询机制,每 10 秒从区块链重新获取一次数据,以保持 UI 与链上状态同步。
将 ReadContract 集成到页面中
为了在页面中看到读取的合约数据,需要再次更新 app/page.js:
app/page.js
'use client';
import { useState } from 'react';
import WalletConnect from './components/WalletConnect';
import ReadContract from './components/ReadContract';
export default function Home() {
const [account, setAccount] = useState(null);
const handleConnect = (connectedAccount) => {
setAccount(connectedAccount);
};
return (
<section className="min-h-screen bg-white text-black flex flex-col justify-center items-center gap-4 py-10">
<h1 className="text-2xl font-semibold text-center">
Ethers.js dApp - Passet Hub Smart Contracts
</h1>
<WalletConnect onConnect={handleConnect} />
<ReadContract />
</section>
);
}
此时,你的 dApp 会自动更新为如下状态:

向区块链写入数据
最后,我们创建一个组件,允许用户向智能合约写入数据(即更新存储的数字)。
新建文件 app/components/WriteContract.js:
app/components/WriteContract.js
'use client';
import { useState } from 'react';
import { getSignedContract } from '../utils/contract';
import { ethers } from 'ethers';
const WriteContract = ({ account }) => {
const [newNumber, setNewNumber] = useState('');
const [status, setStatus] = useState({ type: null, message: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
// Validation checks
if (!account) {
setStatus({ type: 'error', message: 'Please connect your wallet first' });
return;
}
if (!newNumber || isNaN(Number(newNumber))) {
setStatus({ type: 'error', message: 'Please enter a valid number' });
return;
}
try {
setIsSubmitting(true);
setStatus({ type: 'info', message: 'Initiating transaction...' });
// Get a signer from the connected wallet
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = await getSignedContract(signer);
// Send transaction to blockchain and wait for user confirmation in wallet
setStatus({
type: 'info',
message: 'Please confirm the transaction in your wallet...',
});
// Call the contract's setNumber function
const tx = await contract.setNumber(newNumber);
// Wait for transaction to be mined
setStatus({
type: 'info',
message: 'Transaction submitted. Waiting for confirmation...',
});
const receipt = await tx.wait();
setStatus({
type: 'success',
message: `Transaction confirmed! Transaction hash: ${receipt.hash}`,
});
setNewNumber('');
} catch (err) {
console.error('Error updating number:', err);
// Error code 4001 is MetaMask's code for user rejection
if (err.code === 4001) {
setStatus({ type: 'error', message: 'Transaction rejected by user.' });
} else {
setStatus({
type: 'error',
message: `Error: ${err.message || 'Failed to send transaction'}`,
});
}
} finally {
setIsSubmitting(false);
}
};
return (
<div className="border border-pink-500 rounded-lg p-4 shadow-md bg-white text-pink-500 max-w-sm mx-auto space-y-4">
<h2 className="text-lg font-bold">Update Stored Number</h2>
{status.message && (
<div
className={`p-2 rounded-md break-words h-fit text-sm ${
status.type === 'error'
? 'bg-red-100 text-red-500'
: 'bg-green-100 text-green-700'
}`}
>
{status.message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="number"
placeholder="New Number"
value={newNumber}
onChange={(e) => setNewNumber(e.target.value)}
disabled={isSubmitting || !account}
className="w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-pink-400"
/>
<button
type="submit"
disabled={isSubmitting || !account}
className="w-full bg-pink-500 hover:bg-pink-600 text-white font-bold py-2 px-4 rounded-lg transition disabled:bg-gray-300"
>
{isSubmitting ? 'Updating...' : 'Update'}
</button>
</form>
{!account && (
<p className="text-sm text-gray-500">
Connect your wallet to update the stored number.
</p>
)}
</div>
);
};
export default WriteContract;
该组件允许用户输入一个新数字,并发送交易调用合约的 setNumber 函数来更新链上状态。当交易成功确认后,ReadContract 组件将在下一个轮询周期中自动显示最新的存储值。
集成所有组件
最后,更新 app/page.js,将所有组件组合在一起:
app/page.js
'use client';
import { useState } from 'react';
import WalletConnect from './components/WalletConnect';
import ReadContract from './components/ReadContract';
import WriteContract from './components/WriteContract';
export default function Home() {
const [account, setAccount] = useState(null);
const handleConnect = (connectedAccount) => {
setAccount(connectedAccount);
};
return (
<section className="min-h-screen bg-white text-black flex flex-col justify-center items-center gap-4 py-10">
<h1 className="text-2xl font-semibold text-center">
Ethers.js dApp - Passet Hub Smart Contracts
</h1>
<WalletConnect onConnect={handleConnect} />
<ReadContract />
<WriteContract account={account} />
</section>
);
}
最终完成的 UI 如下所示:

总结
恭喜你!你已经成功构建了一个完整的 dApp,使用 Ethers.js 与 Next.js,并与 Polkadot Hub 测试网上的智能合约进行了交互。
你的应用现在已经具备以下能力:
• 连接用户钱包
• 从智能合约中读取数据
• 发送交易并更新合约状态
这些基础能力为你在 Polkadot Hub 上构建更复杂的 dApp 打下了坚实的基础。你可以在此之上继续扩展,与更复杂的智能合约交互,并构建更加高级的用户界面。
获取完整示例代码
如果你希望直接使用一个可运行的完整示例,可以克隆官方仓库并进入对应目录:
git clone https://github.com/polkadot-developers/polkavm-storage-contract-dapps.git -b v0.0.2
cd polkavm-storage-contract-dapps/ethers-dapp
原文链接:https://docs.polkadot.com/tutorials/smart-contracts/launch-your-first-project/create-dapp-ethers-js/

1862

被折叠的 条评论
为什么被折叠?



