Rust编写Near智能合约

先决条件

  • Rust(安装指南,如果您想了解有关 Rust 的更多信息,请在此处查看本指南)
  • NEAR CLI(安装指南
  • NEAR Testnet 帐户(如果您没有 testnet 帐户,请在此处查看本指南)

设置

要设置我们的项目,我们需要将 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>,
}

pairsUnorderedMap中在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_idfido.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 检查是否设置成功

猜你喜欢

转载自blog.csdn.net/lk2684753/article/details/119416479