Skip to main content

你的第一个 Dapp

在本教程中,您将学习如何在 Aptos 区块链上构建 dapp。一个 dapp 通常由一个用 JavaScript 编写的用户界面组成,它与一个或多个 Move 模块交互。

在本教程中,我们将使用 Your First Move Module 中描述的 Move 模块 HelloBlockchain 并专注于构建用户界面。

我们将使用:

最终结果是一个允许用户在 Aptos 区块链上发布和共享文本片段的 dapp。

完整源代码

本教程的完整源代码正在更新。同时,较旧的可用 点击这里

先决条件

Aptos 钱包

在开始本教程之前,请安装 Aptos Wallet 扩展

安装后:

  1. 打开钱包,点击创建新钱包。然后点击 Create account 创建 Aptos 帐户。
  2. 复制私钥。在下一部分中,您将需要它来设置 Aptos CLI。
提示

通过点击 Faucet 按钮,确保您的账户有足够的资金来执行交易。

Aptos CLI

  1. 安装 Aptos CLI

  2. 运行 aptos init,当它询问您的私钥时,粘贴您之前复制的 Aptos 钱包中的私钥。这将初始化 Aptos CLI 以使用与 Aptos 钱包相同的帐户。

  3. 运行 aptos account list 来验证它是否正常工作。

第 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 为 dapps 提供了 Web3 API。 您可以通过打开浏览器控制台并运行await window.aptos.account()来查看它是如何工作的。 它将打印出与您在 Aptos 钱包中设置的帐户对应的地址。

接下来我们将更新我们的应用程序以使用此 API 来显示钱包帐户的地址。

等到 window.aptos 被定义

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 将是未定义)。

###(可选)window.aptos 的 TypeScript 设置

如果你使用 TypeScript,你可能还想通知编译器window.aptos API 的存在。 将以下内容添加到 src/index.tsx

declare global {
interface Window {
aptos: any;
}
}

这让我们可以使用 window.aptos API,而无需执行 (window as any).aptos

在应用程序中显示window.aptos.account()

我们的应用程序现在可以使用 window.aptos API。 我们将更改src/App.tsx以在初始渲染时检索window.aptos.account()(钱包帐户)的值,将其存储在状态中,然后显示它:

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 {
border: 0;
min-height: 50vh;
outline: 0;
padding: 0;
width: 100%;
}

第 3 步:使用 SDK 从区块链获取数据

钱包现已与我们的 dapp 集成。 接下来,我们将集成 Aptos SDK 从区块链中获取数据。 我们将使用 Aptos SDK 检索有关我们帐户的信息并在页面上显示该信息。

aptos 依赖添加到 package.json

首先,将 SDK 添加到项目的依赖项中:

npm install --save aptos

您现在将在您的 package.json 中看到 "aptos": "^0.0.20"(或类似的)。

创建一个AptosClient

现在我们可以导入 SDK 并创建一个 AptosClient 来与区块链交互(技术上它与 REST API 交互 ,然后与区块链交互)。

由于我们的钱包账户在 devnet 上,我们将设置 AptosClient 来与 devnet 交互。 将以下内容添加到 src/App.tsx

import { Types, AptosClient } from "aptos";

// 创建一个 AptosClient 以与 devnet 交互
const client = new AptosClient("https://fullnode.devnet.aptoslabs.com/v1");

function App() {
// ...

// 使用 AptosClient 检索有关帐户的详细信息。
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}</code>
</p>
<p>
<code>{account?.sequence_number}</code>
</p>
</div>
);
}

现在,除了显示账户地址,app 还会显示账户的 sequence_number。这个 sequence_number 代表下一个事务的序号,以防止事务的可重入攻击。当您使用该帐户进行交易时,您会看到这个数字在增加。

第 4 步:发布 Move 模块

我们的 dapp 现在设置为从区块链中读取。下一步是写入区块链。为此,我们将向我们的帐户发布一个 Move 模块。

Move模块提供了存储此数据的位置。具体来说,我们将使用 Your First Move Module 中的 HelloBlockchain 模块,它提供了一个名为 MessageHolder 的资源,其中包含一个字符串(称为 message)。

使用 Aptos CLI 发布 HelloBlockchain 模块

我们将使用 Aptos CLI 编译和发布 HelloBlockchain 模块。

  1. 下载hello_blockchain

  2. 接下来,使用 aptos move publish 命令(替换 /path/to/hello_blockchain/<address>):

aptos move publish --package-dir /path/to/hello_blockchain/ --named-addresses HelloBlockchain=<address>

例如:

aptos move publish --package-dir ~/code/aptos-core/aptos-move/move-examples/hello_blockchain/ --named-addresses HelloBlockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481

--named-addressesHelloBlockchain.move 中的命名地址 HelloBlockchain 替换为指定的地址。 例如,如果我们指定 --named-addresses HelloBlockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481,则如下:

module HelloBlockchain::message {

变成:

module 0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::message {

这使得给指定帐户发布模块成为可能(在本例中是我们的钱包帐户,0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481)。

假设您的账户有足够的资金来执行交易,您现在可以在您的账户中发布 HelloBlockchain 模块。 如果您刷新应用程序,您会看到帐号序列号已从 0 增加到 1。

您还可以通过转到 Aptos Explorer 并查找您的帐户来验证该模块是否已发布。 如果您向下滚动到 Account Modules 部分,您应该会看到如下内容:

{
"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

为了方便用户,如果模块不存在,我们可以让应用显示 aptos move publish 命令。 为此,我们将使用 Aptos SDK 检索帐户模块并查找 module.abi.name 等于 "Message" 的模块(即,我们在 Aptos 资源管理器)。

更新src/App.tsx

function App() {
// ...

// 检查模块; 如果不存在,则显示发布说明。
const [modules, setModules] = React.useState<Types.MoveModule[]>([]);
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 HelloBlockchain={address}
</pre>
);

return <div className="App">{!hasModule && publishInstructions}</div>;
}

新用户将能够使用此命令为其帐户创建页面。

第五步:在区块链上写一条消息

现在模块已经发布,我们可以使用它在区块链上写消息了。 对于这一步,我们将使用模块公开的 set_message 函数。

调用 set_message 函数的事务

set_message 的签名如下所示:

public(script) fun set_message(account: signer, message_bytes: vector<u8>)

要调用这个函数,我们需要使用钱包提供的window.aptos API 来提交交易。 具体来说,我们将创建一个如下所示的 entry_function_payload 事务:

{
type: "entry_function_payload",
function: "<address>::message::set_message",
arguments: ["<hex encoded utf-8 message>"],
type_arguments: []
}

无需提供 account:signer 参数。 Aptos 自动提供它。

但是,我们确实需要指定 message_bytes 参数:这是交易中的 "<hex encoded utf-8 message>"。 我们需要一种将 JS 字符串转换为这种格式的方法。 我们可以通过使用 TextEncoder 转换为 utf-8 字节,然后使用单线对字节进行十六进制编码来做到这一点。

将此函数添加到 src/App.tsx 中:

/** 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("");
}

使用这个函数,我们的交易 payload 变成:

{
type: "entry_function_payload",
function: "<address>::message::set_message",
arguments: [stringToHex(message)],
type_arguments: []
}

使用 window.aptos API 提交 set_message 交易

现在我们了解了如何使用事务来调用set_message函数,接下来我们使用window.aptos.signAndSubmitTransaction()从我们的应用程序中调用这个函数。

我们将添加:

  • 一个<textarea\>,用户可以在其中输入消息,以及
  • 使用 <textarea> 的内容调用 set_message 函数的 <button>

更新src/App.tsx

function App() {
// ...

// 在提交时使用 textarea 值调用 set_message
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: [stringToHex(message)],
type_arguments: [],
};

try {
setIsSaving(true);
await window.aptos.signAndSubmitTransaction(transaction);
} finally {
setIsSaving(false);
}
};

return (
<div className="App">
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} />
<input disabled={isSaving} type="submit" />
</form>
) : (
publishInstructions
)}
</div>
);
}

要测试它:

  • <textarea> 中输入内容并提交表单。
  • Aptos Explorer 中找到您的帐户,您现在将在帐户资源下看到一个带有您编写的 messageMessageHolder 资源。

如果您没有看到它,请尝试使用较短的消息。长消息可能会导致交易失败,因为更长的消息需要更多的 Gas 。

步骤 6:在 dapp 中显示消息

现在已经创建了 MessageHolder 资源,我们可以使用 Aptos SDK 来检索它并显示消息。

获取钱包账号的消息

要检索消息,我们将:

  • 首先使用 AptosClient.getAccountResources() 函数来获取帐户的资源并将它们存储在状态中。

  • 然后我们将寻找一个typeMessageHolder的。完整类型是 $address::message::MessageHolder,因为它是 $address::message 模块的一部分。

    在我们的示例中是:

     0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::message::MessageHolder
  • 我们将使用它作为 <textarea> 的初始值。

更新src/App.tsx

function App() {
// ...

// Get the message from account resources.
const [resources, setResources] = React.useState<Types.AccountResource[]>([]);
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;

return (
// ...
<textarea ref={ref} defaultValue={message} />
// ...
);
}

要测试它:

  • 刷新页面,你会看到你之前写的消息。
  • 更改文本,提交表单,然后再次刷新页面。 您将看到内容已更新为您的新消息。

这将确认您正在 Aptos 区块链上读写消息。

显示来自其他帐户的消息

到目前为止,我们已经构建了一个单人dapp,您可以在其中使用自己的帐户读取和写入消息。 接下来,我们将让其他人能够阅读消息,包括没有安装 Aptos 钱包的人。

我们将对其进行设置,以便转到 URL /<account address> 显示存储在 <account address> 中的消息(如果存在)。

  • 如果应用程序在/<account address>加载,我们也将禁用编辑。

  • 如果启用编辑,我们将显示获取公共 URL链接,以便您分享您的信息。

更新src/App.tsx

function App() {
// Retrieve aptos.account on initial render and store it.
const urlAddress = window.location.pathname.slice(1);
const isEditable = !urlAddress;
const [address, setAddress] = React.useState<string | null>(null);
React.useEffect(() => {
if (urlAddress) {
setAddress(urlAddress);
} else {
window.aptos.account().then((data: { address: string }) => setAddress(data.address));
}
}, [urlAddress]);

// ...

return (
<div className="App">
{hasModule ? (
<form onSubmit={handleSubmit}>
<textarea ref={ref} defaultValue={message} readOnly={!isEditable} />
{isEditable && <input disabled={isSaving} type="submit" />}
{isEditable && <a href={address!}>Get public URL</a>}
</form>
) : (
publishInstructions
)}
</div>
);
}

本教程到此结束.