# Cairo 之旅 VIII：用 Protostar 编写、部署第一个 Starknet 合约

By [致力于Starknet](https://paragraph.com/@guobat) · 2023-11-19

---

今天，我们将做一件非常棒的事情：编写和部署第一个 Starknet 合约！ 像往常一样，如果你是中途加入，建议从头开始看我们的文章。

对于本地开发，我们将使用 Protostar，所以如果你还没有在本地设置 Protostar，请阅读本系列[第一篇文章](https://mirror.xyz/starknet-zh.eth/PSNMGAXKIrXvxhILjp35aM3tW4c4AnGclfzMlQ5hQ64)。

_P.S：终于能使用 Cairo0.10 版本了！今天不用 Starklings 了。别忘记更新你的 Cairo 版本和 Protostar 0.4.2 版本！_

**目标**
------

在本篇教程中学会：

1.  用 Cairo 编写一个简单的 Starknet 合约。
    
2.  理解事件 (Event)、构造器 (Constructor)、外部函数 (External) 和视图函数 (View)。
    
3.  使用 Protostar 部署你的合约。
    
4.  通过 Voyager 与合约进行交互。
    

**项目描述**
--------

我们将建立一个简单的 **Starknet 命名服务** (Naming Service)，将名字映射到钱包地址。

这是关于如何使用 [Starknet.js](https://medium.com/@darlingtonnnam/an-in-depth-guide-to-getting-started-with-starknet-js-a55c04d0ccb7) 构建前端应用程序的教程中使用过类似的合同，查看 [demo](https://starknet-ens.netlify.app/)。

**开始工作**
--------

首先完成准备工作，用 Protostar 建立本地环境，为了减少篇幅精简知识点，在此不再过多叙述[如何设置 Protostar](https://mirror.xyz/starknet-zh.eth/PSNMGAXKIrXvxhILjp35aM3tW4c4AnGclfzMlQ5hQ64)。

### **初始化一个新的 Protostar 项目**

安装好 Protostar 后，我们通过运行命令来初始化一个新项目：

    protostar init
    

此步骤要求我们提供项目名称和 lib 名称，需要输入这些信息以成功创建一个新项目。

在我们的代码编辑器中打开新项目查看已创建的文件夹：

注意 protostar 创建了一个 **main.cairo** 文件包含一些已预先写好的 Cairo 代码。你可以在我们完成后查看一下，并尝试修改它以巩固知识点。

**src** 文件夹包含我们的合约代码，**lib** 文件夹包含所有的外部进口，如 openzeppelin 合同等，test 文件夹包含测试脚本，**protostar.toml** 文件是我们的项目配置文件。

### **创建新的文件**

下一步，我们在 src 文件夹中创建命名 Starknet.cairo 新文件，开始编写我们的合约代码。

### **导入**

首先，类似于我们用 **pragma solidity** 完成 solidity 合约，用 **%lang starknet** 指令来指定文件中包含 Starknet 合约的代码。

然后，我们将导入所有可能在合约中使用到的，必要的库函数。

    %lang starknet
    from starkware.cairo.common.cairo_builtins import HashBuiltin
    from starkware.starknet.common.syscalls import get_caller_address
    

导入 HashBuiltin 是因为我们的 pedersen 内置程序需要它，而 get\_caller\_address 则是为了获取 msg.sender (调用合约函数的用户地址)。

### **存储变量**

详情阅读 [Cairo 存储文章](https://mirror.xyz/starknet-zh.eth/6MUyx_DzjlJctp7S17RFvhZaQADvs3CJmVtzzHnC5mk)，我们的合约只有一个单一的存储变量，作为一个地址到名字的映射。

    @storage_var
    func names(address) -> (name: felt) {
    }
    

### **事件**

[事件](https://cairo-lang.org/docs/hello_starknet/events.html)允许合约以特定的格式向区块链记录状态的变化，用于允许虚拟机轻松检索和过滤它们。

**@event** 修饰器在 Cairo 中创建一个事件。每当我们存储一个新名字，事件将发出调用者地址和输入名。

    @event
    func stored_name(address: felt, name: felt){
    }
    

### **构造器**

[构造器](https://cairo-lang.org/docs/hello_starknet/constructors.html)是编写合约的重要部分。你可以用它们来在合约部署时初始化某些状态变量。

使用 **@constructor** 修饰器在 Cairo 中创建一个构造函数。

虽然我们目前的项目不一定需要构造函数，但为了演示如何使用，我们将创建一个构造函数，为调用者设置一个默认名。

    @constructor
    func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt) {
    let (caller) = get_caller_address();
    names.write(caller, _name);
    return ();
    }
    

### **外部函数**

如果你学过 Solidity，你会习惯于四种函数类型 (public, private, external, internal)，但在 Cairo 中只有两种类型的函数，_外部函数和视图函数_。

外部函数：改变区块链状态的函数，使用 **@external** 修饰器创建。

对于我们的合约，创建一个外部函数 **_store\_name_**，它能接收一个**输入名**，并更新**名字变量**。

    @external
    func store_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt){
    let (caller) = get_caller_address();
    names.write(caller, _name);
    stored_name.emit(caller, _name);
    return ();
    }
    

正如代码展示的，首先使用我们先前导入的 get\_caller\_address 库函数获得调用者，然后更新名字存储变量，最后发出一个 stored\_name 事件。

### **视图函数**

视图函数：getter 函数，它们不改变区块链的状态，使用 **@view** 修饰器创建。

对于我们的合约，创建一个视图函数 **get\_name**，它能接收一个**输入地址**，并返回对应名称。

    @view
    func get_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_address: felt) -> (name: felt){
    let (name) = names.read(_address);
    return (name,);
    }
    

检查我们的代码是否完整：

    %lang starknet
    from starkware.cairo.common.cairo_builtins import HashBuiltin
    from starkware.starknet.common.syscalls import get_caller_address
    @storage_var
    func names(address) -> (name: felt) {
    }
    @event
    func stored_name(address: felt, name: felt){
    }
    @constructor
    func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt) {
    let (caller) = get_caller_address();
    names.write(caller, _name);
    return ();
    }
    @external
    func store_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_name: felt){
    let (caller) = get_caller_address();
    names.write(caller, _name);
    stored_name.emit(caller, _name);
    return ();
    }
    @view
    func get_name{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(_address: felt) -> (name: felt){
    let (name) = names.read(_address);
    return (name,);
    }
    

下面开始在 StarkNet 上部署它！

### **部署合约**

有了 Protostar，可以轻松地部署你的合约。我们需要做的第一件事是更新配置文件 protostar.toml 中的 protostar.contracts 部分，可以包含合约代码的路径。

下一步，建立我们的合约。在 Protostar 中建立合约类似于用 Hardhat 编译合约：

成功显示为：

我们需要一个 Python 脚本来进行字符串到 felts 的转换，因为我们的构造函数需要一个 felts 类型的**名字**。

第一步，在你的根目录下创建一个 **utils.py** 文件，粘贴以下代码：

    MAX_LEN_FELT = 31
    
    def str_to_felt(text):
    if len(text) > MAX_LEN_FELT:
    raise Exception("Text length too long to convert to felt.")
    return int.from_bytes(text.encode(), "big")
    
    def felt_to_str(felt):
    length = (felt.bit_length() + 7) // 8
    return felt.to_bytes(length, byteorder="big").decode("utf-8")
    
    def str_to_felt_array(text):
    return [str_to_felt(text[i:i+MAX_LEN_FELT]) for i in range(0, len(text), MAX_LEN_FELT)]
    
    def uint256_to_int(uint256):
    return uint256[0] + uint256[1]*2**128
    
    def uint256(val):
    return (val & 2128-1, (val & (2256-2**128)) >> 128)
    
    def hex_to_felt(val):
    return int(val, 16)
    

打开终端运行：

    python3 -i utils.py
    

检查：

我们将短字符串转换成 felts：

    str_to_felt("Darlington")
    

你可以在终端看到产生的短字符串。

完成以上步骤，准备好部署我们的合约！

### **Proostar 部署命令**

运行 Protostar 部署命令，传入测试网络和 felt 格式的名称输入：

    protostar deploy ./build/starknet.json --network testnet -i 322918500091226412576622
    

*   `deploy` 命令是 protostar 的一个内置命令。
    
*   `./build/main.json` 指定编译文件的路径。
    
*   `--network` 变量用于指定需要部署到的网络。
    
*   `--i` 变量用于指定构造函数所需的输入。
    

阅读更多的[部署指令](https://docs.swmansion.com/protostar/docs/tutorials/deploying/cli)。

部署完毕后，我们会在屏幕上得到合约地址和交易哈希，在 Voyager 上复制并进行交互。

### **与合约交互**

合约部署完成后，可以通过 [Voyager](https://goerli.voyager.online/) 检查它并进行交互。

**读取合约**部分是我们可以与视图函数交互的地方，而**写入合约**部分是我们与外部函数交互的地方。

### **通过 Voyager 存储名字**

### **通过 Voyager 读取名字**

---

*Originally published on [致力于Starknet](https://paragraph.com/@guobat/cairo-viii-protostar-starknet)*
