在本教程中,你将学习如何在 Aptos 区块链上建立一个 DApp。一个 DApp 通常由一个用 JavaScript 编写的用户界面组成,它与一个或多个 Move 模块进行交互。
我们将使用你的第一个 Move 模块中使用的 Move 模块 HelloBlockchain
,并着重于构建用户界面。
我们将使用以下工具:
最终的结果是一个让用户在 Aptos 区块链上发布和分享文本片段的 DApp。
本教程的完整源代码可在此获得。
前置准备
Aptos 钱包
在开始教程之前,我们应当先安装 Aptos 钱包扩展程序。
在你安装完钱包之后
打开钱包并且点击 Create a new wallet,然后点击 Create account 来创建一个 Aptos 的钱包账户。
复制私钥,你将会在后续章节中的用到它来设置 Aptos CLI
💡 注意
点击 Faucet 按钮,确保你的账户有足够的资金进行交易。
Aptos CLI
运行 aptos init
命令,当屏幕提示你输入私钥时,把之前你从钱包复制过来的私钥粘贴进去。这将让 Aptos CLI 程序以与你钱包的相同的账户初始化。
运行 aptos account list
命令来确认 Aptos CLI 是正常运行的
💡 译者注:
截至翻译完成时期,Aptos 代码仍在高强度更新中,可能会出现不兼容的情况。因此,请使用最新版本的 Aptos CLI,来进行以下的操作。本次使用的 Aptos CLI 版本为 0.3.2,你可以使用 aptos --version
来查看自身的 Aptos CLI 版本。
第 1 步:初始化一个单页应用
我们现在将为我们的 DApp 设置前端的用户界面。在本教程中,我们将使用create-react-app
来初始化应用,但 React 和 create-react-app
都不是必需的。你可以选择使用你喜欢的 JavaScript 框架。
$ npx create-react-app first-dapp --template typescript
$ cd first-dapp
$ npm start
至此我们将有一个可以在浏览器中运行的基础 React 应用。
第 2 步:集成 Aptos Wallet Web3 API
Aptos 钱包通过 window.aptos
接口为 dApp 提供了一个 Web3 API。你可以在第一步中由 npm start
打开的浏览器 console 中运行 await window.aptos.account()
来了解它是如何工作的(注意一定要是通过 npm start
打开的浏览器来调用 window.aptos
接口)。它将显示出与你在 Aptos 钱包中设置的帐户相对应的地址,如下图所示:
接下来,我们将更新我们的应用程序,使用这个 API 来显示钱包帐户的地址。
等待 window.aptos 接口被注入到 window DOM 下
在浏览器中集成 window.aptos
API 的第一步是在 window.onload
事件触发之后,才开始渲染你的前端界面。
打开 src/index.tsx
,更改以下代码片段:
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
为
window.addEventListener('load', () => {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
这一更改将确保在我们渲染前端元素时 window.aptos
API 已经初始化(如果我们渲染得太早,钱包扩展可能还没有机会初始化 API,因此 window.aptos
将变成 undefiend
)。
(可选)TypeScript 语言下 window.aptos
的设置
如果你使用的是 TypeScript 语言,你可能还需要通知编译器 window.aptos
API 的存在。将以下代码添加到 src/index.tsx
中:
declare global {
interface Window { aptos: any; }
}
这将使我们不再依赖 (window as any).aptos
命令,而直接可以调用 window.aptos
API
在应用中展示 window.aptos.account()
结果
我们的应用现在已经可以调用 window.aptos
API,我们将改动 src/App.tsx
的代码从而在初始化渲染时获取到window.aptos.account()
(你的钱包账户)的值,存储到 React state 下,然后把它展示在前端,代码改动如下:
import React from 'react';
import './App.css';
function App() {
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
}, []);
return (
<div className="App">
<p><code>{ address }</code></p>
</div>
);
}
export default App;
刷新页面即可看到您的帐户地址。
增加 CSS 样式
接下来,替换 src/App.css
代码
a, input, textarea {
display: block;
}
textarea {
margin: 4px;
padding: 8px;
border: solid 1px #333;
width: 80%;
}
我们将在测试页面看到自己的 Aptos 钱包地址:
第 3 步:通过 SDK 获取链上数据
现在我们的应用已经集成了钱包的能力,下一步,我们将集成链上数据来获取链上数据。我们将使用 Aptos SDK 来获取我们账户的链上信息并且将其展示在前端页面上
在 package.json
中增加 aptos
依赖
首先,在工程依赖中增加 Aptos SDK 相关依赖:
$ npm install --save aptos
你将会在 package.json
中看到 "aptos": "^0.0.20"
(或者类似的依赖信息)。
创建一个 AptosClient
现在我们可以引入 SDK 并且创建一个可以与 Aptos 区块链交互的 AptosClient
客户端 (准确来说是与 Aptos REST API 交互,这才是直接与 Aptos 区块链交互的基础设施)
只要我们的钱包在测试网(devnet)下,我们就可以搭建一个 AptosClient
来与测试网环境交互,在 src/App.tsx
文件下,增加以下代码:
import React from 'react';
import './App.css';
import { Types, AptosClient } from 'aptos';
// Create an AptosClient to interact with devnet.
const client = new AptosClient('<https://fullnode.devnet.aptoslabs.com/v1>');
function App() {
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
}, []);
// Use the AptosClient to retrieve details about the account.
const [account, setAccount] = React.useState<Types.AccountData | null>(null);
React.useEffect(() => {
if (!address) return;
client.getAccount(address).then(setAccount);
}, [address]);
return (
<div className="App">
<p><code>address: { address }</code></p>
<p><code>sequence: { account?.sequence_number }</code></p>
</div>
);
}
export default App;
现在,前端界面除了展示账户地址,还会展示账户的 sequence_number
。该 sequence_number
代表了下一笔交易的 sequence_number
,以用于避免重放攻击。随着你通过该账户执行的交易的增加,该 sequence_number
也会随之增加。
在这一步结束后,你将得到以下的页面:
第 4 步:发布一个 Move 模块
我们的 DApp 现在已经可以读取链上数据,下一步则是在链上写入数据。为了完成这个目标,我们将在自己的账户地址上,发布一个 Move 模块。
Move 模块提供了(我们将要写入的数据)存储的空间。具体来说,我们将使用你的第一个 Move 模块中的 hello_blockchain
模块,它提供了一个名为 MessageHolder
的资源,该资源持有一个字符串(称为 message
)。
使用 Aptos CLI 发布 hello_blockchain
模块
我们将使用 Aptos CLI 来编译并发布 hello_blockchain
模块。
💡 译者注:
截至翻译完成时期,Aptos 代码仍在高强度更新中,可能会出现不兼容的情况。因此,请使用最新版本的 Aptos CLI 和 Petra 钱包,来进行以下的操作。本次使用的 Aptos CLI 版本为 0.3.2,交互钱包为 Petra 0.1.8 你可以使用 aptos --version
来查看自身的 Aptos CLI 版本。
接着(在终端)调用 aptos move publish
命令(记得把 /path/to/hello_blockchain/
替换成你的下载路径,并且将 <address>
替换为你的 Aptos 账户地址;同时以最新版本的 move-examples/hello_blockchain/sources/hello_blockchain.move
下的命名地址为准,比如 Aptos CLI 0.3.2 版本下的命名地址即为 hello_blockchain
)
$ aptos move publish --package-dir /path/to/hello_blockchain/ --named-addresses hello_blockchain=<address>
比如:
$ aptos move publish --package-dir ~/code/aptos-core/aptos-move/move-examples/hello_blockchain/ --named-addresses HelloBlockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481
—name-addresses
将会把 hello_blockchain.move
中的命名地址 hello_blockchain
替换为用户在命令行中指定的地址。举个例子,如果我们指定 -named-addresses hello_blockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481
,那么以下代码:
module hello_blockchain::Message {
将等价为:
module 0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message {
这使得它可以在指定的账户地址上(在这种情况下是我们的钱包账户地址)发布模块。
假设你的账户有足够的资金来执行交易,你现在可以在你的账户中发布 HelloBlockchain
模块。如果你刷新你的 DApp 前端页面,你会看到账户的序列号已经从 0 增加到 1。
你也可以通过进入 Aptos Explorer 查找你的账户来验证该模块是否已发布。如果你向下滚动到账户模块部分,你应该看到类似以下内容。
{
"address": "0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481",
"name": "Message",
"friends": [],
"exposedFunctions": [
{
"name": "get_message",
"visibility": "public",
"genericTypeParams": [],
"params": [
"address"
],
"_return": [
"0x1::string::String"
]
},
{
"name": "set_message",
"visibility": "script",
"genericTypeParams": [],
"params": [
"signer",
"vector"
],
"_return": []
}
],
"structs": [
{
"name": "MessageChangeEvent",
"isNative": false,
"abilities": [
"drop",
"store"
],
"genericTypeParams": [],
"fields": [
{
"name": "from_message",
"type": "0x1::string::String"
},
{
"name": "to_message",
"type": "0x1::string::String"
}
]
},
{
"name": "MessageHolder",
"isNative": false,
"abilities": [
"key"
],
"genericTypeParams": [],
"fields": [
{
"name": "message",
"type": "0x1::string::String"
},
{
"name": "message_change_events",
"type": "0x1::event::EventHandle<0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message::MessageChangeEvent>"
}
]
}
]
}
记下 "name”: "message",我们将在下一节使用它。
在 DApp 中增加模块发布命令
为了方便用户,如果模块不存在,我们可以在 DApp 界面上显示 aptos move publish
命令。为此,我们将使用 Aptos SDK 来检索账户模块,并寻找其中 [module.abi.name](<http://module.abi.name>)
等于 "Message "的模块(即我们在 Aptos Explorer 中看到的 "name": "message"
)。
将以下代码更新到 src/App.tsx
中:
import React from 'react';
import './App.css';
import { Types, AptosClient } from 'aptos';
// Create an AptosClient to interact with devnet.
const client = new AptosClient('<https://fullnode.devnet.aptoslabs.com/v1>');
/** Convert string to hex-encoded utf-8 bytes. */
function stringToHex(text: string) {
const encoder = new TextEncoder();
const encoded = encoder.encode(text);
return Array.from(encoded, (i) => i.toString(16).padStart(2, "0")).join("");
}
function App() {
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
}, []);
// Use the AptosClient to retrieve details about the account.
const [account, setAccount] = React.useState<Types.AccountData | null>(null);
React.useEffect(() => {
if (!address) return;
client.getAccount(address).then(setAccount);
}, [address]);
// Check for the module; show publish instructions if not present.
const [modules, setModules] = React.useState<Types.MoveModuleBytecode[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccountModules(address).then(setModules);
}, [address]);
const hasModule = modules.some((m) => m.abi?.name === 'message');
const publishInstructions = (
<pre>
Run this command to publish the module:
<br />
aptos move publish --package-dir /path/to/hello_blockchain/
--named-addresses hello_blockchain={address}
</pre>
);
const hasModuleText = (
<pre>
you have a module called "message" defined!
</pre>
)
return (
<div className="App">
<p><code>address: { address }</code></p>
<p><code>sequence: { account?.sequence_number }</code></p>
{hasModule ? (
hasModuleText
) : publishInstructions}
</div>
);
}
export default App;
此时,如果你的钱包地址下已经发布了名称为 message
的模块,你将会看到:
如果没有发布过对应名称的模块,则会提醒你使用以下命令去发布一个 Move 模块:
第 5 步:在链上写入消息
现在我们的 Move 模块已经发布到链上,我们已经可以在链上写入数据了!在这一步我们将使用模块中暴露的 set_message
函数来在链上写入一条消息
一笔调用了 set_message
函数的交易
set_message
的签名如下所示:
public entry fun set_message(account: signer, message: string::String)
acquires MessageHolder
为了调用这个函数,我们需要使用钱包提供的 window.aptos
API来提交一个交易。具体来说,我们将创建一个 entry_function_payload
交易,看起来像这样。
{
type: "entry_function_payload",
function: "<address>::Message::set_message",
arguments: ["message>"],
type_arguments: []
}
不需要提供 account: signer
参数。Aptos 钱包会自动提供它。
使用 window.aptos
API 提交 set_message
交易
现在我们明白了如何使用交易来调用 set_message
函数,接下来我们使用window.aptos.signAndSubmitTransaction()
从我们的 DApp 中调用这个函数。
我们将增加如下功能:
一个 <button>
,用 `<textarea>的内容调用set_message函数。
💡 译者注:
后续的逻辑涉及到 signAndSubmitTransaction
操作,Petra 钱包做了权限的控制,因此,你必须要在 window.aptos
注入完成后,调用 window.aptos.connect()
,才能完成后续的操作。
更新 src/App.tsx
的代码:
import React from 'react';
import './App.css';
import { Types, AptosClient } from 'aptos';
// Create an AptosClient to interact with devnet.
const client = new AptosClient('<https://fullnode.devnet.aptoslabs.com/v1>');
const connectToWallet = async() =>{
const status = await(window as any).aptos.isConnected();
if (!status){
await window.aptos.connect();
}
}
function App() {
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
connectToWallet().then(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
})
}, []);
// Use the AptosClient to retrieve details about the account.
const [account, setAccount] = React.useState<Types.AccountData | null>(null);
// Check for the module; show publish instructions if not present.
const [modules, setModules] = React.useState<Types.MoveModuleBytecode[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccount(address).then(setAccount);
client.getAccountModules(address).then(setModules);
}, [address]);
const hasModule = modules.some((m) => m.abi?.name === 'message');
const publishInstructions = (
<pre>
Run this command to publish the module:
<br />
aptos move publish --package-dir /path/to/hello_blockchain/
--named-addresses hello_blockchain={address}
</pre>
);
// Call set_message with the textarea value on submit.
const ref = React.createRef<HTMLTextAreaElement>();
const [isSaving, setIsSaving] = React.useState(false);
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!ref.current) return;
const message = ref.current.value;
const transaction = {
type: "entry_function_payload",
function: `${address}::message::set_message`,
arguments: [message],
type_arguments: [],
};
try {
setIsSaving(true);
await window.aptos.signAndSubmitTransaction(transaction);
} finally {
setIsSaving(false);
}
};
return (
<div className="App">
<p><code>address: { address }</code></p>
<p><code>sequence: { account?.sequence_number }</code></p>
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} />
<input disabled={isSaving} type="submit" />
</form>
) : publishInstructions}
</div>
);
}
export default App;
运行你的 DApp 之后,你将看到以下的界面:
测试你的 DApp:
在 <textarea>
中输入一些信息并且点击 提交
按钮提交该表单
在 Aptos Explorer 中搜索你的账户地址,现在你会在账户资源下看到一个 MessageHolder 资源,上面有你刚刚提交的信息。
如果你没有看到,请尝试一个较短的信息。长信息可能会导致交易失败,因为长信息需要消耗更多的 gas 费用。
第 6 步:在 DApp 中展示刚刚设置的信息
现在我们已经创建了 MessageHolder
资源,可以使用 Aptos SDK 来获取它并显示消息。
获取钱包地址下的信息
为了获取这条信息,我们将:
首先使用 AptosClient.getAccountResources()
函数获取该地址下的资源,并且将它缓存在前端的 state 中
接下来我们要寻找 type
为 MessageHolder
的资源,该类型的完整路径地址为 $address::message::MessageHolder
,是 $address::message
模块的一部分,在我们的例子中,完整的路径地址为:
0xaedd72868bc24fcc42eb481b874a1aadd0a648779aaacb69498b69c5c10fc47a::message::MessageHolder
我们将使用这个地址下的现值作为 DApp 的 <textarea>
的默认缺省值
将 src/App.tsx
代码更新为:
import React from 'react';
import './App.css';
import { Types, AptosClient } from 'aptos';
// Create an AptosClient to interact with devnet.
const client = new AptosClient('<https://fullnode.devnet.aptoslabs.com/v1>');
const connectToWallet = async() =>{
const status = await(window as any).aptos.isConnected();
if (!status){
await window.aptos.connect();
}
}
function App() {
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
connectToWallet().then(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
})
}, []);
// Use the AptosClient to retrieve details about the account.
const [account, setAccount] = React.useState<Types.AccountData | null>(null);
// Check for the module; show publish instructions if not present.
const [modules, setModules] = React.useState<Types.MoveModuleBytecode[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccount(address).then(setAccount);
client.getAccountModules(address).then(setModules);
}, [address]);
const hasModule = modules.some((m) => m.abi?.name === 'message');
const publishInstructions = (
<pre>
Run this command to publish the module:
<br />
aptos move publish --package-dir /path/to/hello_blockchain/
--named-addresses hello_blockchain={address}
</pre>
);
const [resources, setResources] = React.useState<Types.MoveResource[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccountResources(address).then(setResources);
}, [address]);
const resourceType = `${address}::message::MessageHolder`;
const resource = resources.find((r) => r.type === resourceType);
const data = resource?.data as {message: string} | undefined;
const message = data?.message;
// Call set_message with the textarea value on submit.
const ref = React.createRef<HTMLTextAreaElement>();
const [isSaving, setIsSaving] = React.useState(false);
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!ref.current) return;
const message = ref.current.value;
const transaction = {
type: "entry_function_payload",
function: `${address}::message::set_message`,
arguments: [message],
type_arguments: [],
};
try {
setIsSaving(true);
await window.aptos.signAndSubmitTransaction(transaction);
} finally {
setIsSaving(false);
}
};
return (
<div className="App">
<p><code>address: { address }</code></p>
<p><code>sequence: { account?.sequence_number }</code></p>
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} defaultValue={message} />
<input disabled={isSaving} type="submit" />
</form>
) : publishInstructions}
</div>
);
}
export default App;
为了验证我们的代码功能:
刷新 DApp 前端页面,我们将会看到我们之前写入的信息被填进 <textarea>
中
更新 <textarea>
中的文字,并且提交表单,最后再刷新一下页面。你将看到 <textarea>
更新成了你刚刚提交的新信息
这也验证了你正在读取/写入 Aptos 区块链上的信息。
展示其他账户上存储的信息
到目前为止,我们已经建立了一个 "只有一个用户的" DApp,你可以在你自己的账户上阅读和编写信息。接下来,我们将使其他人也能阅读信息,包括没有安装 Aptos 钱包的人。
我们将对其进行设置,以便进入地址尾缀为 /<账户地址>
的页面显示存储在 <账户地址>
下的信息(如果它存在的话)。
如果应用程序在 /<账户地址>
处加载,我们也将禁止编辑。
如果编辑功能被启用,我们将显示一个"获取公共URL"的链接,以便你可以分享你的信息。
更新 src/App.tsx
:
import React from 'react';
import './App.css';
import { Types, AptosClient } from 'aptos';
// Create an AptosClient to interact with devnet.
const client = new AptosClient('<https://fullnode.devnet.aptoslabs.com/v1>');
const connectToWallet = async() =>{
const status = await(window as any).aptos.isConnected();
if (!status){
await window.aptos.connect();
}
}
function App() {
// Retrieve address first on URL
const urlAddress = window.location.pathname.slice(1);
const isEditable = !urlAddress;
// Retrieve aptos.account on initial render and store it.
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
if (urlAddress) {
setAddress(urlAddress)
} else {
connectToWallet().then(() => {
window.aptos.account().then((data : {address: string}) => setAddress(data.address));
})
}
}, [urlAddress]);
// Use the AptosClient to retrieve details about the account.
const [account, setAccount] = React.useState<Types.AccountData | null>(null);
// Check for the module; show publish instructions if not present.
const [modules, setModules] = React.useState<Types.MoveModuleBytecode[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccount(address).then(setAccount);
client.getAccountModules(address).then(setModules);
}, [address]);
const hasModule = modules.some((m) => m.abi?.name === 'message');
const publishInstructions = (
<pre>
Run this command to publish the module:
<br />
aptos move publish --package-dir /path/to/hello_blockchain/
--named-addresses hello_blockchain={address}
</pre>
);
const [resources, setResources] = React.useState<Types.MoveResource[]>([]);
React.useEffect(() => {
if (!address) return;
client.getAccountResources(address).then(setResources);
}, [address]);
const resourceType = `${address}::message::MessageHolder`;
const resource = resources.find((r) => r.type === resourceType);
const data = resource?.data as {message: string} | undefined;
const message = data?.message;
// Call set_message with the textarea value on submit.
const ref = React.createRef<HTMLTextAreaElement>();
const [isSaving, setIsSaving] = React.useState(false);
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!ref.current) return;
const message = ref.current.value;
const transaction = {
type: "entry_function_payload",
function: `${address}::message::set_message`,
arguments: [message],
type_arguments: [],
};
try {
setIsSaving(true);
await window.aptos.signAndSubmitTransaction(transaction);
} finally {
setIsSaving(false);
}
};
return (
<div className="App">
<p><code>address: { address }</code></p>
<p><code>sequence: { account?.sequence_number }</code></p>
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} defaultValue={message} readOnly={!isEditable} disabled={!isEditable}/>
{isEditable && (<input disabled={isSaving} type="submit" />)}
{isEditable && (<a href={address!}>Get public URL</a>)}
</form>
) : publishInstructions}
</div>
);
}
export default App;
我们可以看到对于加了账户地址后缀的 url,进入时 <textarea>
被禁用,且展示了当前账户地址下的 message 信息:
而没有加账户地址后缀时,则依然可以输入信息,提交表单,修改链上当前钱包账户地址下的数据。
至此,我们的 DApp 内容结束 🎉