先决条件
设置
要设置我们的项目,我们需要将 WASM (WebAssembly) 目标添加到我们的工具链中。要添加,我们需要在终端中运行以下命令:
rustup target add wasm32-unknown-unknown
终端输出:
info: downloading component 'rust-std' for 'wasm32-unknown-unknown'
info: installing component 'rust-std' for 'wasm32-unknown-unknown'
info: using up to 500.0 MiB of RAM to unpack components 13.9 MiB / 13.9 MiB (100 %) 10.0 MiB/s in 1s ETA: 0s
如果目标已经添加,则终端中的输出将是:
info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date
什么是 Rust 工具链?工具链是编译 Rust 应用程序所需的程序集合的特定版本。
为什么我们需要添加 WASM 目标?要在 NEAR 上部署我们的智能合约,我们需要将其编译为 WebAssembly(文件)。上面的命令为 WebAssembly 目标三元组 (wasm32-unknown-unknown) 安装标准库。.wasm``rustup
在docs上rustup
阅读有关交叉编译的更多信息。
现在,让我们创建一个名为key_value_storage
的目录,然后切换到该目录并在终端中运行以下命令:
cargo init --lib
输出:
Created library package
修改默认的 Cargo.toml
文件,将下列代码粘贴:
[package]
name = "key_value_storage"
version = "0.1.0"
authors = ["Your Name <Your Email>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
near-sdk = "3.1.0"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
编写智能合约
删除之前lib.rs代码,将下面代码粘贴过去
我们将在 Rust 中创建一个简单的创建、读取、更新、删除 ( CRUD ) 后端,它利用 NEAR 提供的链上存储。
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};
use near_sdk::collections::UnorderedMap;
near_sdk::setup_alloc!();
// 1. Main Struct
// 2. Default Implementation
// 3. Core Logic
// 4. Tests
在合约的顶部,我们需要导入一些带有use
声明的代码模块。我们将在下面的这些部分进行扩展。near_sdk
接下来,我们使用宏从crate设置全局分配器。分配器是 Rust 中的程序在运行时从系统获取内存的方式。是为 WebAssembly 设计的内存分配器。它生成不到 1 KB 的未压缩 WebAssembly 代码。
#[cfg(target_arch = "wasm32")]
#[global_allocator]
static ALLOC: near_sdk::wee_alloc::WeeAlloc<'_> = near_sdk::wee_alloc::WeeAlloc::INIT;
Main Struct
在编写我们的智能合约时,我们将遵循一种使用一个结构 ( ) 和与之相关的实现 ( ) 的模式。这是 NEAR 上大多数 Rust 合约中使用的模式。在注释下添加以下代码段中:// 1. Main Struct
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct KeyValue {
pairs: UnorderedMap<String, String>,
}
pairs
是UnorderedMap
中在near_sdk::collections
的一种更有效地利用底层区块链存储的数据结构。
Default implementation
在// 2. Default Implementation
中添加如下代码
impl Default for KeyValue {
fn default() -> Self {
Self {
pairs: UnorderedMap::new(b"r".to_vec())
}
}
}
Rust 中的每种类型都有一个实现,但在这里我们想为struct提供我们自己的默认实现。
Core Logic
在// 3. Core Logic
:现在我们要向struct添加方法。这些方法是我们智能合约的核心逻辑
#[near_bindgen]
impl KeyValue {
pub fn create_update(&mut self, k: String, v: String) {
env::log(b"created or updated");
self.pairs.insert(&k, &v);
}
pub fn read(&self, k: String) -> Option<String> {
env::log(b"read");
return self.pairs.get(&k);
}
pub fn delete(&mut self, k: String) {
env::log(b"delete");
self.pairs.remove(&k);
}
}
测试合约代码
我们的 CRUD 智能合约的代码现已完成。Rust 的一个很好的特性是它允许内联单元测试。这意味着我们可以在与合约相同的源文件中编写单元测试lib.rs
为什么要为智能合约编写单元测试?单元测试是软件开发中的常见做法。在编写智能合约时,单元测试很重要,因为智能合约通常是不可变的,有时负责管理资金。编写好的单元测试是安全可靠的智能合约开发的关键组成部分。
复制并粘贴在注释下下面的代码中:// 4. Tests
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
mod tests {
use super::*;
use near_sdk::MockedBlockchain;
use near_sdk::{testing_env, VMContext};
fn get_context(input: Vec<u8>, is_view: bool) -> VMContext {
VMContext {
current_account_id: "alice_near".to_string(),
signer_account_id: "bob_near".to_string(),
signer_account_pk: vec![0, 1, 2],
predecessor_account_id: "carol_near".to_string(),
input,
block_index: 0,
block_timestamp: 0,
account_balance: 0,
account_locked_balance: 0,
storage_usage: 0,
attached_deposit: 0,
prepaid_gas: 10u64.pow(18),
random_seed: vec![0, 1, 2],
is_view,
output_data_receivers: vec![],
epoch_height: 0,
}
}
// Test 1
#[test]
fn create_read_pair() {
let context = get_context(vec![], false);
testing_env!(context);
let mut contract = KeyValue::default();
contract.create_update("first_key".to_string(), "hello".to_string());
assert_eq!(
"hello".to_string(),
contract.read("first_key".to_string()).unwrap()
);
}
// Test 2
#[test]
fn read_nonexistent_pair() {
let context = get_context(vec![], true);
testing_env!(context);
let contract = KeyValue::default();
assert_eq!(None, contract.read("first_key".to_string()));
}
}
Test1和Test2分别测试了合约的两个方法
在命令行运行:
cargo test -- --nocapture
终端输出:
Finished test [unoptimized + debuginfo] target(s) in 1m 05s
Running target/debug/deps/key_value_storage-958f616e81cf3269
running 2 tests
test tests::read_nonexistent_pair ... ok
test tests::create_read_pair ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests key_value_storage
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
编译合约
设置环境变量,交叉编译合约代码
// Linux and macOS users can use these commands:
env 'RUSTFLAGS=-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
// Windows users can use these commands:
set RUSTFLAGS=-C link-arg=-s
cargo build --target wasm32-unknown-unknown --release
编译后输出:
Compiling near-sdk v3.1.0
Compiling key_value_storage v0.1.0 (/home/xqc/key_value_storage)
Finished release [optimized] target(s) in 1m 00s
现在生成了WebAssembly 文件,下面我们可以登陆账号,把合约部署在testnet上
登陆Testnet账号
使用near cli
命令登陆账号
near login
默认$NEAR_ENV就是Testnet网络
执行后命令行会弹出浏览器钱包界面,将Testnet账号助记词导入,allow授权后,命令行就有账号登陆成功的回执信息
我们使用near cli工具创建子账号,用于合约部署
near create-account CONTRACT_NAME.ACCOUNT_ID --masterAcount ACCOUNT_ID --initialBalance 10
创建完成后,会有账号保存路径信息如下:
Saving key to '/home/xxx/.near-credentials/testnet/CONTRACT_NAME.ACCOUNT_ID.json'
Account CONTRACT_NAME.ACCOUNT_ID.testnet for network "testnet" was created.
- 例如,假设您在 testnet 附近的当前account_id是fido.testnet并且您想命名合约**dodo,**那么您最终将创建以下新 account_id dodo.fido.testnet,它代表contract_id。
--initialBalance
如果省略默认为100 Near。
部署合约
CONTRACT_ID 为我们创建的子账号
near deploy --wasmFile target/wasm32-unknown-unknown/release/key_value_storage.wasm --accountId CONTRACT_ID
部署成功后,会有下面信息输出:
Starting deployment. Account id: CONTRACT_ID, node: https://rpc.testnet.near.org, helper: https://helper.testnet.near.org, file: target/wasm32-unknown-unknown/release/key_value_storage.wasm
Transaction Id E4uT8wV5uXSgsJpB73Uox27iPgXztWfL3b5gzxfA3fHo
To see the transaction in the transaction explorer, please open this url in your browser
https://explorer.testnet.near.org/transactions/E4uT8wV5uXSgsJpB73Uox27iPgXztWfL3b5gzxfA3fHo
Done deploying to CONTRACT_ID
调用合约
–accountId 你的near 账户
1.创建键值对 调用合约create_update方法
near call CONTRACT_ID create_update '{"k": "first_key", "v" : "1"}' --accountId ACCOUNT_ID
2.读取键值对 调用read方法
near view CONTRACT_ID read '{"k": "first_key"}' --accountId ACCOUNT_ID
View call: CONTRACT_ID.read({
"k": "first_key"})
Log [CONTRACT_ID]: read
'1'
3.删除键值对 调用delete方法
near call CONTRACT_ID delete '{"k": "first_key"}' --accountId ACCOUNT_ID
以上就是简单的near 合约创建 部署 调用的实例
一个投票合约
我们使用cargo创建一个vote的项目
lib.rs
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{env, near_bindgen};
use near_sdk::serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[derive(Serialize, Deserialize, Clone, BorshDeserialize, BorshSerialize)]
pub struct VotingOption {
option_id: String,
message: String,
}
#[derive(Serialize, Deserialize, Clone, BorshDeserialize, BorshSerialize)]
pub struct VotingOptions {
// Author of the vote (account id).
creator: String,
// Unique voting id.
poll_id: String,
// Question voted on.
question: String,
variants: Vec<VotingOption>,
}
#[derive(Serialize, Deserialize, Clone, BorshDeserialize, BorshSerialize)]
pub struct VotingResults {
// Unique poll id.
poll_id: String,
// Map of option id to the number of votes.
variants: HashMap<String, i32>,
// Map of voters who already voted.
voted: HashMap<String, i32>,
}
#[derive(Serialize, Deserialize)]
pub struct VotingStats {
poll: VotingOptions,
results: VotingResults,
}
#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Voting {
// Map of poll id to voting options.
polls: HashMap<String, VotingOptions>,
// Map of poll id to voting results.
results: HashMap<String, VotingResults>,
}
#[near_bindgen]
impl Voting {
pub fn vote(&mut self, poll_id: String, votes: HashMap<String, i32>) -> bool {
let voter_contract = env::signer_account_id();
let owner_contract = env::current_account_id();
env::log(
format!(
"{} is voting on {} owner is {}",
voter_contract, poll_id, owner_contract
)
.as_bytes(),
);
// Now we need to find a contract to vote for.
match self.results.get_mut(&poll_id) {
Some(results) => {
match results.voted.get(&voter_contract) {
Some(_) => {
env::log(
format!("{} already voted in {}", voter_contract, poll_id).as_bytes(),
);
return false;
}
None => {
results.voted.insert(voter_contract, 1);
}
}
for (vote, checked) in votes.iter() {
if *checked == 0 {
continue;
}
match results.variants.get_mut(vote) {
Some(result) => {
*result = *result + 1;
}
None => {
results.variants.insert(vote.to_string(), 1);
}
}
}
return true;
}
None => {
env::log(format!("no poll known for {}", poll_id).as_bytes());
return false;
}
};
}
pub fn create_poll(&mut self, question: String, variants: HashMap<String, String>) -> String {
env::log(
format!(
"create_poll for {} currently have {} polls",
question,
self.polls.len()
)
.as_bytes(),
);
let creator_account_id = env::signer_account_id();
let poll_id = bs58::encode(env::sha256(&env::random_seed())).into_string();
let result = poll_id.clone();
let mut variants_vec = <Vec<VotingOption>>::new();
for (k, v) in variants.iter() {
variants_vec.push(VotingOption {
option_id: k.to_string(),
message: v.to_string(),
})
}
self.polls.insert(
poll_id.clone(),
VotingOptions {
creator: creator_account_id,
poll_id: poll_id.clone(),
question: question,
variants: variants_vec,
},
);
self.results.insert(
poll_id.clone(),
VotingResults {
poll_id: poll_id,
variants: HashMap::new(),
voted: HashMap::new(),
},
);
return result;
}
pub fn show_poll(&self, poll_id: String) -> Option<VotingOptions> {
match self.polls.get(&poll_id) {
Some(options) => Some(options.clone()),
None => {
env::log(format!("Unknown voting {}", poll_id).as_bytes());
None
}
}
}
pub fn show_results(&self, poll_id: String) -> Option<VotingStats> {
match self.polls.get(&poll_id) {
Some(poll) => match self.results.get(&poll_id) {
Some(results) => Some(VotingStats {
results: results.clone(),
poll: poll.clone(),
}),
None => None,
},
None => None,
}
}
pub fn ping(&self) -> String {
"PONG".to_string()
}
}
cargo.toml:
[package]
name = "vote"
version = "0.2.0"
authors = ["Near lk2684753"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
# serde = { version = "1.0", features = ["derive"] }
# serde_json = { git = "https://github.com/nearprotocol/json", rev = "1f5779f3b0bd3d2a4b0b975abc46f3d3fe873331", features = ["no_floats"] }
# near-bindgen = "0.6.0"
# borsh = "*"
# wee_alloc = "0.4.5"
serde = { version = "*", features = ["derive"] }
serde_json = "*"
borsh = "*"
near-sdk = "2.0.0"
wee_alloc = { version = "0.4.5", default-features = false, features = [] }
bs58 = "0.3"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []
按照上述讲的将合约部署在testnet网络
部分调用方法实例
#投票
near call lkvote.$ID vote '{"poll_id":"f1bVk4smE3t55qPCt9gASy9BKNr1wmj7ifqwXNEALrH","votes":{"0":1}}' --accountId $ID
#投票内容
near call lkvote.$ID show_poll '{"poll_id":"f1bVk4smE3t55qPCt9gASy9BKNr1wmj7ifqwXNEALrH"}' --accountId lk26847853.testnet
#投票目前结果
near call lkvote.$ID show_results '{"poll_id":"f1bVk4smE3t55qPCt9gASy9BKNr1wmj7ifqwXNEALrH"}' --accountId $ID
$ID可通过环境变量设置
ID=lk26847853.testnet
echo $ID 检查是否设置成功