> For the complete documentation index, see [llms.txt](https://mastering-uos.gitbook.io/mastering-uos/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mastering-uos.gitbook.io/mastering-uos/di-jiu-zhang-he-yue-kai-fa-jiao-cheng.md).

# 第九章 合约开发教程

## 6.1 环境部署

### 6.1.1 简介

本系列教程非常适合从开发人员的角度学习UOSIO的细节。

**您将学习什么**

* 如何快速启动节点
* 管理钱包和钥匙
* 创建账户
* 编写智能合约
* 合约编译及ABI
* 部署合约

**开发经验**

* C / C++ 开发经验

基于UOSIO的区块链使用WebAssembly (WASM)执行用户生成的应用程序和代码。WASM是一个新兴的web标准，得到了Google、Microsoft、Apple等行业领先公司的广泛支持。

目前，编译WASM的应用程序最成熟的工具链是clang/llvm及其C/ C++编译器。为获得最佳的兼容性，建议您使用UOSIO C++工具链。

第三方开发的其他工具包括:Rust、Python和solid。虽然这些语言可能看起来更简单，但是它们的性能可能会影响您可以编译的应用程序的规模。我们预估C++将是开发高性能和安全智能合约的最佳语言，并计划在可预见的未来使用C++。

* Linux开发经验

UOSIO支持环境：Ubuntu 16.04 及以上

* 命令行知识

UOSIO提供了各种工具，这些工具要求您具备基本的命令行知识，以便与之进行交互。

**C++环境设置**

我们可以使用任何文本编辑器，最好支持C++语法高亮显示。流行的编辑器有Sublime Text和Atom。另一个选项是IDE，它提供更复杂的代码完成和更完整的开发体验。欢迎您使用您个人喜欢的软件，如果您不确定使用什么，我们提供了一些选项供您选择。

可用的编辑器和IDE：

* [Sublime Text](https://www.sublimetext.com/)
* [Atom Editor](https://atom.io/)
* [CLion](https://www.jetbrains.com/clion/)
* [Eclipse](http://www.eclipse.org/downloads/packages/release/oxygen/1a/eclipse-ide-cc-developers)
* [Visual Studio Code](https://code.visualstudio.com/)

**开发环境的操作系统**

如果您使用linux的操作系统，包括但不限于Ubuntu，您就可以轻松地开始开发。

如果您在Windows上进行开发，很遗憾我们目前没有提供powershell端口和指令。将来我们可能会附加powershell命令。与此同时，建议您使用Ubuntu系统的虚拟机，并在这个虚拟机中设置您的开发环境。如果您是一个熟悉移植Linux指令的高级Windows开发人员，您遇到的问题会少很多。

### 6.1.2 准备

**第一步: 安装二进制文件**

本文档使用预编译二进制文件。为了让您尽快开始，这是最好的选择。

以下命令将下载该二进制文件。暂时仅限Ubuntu 16.04。

提示：如果您的系统上已经安装了以前版本的uosio，请卸载之后再继续。

(此处是测试链的安装包，主链安装包为**UOS\_Mainnet.tar.gz**)

{% code title="shell" %}

```bash
wget ftp://tools.ulord.one/UOS_Testnet.tar.gz
```

{% endcode %}

下载完成后，解压并安装：

{% code title="shell" %}

```bash
tar -xzvf UOS_Testnet.tar.gz
cd uos
sudo ./install.sh
```

{% endcode %}

**第二步: 新建并进入合约开发目录**

您需要选择一个目录来工作，建议在本地驱动器的某个地方创建一个合约目录。

{% code title="shell" %}

```bash
mkdir contracts
cd contracts
```

{% endcode %}

**第三步: 获取并保存该目录**

获取该目录的路径并保存，以便后续使用，您可以使用以下命令获取绝对路径。

{% code title="shell" %}

```bash
pwd
```

{% endcode %}

### 6.1.3 安装CDT

UOSIO合约开发工具包(简称CDT)，是合约编译相关工具的集合。后续教程主要使用CDT编译合约和生成ABI。

**提示**：如果您已经安装之前的uosio.cdt版本。请卸载之后再继续。

**Ubuntu**

下载

{% code title="shell" %}

```bash
wget ftp://tools.ulord.one/uosio.cdt.tar.gz
```

{% endcode %}

解压

{% code title="shell" %}

```bash
tar -xzvf uosio.cdt.tar.gz
```

{% endcode %}

安装

{% code title="shell" %}

```bash
cd uosio.cdt
sudo ./install.sh
```

{% endcode %}

以上命令需要sudo权限，因为uosio.cdt将在本地安装各种二进制文件。系统将要求您输入计算机的帐户密码。

安装uosio.cdt将使编译后的二进制文件全局化，以便在任何地方访问它。对于本文档，**强烈建议您不要跳过uosio.cdt的安装步骤**，如果不安装将使您很难遵循此文档和其他文档，并且通常更难进行合约编译。

### 6.1.4 启动节点和设置

说明：为简化操作，以下节点仅是简单的本地环境部署，并没有加载系统合约。

**第一步: 启动节点和钱包**

**步骤1.1: 启动kuosd**

{% code title="shell" %}

```bash
kuosd &
```

{% endcode %}

您将看到如下的输出:

```
info  2018-11-27T06:54:24.789 thread-0  wallet_plugin.cpp:42          plugin_initialize    ] initializing wallet plugin
info  2018-11-27T06:54:24.795 thread-0  http_plugin.cpp:554           add_handler          ] add api url: /v1/kuosd/stop
info  2018-11-27T06:54:24.796 thread-0  wallet_api_plugin.cpp:73      plugin_startup       ] starting wallet_api_plugin
info  2018-11-27T06:54:24.796 thread-0  http_plugin.cpp:554           add_handler          ] add api url: /v1/wallet/create
info  2018-11-27T06:54:24.796 thread-0  http_plugin.cpp:554           add_handler          ] add api url: /v1/wallet/create_key
info  2018-11-27T06:54:24.796 thread-0  http_plugin.cpp:554           add_handler          ] add api url: /v1/wallet/get_public_keys
```

按回车键退出。

**步骤1.2: 启动noduos**

{% code title="shell" %}

```bash
noduos -d ~/uosdata --config-dir ~/uos/cfg --genesis-json ~/uos/cfg/genesis.json
 --contracts-console -filter-on=‘*’ >> noduos.log 2>&1 &
```

{% endcode %}

这些设置实现以下功能：

1. 在开发目录下的uosio目录中为区块链数据和配置分配工作目录。这里我们分别使用\~/uos/data和\~/uos/cfg。
2. 运行noduos，同时该命令加载所有基本插件。

**第二步: 检查安装**

**步骤 2.1: 检查节点是否正常运行**

运行以下命令

{% code title="shell" %}

```bash
tailf noduos.log
```

{% endcode %}

您应该会在控制台中看到如下的输出:

```
info  2018-12-01T08:19:56.000 thread-0  producer_plugin.cpp:1490      produce_block        ] Produced block 000001a484763d72... #420 @ 2018-12-06T08:19:56.000 signed by uosio [trxs: 0, lib: 419, confirmed: 0]
info  2018-12-01T08:19:57.001 thread-0  producer_plugin.cpp:1490      produce_block        ] Produced block 000001a581e0e64f... #421 @ 2018-12-06T08:19:57.000 signed by uosio [trxs: 0, lib: 420, confirmed: 0]
info  2018-12-01T08:19:58.000 thread-0  producer_plugin.cpp:1490      produce_block        ] Produced block 000001a6c001325e... #422 @ 2018-12-06T08:19:58.000 signed by uosio [trxs: 0, lib: 421, confirmed: 0]
info  2018-12-01T08:19:59.000 thread-0  producer_plugin.cpp:1490      produce_block        ] Produced block 000001a7b44d8a1d... #423 @ 2018-12-06T08:19:59.000 signed by uosio [trxs: 0, lib: 422, confirmed: 0]
```

按ctrl+c关闭日志。

**步骤2.2: 检查钱包**

打开shell并运行以下命令。

{% code title="shell" %}

```bash
cluos wallet list
```

{% endcode %}

您将看到以下输出，现在还没有钱包。

```bash
Wallets:
[]
```

**步骤2.3: 检查noduos端点**

您需要检查RPC API是否正常工作。

{% code title="shell" %}

```bash
cluos get info
```

{% endcode %}

您将看到以下输出，表示运行成功。

```bash
{
  "server_version": "1f18fbff",
  "chain_id": "cb1025fad7e421fa75e769f4cdd76a2fc4db639ea7732fd299c6d28d14767771",
  "head_block_num": 3814186,
  "last_irreversible_block_num": 3814163,
  "last_irreversible_block_id": "003a3313f676cea154e9d122072cf6f494890dd0062108a455f40f3451757954",
  "head_block_id": "003a332ad48da528e53a52a78c33a9bc7e55024968f19eee556d6e444afe2ce8",
  "head_block_time": "2019-03-14T07:25:10.000",
  "head_block_producer": "uoszhongguo1",
  "virtual_block_cpu_limit": 450000000,
  "virtual_block_net_limit": 2097152000,
  "block_cpu_limit": 449900,
  "block_net_limit": 2097152,
  "server_version_string": "1.3.2-90-g1f18fbf"
}
```

### 6.1.5 创建开发钱包

**第一步: 创建钱包**

第一步是创建一个钱包。使用“cluos wallet create -n test”创建一个“test”钱包，使用选项--to-console。如果在生产环境中使用该命令，明智的做法是使用--file选项，这样您的钱包秘钥就不会出现在bash历史记录中。由于我们是以开发为目，而不是实际使用，因此--to-console选项不会带来安全隐患。

{% code title="shell" %}

```bash
cluos wallet create -n test --to-console
```

{% endcode %}

cluos将会返回一个秘钥，**保存该秘钥**以便接下来使用。

```bash
Creating wallet: test
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"PW5KBfdsViLUJ1vr4YQMRkc6qj8BR3wpZpYRhVgjvgyy1VzQy7sWP"
```

注意：使用cluos wallet create --to-console将创建一个名为“default”钱包。

**拓展：关于钱包**

加密货币中对于钱包的一个常见误解是认为它是用来存储代币的。事实上，钱包并不存储代币。钱包的作用是将私钥存储在加密文件中并签署交易。

用户通常通过接口构建事务对象，将该事务发送到要签名的钱包，然后钱包返回具有签名的事务，该签名随后被广播到网络中。当/如果网络确认该事务是有效的，它将被包含到区块链上的区块中。

**第二步: 打开钱包**

默认情况下，在启动kuosd时钱包是关闭的，请先运行以下命令：

{% code title="shell" %}

```bash
cluos wallet open -n test
```

{% endcode %}

运行以下命令获取钱包列表：

{% code title="shell" %}

```bash
cluos wallet list 
```

{% endcode %}

返回如下，此时，钱包已被打开，但是还没有解锁。

```bash
Wallets:
[
  "test"
]
```

**第三步: 解锁钱包**

刚才您获得了该钱包的秘钥，现在需要使用它来解锁钱包。运行以下命令：

{% code title="shell" %}

```bash
cluos wallet unlock -n test
```

{% endcode %}

系统会提示您输入密码，复制秘钥（**创建钱包时生成的秘钥**）然后粘贴并按回车键。

再次运行以下命令：

{% code title="shell" %}

```bash
cluos wallet list
```

{% endcode %}

返回如下：

```bash
Wallets:
[
  "test *"
]
```

特别注意星号（\*）。这意味着钱包目前已解锁。

**第四步: 导入开发私钥**

每个新的UOSIO链都有一个名为“uosio”的默认“系统”用户。此帐户通过加载系统合约来对链进行配置，这些合约规定了对链的管理和共识。每条新的UOSIO链都带有一个开发私钥，这个私钥是相同的。加载此私钥代表系统用户(uosio)签署事务。接来下我们将开发私钥导入钱包。

{% code title="shell" %}

```bash
cluos wallet import -n test
```

{% endcode %}

将提示您输入私钥，请输入下面提供的uosio开发私钥：

```bash
5KWHUBmukJaqccDQ3EGfbR3jpYnMfo4Ys98fGtXY8GEMasyhTRM
```

**提示：**&#x8BE5;私钥可以在配置文件config.ini中找到。

**警告：**

切勿将开发私钥用于生产帐户！这样做会导致您无法访问您的帐户，此私钥是公开的。

您现在已经有一个默认钱包，已解锁并导入了一个私钥。

### 6.1.6  创建测试账户

**什么是账户？**

帐户是存储在区块链上的授权集合，用于标识发送者/接收者。它具有灵活的授权结构，使其可以由个人或一组人拥有，具体取决于如何配置权限。您需要拥有一个帐户才能向区块链发送或接收有效的交易。

该系列教程主要使用两个“用户”bob和alice。

**创建测试账户**

我们将创建两个账户，分别为bob和alice。

首先，使用cluos wallet create\_key导入私钥并打印公钥：

{% code title="shell" %}

```bash
cluos wallet create_key -n test
```

{% endcode %}

返回结果如下：

```bash
Created new private key with a public key of: "UOS6PvswBMZqjdQPsa7M6WTPiUfVikMSeThUBi3rzksb29PRoDKWo"
```

使用命令[cluos create account](https://developers.eos.io/eosio-cleos/reference#cleos-create-account)创建账户：

{% code title="shell" %}

```bash
cluos create account uosio bob UOS6PvswBMZqjdQPsa7M6WTPiUfVikMSeThUBi3rzksb29PRoDKWo
```

{% endcode %}

然后，您会看到事务已被广播的反馈消息，返回如下，表示创建Bob账户成功。

```bash
executed transaction: 40c605006de...  200 bytes  153 us
#         uosio <= uosio::newaccount            {"creator":"uosio","name":"bob","owner":{"threshold":1,"keys":[{"key":"UOS6PvswBMZqjdQPsa7M6WTPiUfVi...
warning: transaction executed locally, but may not be confirmed by the network yet    ]
```

请尝试自己创建alice账户。

**注意：该文档中owner和active权限使用相同的私钥，在生产环境中owner/active请使用不同的私钥。**

UOSIO具有独特的授权结构，为您的帐户增加了安全性。您可以在使用与您的active权限相关联的私钥时保持owner私钥的安全，从而最大限度地减少帐户的风险。这样，如果您的active私钥全部遭到入侵，您可以使用owner私钥重新获得对帐户的控制权。

**故障排除**

如果您在创建帐户时遇到错误，请确保您的钱包已解锁。

{% code title="shell" %}

```bash
cluos wallet list
```

{% endcode %}

如果钱包已解锁，您应该在钱包名后面看到一个星号（\*），如下所示：

```bash
Wallets:
[
  "test *"
]
```

至此，开发环境已经部署完成。随后，将向您简单介绍合约编写和调用。

## 6.2 Hello合约

### 6.2.1 合约编写

创建合约目录（该文档示例为/root/contracts/）,并在该目录下创建一个名为“hello”的新目录，或者通过系统GUI创建一个名为“hello”的目录，进入该目录。

{% code title="shell" %}

```bash
cd /root
mkdir contracts && cd contracts
mkdir hello && cd hello
```

{% endcode %}

创建一个新文件“hello.cpp”并使用您喜欢的编辑器(教程使用vim)打开它。

{% code title="shell" %}

```bash
touch hello.cpp
vim hello.cpp
```

{% endcode %}

使所需要的库包含在文件中。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
```

{% endcode %}

在合约中使用命名空间uosio，可以使代码更简洁。

{% code title="c++" %}

```cpp
using namespace uosio;
```

{% endcode %}

* uosiolib/uosio.hpp将UOSIO C和C ++ API加载到合约中。

创建一个标准的C ++ 11类。合约类需要继承uosio::contract。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
using namespace uosio;
class hello : public contract {};
```

{% endcode %}

添加公共访问说明符和using声明。这样能让我们写出更简洁的代码。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
using namespace uosio;
class hello : public contract {
  public:
      using contract::contract;
};
```

{% endcode %}

本着hello world的精神编写一个接受“name”参数的action，然后输出该参数。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
using namespace uosio;
class hello : public contract {
  public:
      using contract::contract;
      void hi( name user ) {
         print( "Hello, ", name{user});
      }
};
```

{% endcode %}

上述action接收一个name类型的user参数，UOSIO附带了许多类型定义，name是您遇到最常见的参数之一。使用先前#include的库函数uosio::print，连接字符串并打印。

在action中添加C++11样式属性，这样abi生成器可以生成更可靠的输出。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
using namespace uosio;
class [[uosio::contract]] hello : public contract {
  public:
      using contract::contract;
      [[uosio::action]]
      void hi( name user ) {
         print( "Hello, ", name{user});
      }
};
```

{% endcode %}

最后，添加UOSIO\_DISPATCH宏来处理调度hello合约的action。

{% code title="c++" %}

```cpp
UOSIO_DISPATCH ( hello, (hi))
```

{% endcode %}

把所有代码组合到一起。至此就完成了hello合约的编写。

{% code title="hello.cpp" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
using namespace uosio;
class [[uosio::contract]] hello : public contract {
  public:
      using contract::contract;
      [[uosio::action]]
      void hi( name user ) {
         print( "Hello, ", name{user});
      }
};
UOSIO_DISPATCH ( hello, (hi))
```

{% endcode %}

### 6.2.2 合约编译及账户创建

您可以将代码编译为Web程序集（.wasm），如下所示：

{% code title="shell" %}

```bash
uosio-cpp -o hello.wasm hello.cpp --abigen
```

{% endcode %}

创建合约帐户hello。（创建账户详见 UOS合约开发—环境 文档）

{% code title="shell" %}

```bash
cluos create account uosio hello YOUR_PUBLIC_KEY
```

{% endcode %}

提示：1.首先要将uosio账户的私钥导入该钱包；2.YOUR\_PUBLIC\_KEY通过cluos wallet create\_key -n test生成。

### 6.2.3 合约部署及调用

使用cluos set contract命令将编译得到的wasm文件广播到网络中。

{% code title="shell" %}

```bash
cluos set contract hello /root/contracts/hello -p hello@active
```

{% endcode %}

**提示**：合约路径可以是绝对路径，也可以是相对路径。

现在合约已经部署，向链上推送一个该合约的action。

{% code title="shell" %}

```bash
cluos push action hello hi '["bob"]' -p bob@active
```

{% endcode %}

运行结果：

```bash
executed transaction: 4c10c1426c16b1656e802f3302677594731b380b18a44851d38e8b5275072857  244 bytes  1000 cycles
#    hello.code <= hello.code::hi               {"user":"bob"}
>> Hello, bob
```

该合约将允许任何帐户调用该action。

{% code title="shell" %}

```bash
cluos push action hello hi '["bob"]' -p alice@active
```

{% endcode %}

运行结果：

```bash
executed transaction: 28d92256c8ffd8b0255be324e4596b7c745f50f85722d0c4400471bc184b9a16  244 bytes  1000 cycles
#    hello.code <= hello.code::hi               {"user":"bob"}
>> Hello, bob
```

正如预期的那样，控制台输出也是“Hello，bob”。

在这种情况下，“alice”是授权它的人，而且user只是一个参数。接下来我们修改合约，要求授权用户“alice”必须与action中“hi”的参数相同。我们将使用require\_auth方法，此方法将name作为参数，并将检查执行操作的用户是否与提供的参数匹配。

{% code title="c++" %}

```cpp
void hi(  name user ) {
   require_auth( user );
   print( "Hello, ", name{user} );
}
```

{% endcode %}

更新合约代码如下：

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>
using namespace uosio;
class [[uosio::contract]] hello : public contract {
  public:
      using contract::contract;
      [[uosio::action]]
      void hi(  name user ) {
         require_auth( user );
         print( "Hello, ", name{user});
      }
};
UOSIO_DISPATCH( hello, (hi))
```

{% endcode %}

重新编译合约

{% code title="shell" %}

```bash
uosio-cpp -o hello.wasm hello.cpp --abigen
```

{% endcode %}

更新合约

{% code title="shell" %}

```bash
cluos set contract hello /root/contracts/hello -p hello@active
```

{% endcode %}

尝试再次执行操作action，但这次授权账户与参数不匹配。

{% code title="shell" %}

```bash
cluos push action hello hi '["bob"]' -p alice@active
```

{% endcode %}

正如预期的那样，require\_auth暂停了事务并提示错误。

运行结果：

```bash
Error 3090004: Missing required authority
Ensure that you have the related authority inside your transaction!;
If you are currently using 'cluos push action' command, try to add the [relevant](**http://google.com**) authority using -p option.
```

通过对合约的更改，验证user的内容与授权用户相同。再试一次，但这一次，传入alice作为参数，并且用alice账户进行授权。

{% code title="shell" %}

```bash
cluos push action hello hi '["alice"]' -p alice@active
```

{% endcode %}

运行结果：

```bash
executed transaction: 235bd766c2097f4a698cfb948eb2e709532df8d18458b92c9c6aae74ed8e4518  244 bytes  1000 cycles
#    hello <= hello::hi               {"user":"alice"}
>> Hello, alice
```

至此，一个简单完整的hello合约已经完成！接下来，我们将尝试更复杂的合约编写和探索！

## 6.3 通讯录合约

### 6.3.1 创建一个新目录和文件

之前，我们已经创建了一个合约目录（/root/contracts），现在我们进入该目录并创建一个新的目录（addressbook）。

{% code title="shell" %}

```bash
cd contracts
mkdir addressbook
cd addressbook
```

{% endcode %}

创建一个新的文件。

{% code title="shell" %}

```bash
touch addressbook.cpp
```

{% endcode %}

使用您喜欢的编辑器打开该文件。

{% code title="shell" %}

```bash
vim addressbook.cpp
```

{% endcode %}

### 6.3.2 编写合约类框架

在之前的教程中，我们创建了一个hello合约，学习了基础知识。您应该了解下面的结构，此处我们修改类名为addressbook。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>

using namespace uosio;

class [[uosio::contract]] addressbook : public uosio::contract {
  public:

  private: 

};
```

{% endcode %}

### 6.3.3 为表创建数据结构

在配置和实例化表之前，需要编写表示通讯录的数据结构，我们姑且称之为“架构”。由于它是一个通讯录，该表将包含人员信息，因此创建一个名为“person”的结构体（struct）。

{% code title="c++" %}

```cpp
struct person {};
```

{% endcode %}

定义multi\_index表的模式时，需要使用唯一值作为主键。

对于此合约，我们使用名为“key”的name类型字段。此合约将为每个用户提供一个唯一条目，因此该“key”将是基于用户且唯一的name类型字段。

{% code title="c++" %}

```cpp
struct person {
         name key; 
};
```

{% endcode %}

此外，该结构体应该为每个条目或人员存储一些相关的信息。

{% code title="c++" %}

```cpp
struct person {
 name key;
 string first_name;
 string last_name;
 string street;
 string city;
 string state;
};
```

{% endcode %}

基本架构现已完成。接下来，定义一个primary\_key方法以供multi\_index迭代器使用。每个multi\_index架构都需要一个主键。要实现此目的，您只需创建一个名为primary\_key()的方法并返回一个值，在本例中我们使用结构中定义的“key”成员。

{% code title="c++" %}

```cpp
struct person {
 name key;
 string first_name;
 string last_name;
 string street;
 string city;
 string state;

 uint64_t primary_key() const { return key.value;}
};
```

{% endcode %}

提示：当表中包含数据时，无法修改表的模式。如果需要更改表的架构，首先需要删除其所有行。

### 6.3.4 配置多索引表

现在已经定义了表的结构，现在我们需要配置表。uosio::multi\_index的构造需要进行命名和配置，从而使用我们前面定义的结构体。

{% code title="c++" %}

```cpp
typedef uosio::multi_index<"people"_n, person> address_index;
```

{% endcode %}

1. 使用\_n运算符定义uosio::name类型并为该表命名。该表包含许多不同的“person”，因此将表命名为“people”。
2. 传入上一步中定义的结构体person。
3. 声明该表的类型。此类型将用于稍后实例化该表。

{% code title="c++" %}

```cpp
//configure the table
typedef uosio::multi_index<"people"_n, person> address_index;
```

{% endcode %}

使用上述multi\_index配置，定义一个名为people的multi\_index表，该表存储person类型的数据。

到目前为止，我们的代码是这样的。

{% code title="c++" %}

```cpp
#include <uosiolib/uosio.hpp>

using namespace uosio;

class [[uosio::contract]] addressbook : public uosio::contract {

  public:

  private:
    struct [[uosio::table]] person {
      name key;
      std::string first_name;
      std::string last_name;
      std::string street;
      std::string city;
      std::string state;

      uint64_t primary_key() const { return key.value;}
    };
    typedef uosio::multi_index<"people"_n, person> address_index;
};
```

{% endcode %}

### 6.3.5 构造函数

使用C++类时，您应该创建的第一个公共方法是构造函数。

我们的构造函数将负责初始化合约。

UOSIO合约继承了contract类。使用合约的名称(code)和接收者(receiver)初始化我们的contract父类。这里的重要参数是code，该参数指定部署合约的账户名。

{% code title="c++" %}

```cpp
addressbook(name receiver, name code, datastream<const char*> ds) : contract(receiver, code, ds) {}
```

{% endcode %}

### 6.3.6 向表中添加数据

多索引表的主键被定义为强制此合约仅为每个用户存储一条记录。为使其生效，需要建立一些关于合约的设定。

1. 授权修改通讯录的唯一帐户是用户。
2. 表的primary\_key是唯一的，基于用户名。
3. 就可用性而言，合约应该能够通过单个操作创建和修改表的某一行。

在uos中，链上某个账户是唯一的，因此在这个特定用例中name作为**primary\_key**是理想的候选者。

接下来，为用户定义添加或更新记录的操作。此操作需要接受某些值并创建或修改记录。

格式化定义可以使代码更容易阅读。为了增强用户体验和简化接口，用一个方法负责创建和修改记录。我们将其命名为“upsert”，即“update”和“insert”的组合。

{% code title="c++" %}

```cpp
void upsert(
  name user, 
  std::string first_name, 
  std::string last_name, 
  std::string street, 
  std::string city, 
  std::string state
) {}
```

{% endcode %}

此前，有人提到只有用户自己才能控制自己的记录，因为这个合约是选择性加入的。为此，我们使用uosio.cdt支持的require\_auth方法。此方法接受一个name类型的参数，并验证执行事务的帐户是否和该参数匹配。

{% code title="c++" %}

```cpp
void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
}
```

{% endcode %}

现在，我们将实例化表。我们已经配置了multi\_index表，并将其声明为address\_index。要实例化一个表，请考虑它的两个必须参数：

1. “code”，代表合约帐户。可以通过范围\_code变量访问该值。
2. “scope”，用于确保合约的唯一性，此例中，由于只有一个表，我们也可以使用“\_code.value”。

{% code title="c++" %}

```cpp
void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
}
```

{% endcode %}

接下来，查询迭代器，将其设置为可变的，因为此迭代器将被多次使用。

{% code title="c++" %}

```cpp
void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
  auto iterator = addresses.find(user.value);
}
```

{% endcode %}

至此，权限验证和表格实例化已经完成。接下来，编写用于创建或修改表的逻辑。检测特定用户是否已存在。

为此，请传递user参数来使用表的find方法。find方法将返回一个迭代器。将迭代器与end方法进行比较。“end”方法是“null”的别名。

{% code title="c++" %}

```cpp
void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
  auto iterator = addresses.find(user.value);
  if( iterator == addresses.end() )
  {
    //The user isn't in the table
  }
  else {
    //The user is in the table
  }
}
```

{% endcode %}

使用multi\_index的emplace方法在表中创建记录。此方法接受两个参数，即支付存储该记录开销的“付款人”和回调函数。

emplace方法的回调函数必须使用lamba来创建引用。在lamba函数体分配“row”的值并将该值传给upsert方法。

{% code title="c++" %}

```cpp
void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
  auto iterator = addresses.find(user.value);
  if( iterator == addresses.end() )
  {
    addresses.emplace(user, [&]( auto& row ) {
      row.key = user;
      row.first_name = first_name;
      row.last_name = last_name;
      row.street = street;
      row.city = city;
      row.state = state;
    });
  }
  else {
    //The user is in the table
  }
}
```

{% endcode %}

接下来，处理“upsert”函数的修改或更新。使用modify方法，并传递参数

* 在调用“upsert”操作时，前面定义的迭代器设置为声明的用户。
* “scope”或“ram payer”，付费账户，是指由谁提供存储数据所需的内存。
* 回调函数用于处理表的修改。

{% code title="c++" %}

```cpp
void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
  require_auth( user );
  address_index addresses(_code, _code.value);
  auto iterator = addresses.find(user.value);
  if( iterator == addresses.end() )
  {
    addresses.emplace(user, [&]( auto& row ) {
      row.key = user;
      row.first_name = first_name;
      row.last_name = last_name;
      row.street = street;
      row.city = city;
      row.state = state;
    });
  }
  else {
    std::string changes;
    addresses.modify(iterator, user, [&]( auto& row ) {
      row.key = user;
      row.first_name = first_name;
      row.last_name = last_name;
      row.street = street;
      row.city = city;
      row.state = state;
    });
  }
}
```

{% endcode %}

现在，该addressbook合约有一个基本动作：如果记录不存在，用户在表中创建一行，如果已经存在则修改它。

但是如果用户想要完全删除记录呢？

### 6.3.7 从表中删除记录

与前面的步骤类似，在addressbook类中创建一个公共方法，确保包含require\_auth，该函数根据操作的参数user进行验证，仅有记录的所有者可以修改。

{% code title="c++" %}

```cpp
    void erase(name user){
      require_auth(user);
    }
```

{% endcode %}

实例化表，在表格中每个帐户只有一个记录。通过调用find方法对迭代器进行赋值。

{% code title="c++" %}

```cpp
...
    void erase(name user){
      require_auth(user);
      address_index addresses(_code, _code.value);
      auto iterator = addresses.find(user.value);
    }
...
```

{% endcode %}

合约不能删除不存在的记录，因此在删除之前验证记录确实存在。

{% code title="c++" %}

```cpp
...
    void erase(name user){
      require_auth(user);
      address_index addresses(_code, _code.value);
      auto iterator = addresses.find(user.value);
      uosio_assert(iterator != addresses.end(), "Record does not exist");
    }
...
```

{% endcode %}

最后，调用表的erase方法，擦除迭代器。

{% code title="c++" %}

```cpp
...
  void erase(name user) {
    require_auth(user);
    address_index addresses(_code, _code.value);
    auto iterator = addresses.find(user.value);
    uosio_assert(iterator != addresses.end(), "Record does not exist");
    addresses.erase(iterator);
  }
...
```

{% endcode %}

现在，合约基本完成。用户可以创建、修改和删除记录。但是，合约还没有准备好编译。

### 6.3.8准备ABI

完成以下步骤以完成合约。

#### **6.3.8.1 UOSIO\_DISPATCH**

在文件的底部，使用UOSIO\_DISPATCH宏，传入合约的名称，以及动作“upsert”和“erase”。

该宏将不同的action调用分派给合约中的相应方法进行处理。

在底部添加以下内容，将使我们的cpp文件与UOSIO的wasm解释器兼容。未能包含此声明可能会在部署合约时导致错误。

{% code title="c++" %}

```cpp
UOSIO_DISPATCH( addressbook, (upsert)(erase) )
```

{% endcode %}

#### **6.3.8.2 ABI的action声明**

uosio.cdt包含一个ABI Generator，为了它能正常工作，我们的合约需要一些小的声明。

在upsert和erase函数之上添加以下C++11声明

{% code title="c++" %}

```cpp
[[uosio::action]]
```

{% endcode %}

上述声明将提取操作的内容并在生成的ABI文件中创建必要的ABI结构描述。

#### **6.3.8.3 ABI的table声明**

向表中添加ABI声明。修改合约私有区域中定义的以下行：

{% code title="c++" %}

```cpp
struct person {
```

{% endcode %}

修改后代码如下：

{% code title="c++" %}

```cpp
struct [[uosio::table]] person {
```

{% endcode %}

该\[\[uosio::table]]声明将对表格添加必要的说明。

现在，我们的合约已经准备好进行编译了。

以下是我们addressbook合约的最终状态：

{% code title="addressbook.cpp" %}

```cpp
#include <uosiolib/uosio.hpp>
#include <uosiolib/print.hpp>

using namespace uosio;

class [[uosio::contract]] addressbook : public uosio::contract {

public:
  using contract::contract;

  addressbook(name receiver, name code,  datastream<const char*> ds): contract(receiver, code, ds) {}

  [[uosio::action]]
  void upsert(name user, std::string first_name, std::string last_name, std::string street, std::string city, std::string state) {
    require_auth( user );
    address_index addresses(_code, _code.value);
    auto iterator = addresses.find(user.value);
    if( iterator == addresses.end() )
    {
      addresses.emplace(user, [&]( auto& row ) {
       row.key = user;
       row.first_name = first_name;
       row.last_name = last_name;
       row.street = street;
       row.city = city;
       row.state = state;
      });
    }
    else {
      std::string changes;
      addresses.modify(iterator, user, [&]( auto& row ) {
        row.key = user;
        row.first_name = first_name;
        row.last_name = last_name;
        row.street = street;
        row.city = city;
        row.state = state;
      });
    }
  }

  [[uosio::action]]
  void erase(name user) {
    require_auth(user);

    address_index addresses(_self, _code.value);

    auto iterator = addresses.find(user.value);
    uosio_assert(iterator != addresses.end(), "Record does not exist");
    addresses.erase(iterator);
  }

private:
  struct [[uosio::table]] person {
    name key;
    std::string first_name;
    std::string last_name;
    std::string street;
    std::string city;
    std::string state;
    uint64_t primary_key() const { return key.value; }
  };
  typedef uosio::multi_index<"people"_n, person> address_index;

};
UOSIO_DISPATCH( addressbook, (upsert)(erase))
```

{% endcode %}

### 6.3.9 编译合约及部署

编译合约，在终端执行以下命令。

{% code title="shell" %}

```bash
uosio-cpp -o addressbook.wasm addressbook.cpp --abigen
```

{% endcode %}

然后，为合约创建一个addressbook帐户（创建账户详见《UOS合约开发—环境》文档）。

部署addressbook合约。

{% code title="shell" %}

```bash
cluos set contract addressbook /root/contracts/addressbook -p addressbook@active
```

{% endcode %}

运行结果：

```bash
5f78f9aea400783342b41a989b1b4821ffca006cd76ead38ebdf97428559daa0  5152 bytes  727 us
#         uosio <= uosio::setcode               {"account":"addressbook","vmtype":0,"vmversion":0,"code":"0061736d010000000191011760077f7e7f7f7f7f7f...
#         uosio <= uosio::setabi                {"account":"addressbook","abi":"0e656f73696f3a3a6162692f312e30010c6163636f756e745f6e616d65046e616d65...
warning: transaction executed locally, but may not be confirmed by the network yet    ]
```

### 6.3.10 测试合约

调用“upsert”方法在表中添加一行。

{% code title="shell" %}

```bash
cluos push action addressbook upsert '["alice", "alice", "liddell", "123 drink me way", "wonderland", "amsterdam"]' -p alice@active
```

{% endcode %}

运行结果：

```bash
executed transaction: 003f787824c7823b2cc8210f34daed592c2cfa66cbbfd4b904308b0dfeb0c811  152 bytes  692 us
#   addressbook <= addressbook::upsert          {"user":"alice","first_name":"alice","last_name":"liddell","street":"123 drink me way","city":"wonde...
```

检查alice能否为其他用户添加记录。

{% code title="shell" %}

```bash
cluos push action addressbook upsert '["bob", "bob", "is a loser", "doesnt exist", "somewhere", "someplace"]' -p alice@active
```

{% endcode %}

正如所料，该合约阻止了alice创建/修改另一个用户的数据。

运行结果：

```bash
Error 3090004: Missing required authority
Ensure that you have the related authority inside your transaction!
If you are currently using 'cluos push action' command, try to add the relevant authority using -p option.
```

检索表格中alice的记录。

{% code title="shell" %}

```bash
cluos get table addressbook addressbook people --lower alice --limit 1
```

{% endcode %}

运行结果：

```bash
{
  "rows": [{
      "key": "alice",
      "first_name": "alice",
      "last_name": "liddell",
      "street": "123 drink me way",
      "city": "wonderland",
      "state": "amsterdam"
    }
  ],
  "more": false
}
```

测试alice是否可以删除自己的记录。

{% code title="shell" %}

```bash
cluos push action addressbook erase '["alice"]' -p alice@active
```

{% endcode %}

运行结果：

```bash
executed transaction: 0a690e21f259bb4e37242cdb57d768a49a95e39a83749a02bced652ac4b3f4ed  104 bytes  1623 us
#   addressbook <= addressbook::erase           {"user":"alice"}
warning: transaction executed locally, but may not be confirmed by the network yet    ]
```

检查记录是否已被删除。

{% code title="shell" %}

```bash
cluos get table addressbook addressbook people --lower alice --limit 1
```

{% endcode %}

运行结果：

```bash
{
  "rows": [],
  "more": false
}
```

至此，如何配置表、实例化表、创建新行、修改现有行以及如何使用迭代器已经展示完成。也展示了如何针对空迭代器的结果进行测试，以及如何配置合约的ABI。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mastering-uos.gitbook.io/mastering-uos/di-jiu-zhang-he-yue-kai-fa-jiao-cheng.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
