Starknet智能合约开发实战:Counter合约编写、测试与部署全流程在Starknet生态中,掌握从代码编写到链上部署的完整闭环是开发者的必经之路。本文将通过一个经典的Counter(计数器)合约,带大家走一遍完整的智能合约开发全流程。我们将使用Scarb进行项目管理,利用
在 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... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!