你的第一个 DApp

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

我们将使用你的第一个 Move 模块中使用的 Move 模块 HelloBlockchain,并着重于构建用户界面。

我们将使用以下工具:

最终的结果是一个让用户在 Aptos 区块链上发布和分享文本片段的 DApp。

本教程的完整源代码可在此获得

前置准备

Aptos 钱包

在开始教程之前,我们应当先安装 Aptos 钱包扩展程序。

在你安装完钱包之后

  1. 打开钱包并且点击 Create a new wallet,然后点击 Create account 来创建一个 Aptos 的钱包账户。

  2. 复制私钥,你将会在后续章节中的用到它来设置 Aptos CLI

💡 注意

点击 Faucet 按钮,确保你的账户有足够的资金进行交易。

Aptos CLI

  1. 安装 Aptos CLI.

  2. 运行 aptos init 命令,当屏幕提示你输入私钥时,把之前你从钱包复制过来的私钥粘贴进去。这将让 Aptos CLI 程序以与你钱包的相同的账户初始化。

  3. 运行 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 版本。

  1. 接着(在终端)调用 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 中调用这个函数。

我们将增加如下功能:

  • 一个可以输入信息详情的 <textarea>

  • 一个 <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 中

  • 接下来我们要寻找 typeMessageHolder 的资源,该类型的完整路径地址为 $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 内容结束 🎉

Last updated