Starknet开发指南:如何使用Cairo实现和测试存储合约

Starknet开发指南:如何使用Cairo实现和测试存储合约在这篇文章中,我们将带领读者逐步完成在Starknet上开发一个简单存储合约的过程。通过使用Cairo语言进行编写,您将学习如何在区块链上存储和读取数据,掌握Starknet合约的基本开发流程,并使用Scarb进行项目构建和测试。本教程

Starknet开发指南:如何使用Cairo实现和测试存储合约

在这篇文章中,我们将带领读者逐步完成在Starknet上开发一个简单存储合约的过程。通过使用Cairo语言进行编写,您将学习如何在区块链上存储和读取数据,掌握Starknet合约的基本开发流程,并使用Scarb进行项目构建和测试。本教程旨在为初学者提供一个清晰的起点,让您迅速上手Starknet开发。

本指南将展示如何在Starknet上构建一个简单的存储合约。您将了解Cairo编程语言的基本用法,合约的结构,以及如何使用Scarb工具进行构建和测试。通过实际操作,您将掌握存储和读取数据的关键技术,为未来的Starknet开发奠定基础。

实操

第一步:创建项目并用 Cursor 打开

通过 Scarb 创建新项目并打开项目文件夹。

scarb new simple_storage
cd simple_storage/
open -a cursor .

image-20240930163905659.png

第二步:项目目录结构

使用 tree 命令查看生成的项目结构。

simple_storage on  main [?] via 🅒 base 
➜ tree . -L 6 -I 'target|coverage|coverage_report|snfoundry_trace'

.
├── README.md
├── Scarb.lock
├── Scarb.toml
├── snfoundry.toml
├── src
│   ├── lib.cairo
│   └── simple_storage.cairo
└── tests
    └── test_contract.cairo

3 directories, 7 files

第三步:代码实现

我们将在 Scarb.toml 文件中配置项目依赖,并在 simple_storage.cairo 中编写智能合约代码。

  1. Scarb.toml 文件配置依赖项,包括 starknet 和 snforge_std 库。

  2. simple_storage.cairo 文件定义了合约的接口及实现,包括基本的数据存储功能和所有者访问控制逻辑。

Scarb.toml 文件

[package]
name = "simple_storage"
version = "0.1.0"
edition = "2023_11"

# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html

[dependencies]
starknet = "2.8.2"

[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.31.0" }
assert_macros = "2.8.2"

[[target.starknet-contract]]
sierra = true

[scripts]
test = "snforge test"
build = "scarb fmt && scarb build"

# https://github.com/software-mansion/cairo-coverage
# https://foundry-rs.github.io/starknet-foundry/testing/coverage.html
[profile.dev.cairo]
unstable-add-statements-functions-debug-info = true
unstable-add-statements-code-locations-debug-info = true
inlining-strategy = "avoid"

lib.cairo 文件

pub mod simple_storage;

simple_storage.cairo 文件

// Interface definition for SimpleStorage contract
#[starknet::interface]
pub trait ISimpleStorage<TContractState> {
    // Function to get the stored data
    fn get_data(self: @TContractState) -> u128;
    // Function to set new data
    fn set_data(ref self: TContractState, new_value: u128);
    // Function to get the owner address
    fn get_owner(self: @TContractState) -> starknet::ContractAddress;
}

// SimpleStorage contract implementation
#[starknet::contract]
mod SimpleStorage {
    use starknet::{ContractAddress, get_caller_address};

    // Storage structure definition
    #[storage]
    struct Storage {
        data: u128,
        owner: ContractAddress,
    }

    // Constructor function
    #[constructor]
    fn constructor(ref self: ContractState, initial_data: u128, owner: ContractAddress) {
        // Initialize data with the provided initial_data
        self.data.write(initial_data);
        // Set the owner to the provided owner address
        self.owner.write(owner);
    }

    // Implementation of the ISimpleStorage interface
    #[abi(embed_v0)]
    impl SimpleStorageImpl of super::ISimpleStorage<ContractState> {
        // Function to retrieve the stored data
        fn get_data(self: @ContractState) -> u128 {
            self.data.read()
        }

        // Function to update the stored data
        fn set_data(ref self: ContractState, new_value: u128) {
            // Check if the caller is the owner
            let caller = get_caller_address();
            assert(caller == self.owner.read(), 'Only the owner can set data');
            self.data.write(new_value);
        }

        // Function to get the owner address
        fn get_owner(self: @ContractState) -> ContractAddress {
            self.owner.read()
        }
    }
}

这段代码定义了一个名为 SimpleStorage 的智能合约,它实现了基本的数据存储和访问控制功能。让我们逐步分析其主要组成部分:

  1. 接口定义:

    #[starknet::interface]
    pub trait ISimpleStorage<TContractState> {
       fn get_data(self: @TContractState) -> u128;
       fn set_data(ref self: TContractState, new_value: u128);
       fn get_owner(self: @TContractState) -> starknet::ContractAddress;
    }

    这个接口定义了合约的公共方法,包括获取数据、设置数据和获取所有者地址。

  2. 合约实现:

    #[starknet::contract]
    mod SimpleStorage {
       // ... 合约内容 ...
    }

    这个模块包含了合约的具体实现。

  3. 存储结构:

    #[storage]
    struct Storage {
       data: u128,
       owner: ContractAddress,
    }

    定义了合约的存储结构,包括一个 128 位无符号整数 data 和一个合约地址 owner

  4. 构造函数:

    #[constructor]
    fn constructor(ref self: ContractState, initial_data: u128, owner: ContractAddress) {
       self.data.write(initial_data);
       self.owner.write(owner);
    }

    初始化合约,设置初始数据和所有者地址。

  5. 接口实现:

    #[abi(embed_v0)]
    impl SimpleStorageImpl of super::ISimpleStorage<ContractState> {
       // ... 方法实现 ...
    }

    实现了之前定义的接口,包括:

    • get_data: 读取存储的数据。
    • set_data: 更新数据,但只有所有者才能调用。
    • get_owner: 获取所有者地址。

这个合约展示了 Cairo 语言的几个重要特性:

  • 使用属性(如 #[starknet::contract], #[storage])来定义合约结构。
  • 实现接口来确保合约遵循预定义的方法。
  • 使用存储结构来管理合约状态。
  • 实现访问控制,确保只有所有者可以修改数据。

总的来说,这是一个简单但功能完整的智能合约,展示了基本的数据存储、访问控制和状态管理功能。它可以作为更复杂合约的起点,或用于教育目的来理解 Cairo 智能合约的基本结构。

第四步:构建并格式化

simple_storage on  main [?] via 🅒 base 
➜ scarb fmt && scarb build
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.22s
   Compiling simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 5 seconds

simple_storage on  main [?] via 🅒 base took 5.0s 
➜ scarb run build         
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.12s
   Compiling simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 4 seconds

这里使用了两种方式来执行格式化并构建的命令:

  1. scarb fmt && scarb build

    1. fmt Format project files
    2. build Compile current project
  2. scarb run build

    1. run Run arbitrary package scripts

    2. Scarb.toml 中配置:

      [scripts]
      build = "scarb fmt && scarb build"

第五步:编写测试代码

在 tests/test_contract.cairo 中编写测试合约的逻辑,模拟部署合约并测试存储、设置数据和所有者验证功能。

test_contract.cairo 文件

use starknet::{ContractAddress, contract_address_const};

use snforge_std::{
    declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
    stop_cheat_caller_address
};

use simple_storage::simple_storage::ISimpleStorageDispatcher;
use simple_storage::simple_storage::ISimpleStorageDispatcherTrait;

fn deploy_contract(name: ByteArray, initial_data: u128, owner: ContractAddress) -> ContractAddress {
    let contract = declare(name).unwrap().contract_class();
    let constructor_args = array![initial_data.into(), owner.into()];
    let (contract_address, _) = contract.deploy(@constructor_args).unwrap();
    contract_address
}

#[test]
fn test_initial_value() {
    let initial_data = 42_u128;
    let owner = contract_address_const::<0x1>();
    let contract_address = deploy_contract("SimpleStorage", initial_data, owner);
    let dispatcher = ISimpleStorageDispatcher { contract_address };
    let stored_data = dispatcher.get_data();
    assert(stored_data == initial_data, 'Initial data should match');
}

#[test]
fn test_set_and_get_data() {
    let initial_data = 0_u128;
    let owner = contract_address_const::<0x1>();
    let contract_address = deploy_contract("SimpleStorage", initial_data, owner);
    let dispatcher = ISimpleStorageDispatcher { contract_address };

    // Use start_cheat_caller_address to set the caller address
    start_cheat_caller_address(contract_address, owner);
    dispatcher.set_data(42);
    let updated_data = dispatcher.get_data();
    assert(updated_data == 42, 'Updated data should be 42');
    stop_cheat_caller_address(contract_address);
}

#[test]
fn test_multiple_updates() {
    let initial_data = 5_u128;
    let owner = contract_address_const::<0x1>();
    let contract_address = deploy_contract("SimpleStorage", initial_data, owner);
    let dispatcher = ISimpleStorageDispatcher { contract_address };

    assert(dispatcher.get_data() == 5, 'Initial data should be 5');

    // Use start_cheat_caller_address for all set_data calls
    start_cheat_caller_address(contract_address, owner);

    dispatcher.set_data(10);
    assert(dispatcher.get_data() == 10, 'Data should be 10');

    dispatcher.set_data(20);
    assert(dispatcher.get_data() == 20, 'Data should be 20');

    dispatcher.set_data(30);
    assert(dispatcher.get_data() == 30, 'Data should be 30');

    stop_cheat_caller_address(contract_address);
}

#[test]
fn test_zero_value() {
    let initial_data = 100_u128;
    let owner = contract_address_const::<0x1>();
    let contract_address = deploy_contract("SimpleStorage", initial_data, owner);
    let dispatcher = ISimpleStorageDispatcher { contract_address };

    assert(dispatcher.get_data() == 100, 'Initial data should be 100');

    // Use start_cheat_caller_address to set the data
    start_cheat_caller_address(contract_address, owner);
    dispatcher.set_data(0);
    assert(dispatcher.get_data() == 0, 'Data should be reset to 0');
    stop_cheat_caller_address(contract_address);
}

#[test]
fn test_owner() {
    let initial_data = 0_u128;
    let owner = contract_address_const::<0x1>();
    let contract_address = deploy_contract("SimpleStorage", initial_data, owner);
    let dispatcher = ISimpleStorageDispatcher { contract_address };

    assert(dispatcher.get_owner() == owner, 'Owner should match');
}

第六步:执行测试

https://foundry-rs.github.io/starknet-foundry/testing/running-tests.html

运行所有测试

simple_storage on  main [?] via 🅒 base 
➜ scarb test
     Running test simple_storage (snforge test)
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.18s
   Compiling simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 4 seconds
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.12s
   Compiling test(simple_storage_unittest) simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
   Compiling test(simple_storage_integrationtest) simple_storage_integrationtest v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 8 seconds

Collected 5 test(s) from simple_storage package
Running 0 test(s) from src/
Running 5 test(s) from tests/
[PASS] simple_storage_integrationtest::test_contract::test_owner (gas: ~169)
[PASS] simple_storage_integrationtest::test_contract::test_initial_value (gas: ~233)
[PASS] simple_storage_integrationtest::test_contract::test_set_and_get_data (gas: ~245)
[PASS] simple_storage_integrationtest::test_contract::test_zero_value (gas: ~184)
[PASS] simple_storage_integrationtest::test_contract::test_multiple_updates (gas: ~261)
Tests: 5 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

运行指定测试

simple_storage on  main [?] via 🅒 base took 14.1s 
➜ scarb test test_set_and_get_data          
     Running test simple_storage (snforge test)
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.18s
   Compiling simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 4 seconds
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.12s
   Compiling test(simple_storage_unittest) simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
   Compiling test(simple_storage_integrationtest) simple_storage_integrationtest v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 8 seconds

Collected 1 test(s) from simple_storage package
Running 1 test(s) from tests/
[PASS] simple_storage_integrationtest::test_contract::test_set_and_get_data (gas: ~245)
Running 0 test(s) from src/
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 4 filtered out

显示测试期间使用的资源

simple_storage on  main [?] via 🅒 base took 14.6s 
➜ scarb test test_set_and_get_data --detailed-resources       
     Running test simple_storage (snforge test)
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.18s
   Compiling simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 4 seconds
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.12s
   Compiling test(simple_storage_unittest) simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
   Compiling test(simple_storage_integrationtest) simple_storage_integrationtest v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 8 seconds

Collected 1 test(s) from simple_storage package
Running 1 test(s) from tests/
[PASS] simple_storage_integrationtest::test_contract::test_set_and_get_data (gas: ~245)
        steps: 7255
        memory holes: 1057
        builtins: (range_check: 115, pedersen: 7)
        syscalls: (StorageWrite: 3, StorageRead: 2, CallContract: 2, Deploy: 1, GetExecutionInfo: 1)

Running 0 test(s) from src/
Tests: 1 passed, 0 failed, 0 skipped, 0 ignored, 4 filtered out

第七步:查看测试覆盖率

https://github.com/software-mansion/cairo-coverage

https://foundry-rs.github.io/starknet-foundry/testing/coverage.html

安装 cairo-coverage

curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh

实操

~ via 🅒 base took 3.3s
➜
curl -L https://raw.githubusercontent.com/software-mansion/cairo-coverage/main/scripts/install.sh | sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3014  100  3014    0     0   2876      0  0:00:01  0:00:01 --:--:--  2878
Downloading and extracting cairo-coverage-v0.2.0-aarch64-apple-darwin...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 2430k  100 2430k    0     0   989k      0  0:00:02  0:00:02 --:--:-- 1695k
cairo-coverage (v0.2.0) has been installed successfully.

~ via 🅒 base
➜
cairo-coverage -V
cairo-coverage 0.2.0

2. Scarb.toml 文件中进行如下配置:

[profile.dev.cairo]
unstable-add-statements-code-locations-debug-info = true
unstable-add-statements-functions-debug-info = true
inlining-strategy = "avoid"

3. 调用 cairo-coverage 运行测试生成测试覆盖率文件

simple_storage on  main [?] via 🅒 base took 14.5s 
➜ scarb test --coverage                                
     Running test simple_storage (snforge test)
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.18s
   Compiling simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 4 seconds
   Compiling snforge_scarb_plugin v0.31.0 (git+https://github.com/foundry-rs/starknet-foundry?tag=v0.31.0#72ea785ca354e9e506de3e5d687da9fb2c1b3c67)
    Finished `release` profile [optimized] target(s) in 0.13s
   Compiling test(simple_storage_unittest) simple_storage v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
   Compiling test(simple_storage_integrationtest) simple_storage_integrationtest v0.1.0 (/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/Scarb.toml)
    Finished release target(s) in 9 seconds

Collected 5 test(s) from simple_storage package
Running 5 test(s) from tests/
[PASS] simple_storage_integrationtest::test_contract::test_initial_value (gas: ~233)
[PASS] simple_storage_integrationtest::test_contract::test_owner (gas: ~169)
[PASS] simple_storage_integrationtest::test_contract::test_set_and_get_data (gas: ~245)
[PASS] simple_storage_integrationtest::test_contract::test_zero_value (gas: ~184)
[PASS] simple_storage_integrationtest::test_contract::test_multiple_updates (gas: ~261)
Running 0 test(s) from src/
[WARNING] No trace data to generate coverage from
Tests: 5 passed, 0 failed, 0 skipped, 0 ignored, 0 filtered out

查看生成的文件:

simple_storage on  main [?] via 🅒 base 
➜ cd coverage         

simple_storage/coverage on  main [?] via 🅒 base 
➜ ls
coverage.lcov

simple_storage/coverage on  main [?] via 🅒 base 
➜ cat coverage.lcov                                            
TN:
SF:/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage/src/simple_storage.cairo
FN:31,simple_storage::simple_storage::SimpleStorage::SimpleStorageImpl::get_data
FNDA:28,simple_storage::simple_storage::SimpleStor

4. 使用 genhtml 工具生成并查看测试覆盖率报告

安装 lcov
brew install lcov

==> Auto-updating Homebrew...
Adjust how often this is run with HOMEBREW_AUTO_UPDATE_SECS or disable with
HOMEBREW_NO_AUTO_UPDATE. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Auto-updated Homebrew!
Updated 3 taps (surrealdb/tap, homebrew/services and pulumi/tap).

You have 89 outdated formulae installed.

==> Fetching lcov
==> Downloading https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles//lcov-2.1.arm64_sequoia.bottle.tar.gz
########################################################################################################################################################## 100.0%
==> Pouring lcov-2.1.arm64_sequoia.bottle.tar.gz
🍺  /opt/homebrew/Cellar/lcov/2.1: 64 files, 1.9MB
==> Running `brew cleanup lcov`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

genhtml --version
genhtml: LCOV version 2.1-1
使用lcov 包中的工具genhtml生成 HTML 报告
simple_storage/coverage on  main [?] via 🅒 base 
➜ genhtml -o coverage_report coverage.lcov         
Reading tracefile coverage.lcov.
Found 2 entries.
Found common filename prefix "/Users/qiaopengjun/Code/starknet-code/hello_starknet/simple_storage"
Generating output.
Processing file src/simple_storage.cairo
  lines=14 hit=14 functions=7 hit=7
Processing file tests/test_contract.cairo
  lines=6 hit=6 functions=2 hit=2
Overall coverage rate:
  source files: 2
  lines.......: 100.0% (20 of 20 lines)
  functions...: 100.0% (9 of 9 functions)
Message summary:
  no messages were reported

simple_storage/coverage on  main [?] via 🅒 base 
➜ ls -l
total 8
-rw-r--r--   1 qiaopengjun  staff  2081 Sep 30 21:57 coverage.lcov
drwxr-xr-x  15 qiaopengjun  staff   480 Sep 30 22:15 coverage_report
在浏览器中打开coverage_report目录中的index.html 文件查看生成的覆盖率报告

image-20240930221912285.png

注意:如果是VSCodeCursor 可以安装 View In Browser 插件

image-20240930222247480.png

然后在index.html 文件中右键点击选择 View In Browser 打开, 即可在浏览器中查看

image-20240930222847819.png

浏览器中查看

image-20240930223030871.png

总结

通过本次实操,我们完成了在 Starknet 上创建、编写和测试一个 SimpleStorage 智能合约的整个过程。该合约展示了如何使用 Cairo 语言实现基本的存储和访问控制功能。在 Starknet 的开发中,保持清晰的代码结构和良好的测试实践是至关重要的。本文为开发者提供了一个完整的开发流程指南,展示了如何使用Cairo语言编写简单的存储合约。通过本教程,您已经学习了如何在Starknet上使用Cairo编写、构建和测试智能合约。您可以基于此合约继续扩展功能,或者探索更复杂的合约逻辑。希望您能在Starknet开发中获得成功!

参考

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
寻月隐君
寻月隐君
0x750E...B6f5
不要放弃,如果你喜欢这件事,就不要放弃。如果你不喜欢,那这也不好,因为一个人不应该做自己不喜欢的事。