Starknet 智能合约开发实战:Counter 合约编写、测试与部署全流程

Starknet智能合约开发实战:Counter合约编写、测试与部署全流程在Starknet生态中,掌握从代码编写到链上部署的完整闭环是开发者的必经之路。本文将通过一个经典的Counter(计数器)合约,带大家走一遍完整的智能合约开发全流程。我们将使用Scarb进行项目管理,利用

Starknet 智能合约开发实战:Counter 合约编写、测试与部署全流程

在 Starknet 生态中,掌握从代码编写到链上部署的完整闭环是开发者的必经之路。

本文将通过一个经典的 Counter(计数器)合约,带大家走一遍完整的智能合约开发全流程。我们将使用 Scarb 进行项目管理,利用 Starknet Foundry (snforge) 编写严谨的测试用例,并最终通过 sncast 完成合约的声明与部署。无论你是 Cairo 初学者,还是希望熟悉最新工具链的开发者,本文都将为你提供一份详尽的实战参考。

实操

前提

scarb --version
snforge --version && sncast --version
starknet-devnet --version
scarb 2.15.0 (56d7d30fb 2025-12-19)
cairo: 2.15.0 (https://crates.io/crates/cairo-lang-compiler/2.15.0)
sierra: 1.7.0
arch: aarch64-apple-darwin
snforge 0.54.1
sncast 0.54.1
starknet-devnet 0.7.1

创建项目

scarb new counter
✔ Which test runner do you want to set up? · Starknet Foundry (default)
 Downloading snforge_scarb_plugin v0.54.1
 Downloading snforge_std v0.54.1
Created `counter` package.

切换到项目目录

cd counter

查看项目目录结构

tree . -L 6 -I "docs|target|node_modules|build"
.
├── Scarb.lock
├── Scarb.toml
├── snfoundry.toml
├── src
│   ├── counter.cairo
│   ├── hello_starknet.cairo
│   └── lib.cairo
└── tests
    └── test_contract.cairo

3 directories, 7 files

实现合约

counter.cairo文件

#[starknet::interface]
pub trait ICounter<TContractState> {
    fn increment(ref self: TContractState);
    fn get_count(self: @TContractState) -> u32;
}

#[starknet::contract]
mod Counter {
    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};

    #[storage]
    struct Storage {
        count: u32,
    }

    #[constructor]
    fn constructor(ref self: ContractState, initial_count: u32) {
        self.count.write(initial_count);
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    pub enum Event {
        CountIncremented: CountIncremented,
    }

    #[derive(Drop, starknet::Event)]
    pub struct CountIncremented {
        count: u32,
    }

    #[abi(embed_v0)]
    impl CounterImpl of super::ICounter<ContractState> {
        fn increment(ref self: ContractState) {
            let new_count = self.count.read() + 1;
            self.count.write(new_count);

            self.emit(CountIncremented { count: new_count });
            // self.emit(X) 只要 X 能被“提升”为合约的 Event,就等价于 self.emit(Event::X(...))
            // self.emit(Event::CountIncremented(CountIncremented { count: new_count }));
        }

        fn get_count(self: @ContractState) -> u32 {
            self.count.read()
        }
    }
}

hello_starknet.cairo文件

/// Interface representing `HelloContract`.
/// This interface allows modification and retrieval of the contract balance.
#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    /// Increase contract balance.
    fn increase_balance(ref self: TContractState, amount: felt252);
    /// Retrieve contract balance.
    fn get_balance(self: @TContractState) -> felt252;
}

/// Simple contract for managing balance.
#[starknet::contract]
mod HelloStarknet {
    use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};

    #[storage]
    struct Storage {
        balance: felt252,
    }

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn increase_balance(ref self: ContractState, amount: felt252) {
            assert(amount != 0, 'Amount cannot be 0');
            self.balance.write(self.balance.read() + amount);
        }

        fn get_balance(self: @ContractState) -> felt252 {
            self.balance.read()
        }
    }
}

lib.cairo文件

pub mod hello_starknet;
pub mod counter;

编写测试

test_contract.cairo文件


use starknet::ContractAddress;

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

use counter::hello_starknet::IHelloStarknetSafeDispatcher;
use counter::hello_starknet::IHelloStarknetSafeDispatcherTrait;
use counter::hello_starknet::IHelloStarknetDispatcher;
use counter::hello_starknet::IHelloStarknetDispatcherTrait;
use counter::counter::ICounterSafeDispatcher;
use counter::counter::ICounterSafeDispatcherTrait;
use counter::counter::ICounterDispatcher;
use counter::counter::ICounterDispatcherTrait;

fn deploy_contract(name: ByteArray) -> ContractAddress {
    let contract = declare(name).unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
    contract_address
}

fn deploy_contract_with_initial_count(name: ByteArray, initial_count: u32) -> ContractAddress {
    let contract = declare(name).unwrap().contract_class();
    let constructor_calldata = array![initial_count.into()];
    let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
    contract_address
}

#[test]
fn test_increase_balance() {
    let contract_address = deploy_contract("HelloStarknet");

    let dispatcher = IHelloStarknetDispatcher { contract_address };

    let balance_before = dispatcher.get_balance();
    assert(balance_before == 0, 'Invalid balance');

    dispatcher.increase_balance(42);

    let balance_after = dispatcher.get_balance();
    assert(balance_after == 42, 'Invalid balance');
}

#[test]
#[feature("safe_dispatcher")]
fn test_cannot_increase_balance_with_zero_value() {
    let contract_address = deploy_contract("HelloStarknet");

    let safe_dispatcher = IHelloStarknetSafeDispatcher { contract_address };

    let balance_before = safe_dispatcher.get_balance().unwrap();
    assert(balance_before == 0, 'Invalid balance');

    match safe_dispatcher.increase_balance(0) {
        Result::Ok(_) => core::panic_with_felt252('Should have panicked'),
        Result::Err(panic_data) => {
            assert(*panic_data.at(0) == 'Amount cannot be 0', *panic_data.at(0));
        }
    };
}

#[test]
fn test_increment_count() {
    let contract_address = deploy_contract_with_initial_count("Counter", 10);

    let dispatcher = ICounterDispatcher { contract_address };

    let count_before = dispatcher.get_count();
    assert(count_before == 10, 'Invalid count');

    dispatcher.increment();

    let count_after = dispatcher.get_count();
    assert...

剩余50%的内容订阅专栏后可查看

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

0 条评论

请先 登录 后评论