Solana区块链智能合约开发简要流程

Posted MateZero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solana区块链智能合约开发简要流程相关的知识,希望对你有一定的参考价值。

Solana区块链智能合约开发简要流程

Solana区块链是当今市值第5的区块链,已经有很多知名生态准备部署在Solana上。相比于类以太坊(EVM)区块链来讲,Solana上智能合约开发(叫Program)存在一定的门槛,因为Solana通常使用系统程序语言Rust进行Program开发而不是使用特定领域语言(例如Solidity)进行开发,学习曲线较为陡峭。另外,Solana上一些基础概念同当今流利的EVM区块链并不相同,习惯了以太坊区块链的开发者会有一个适应期。

幸好,Solana的基础开发者已经写了一篇很详细的教学文章,上面对Solana的区块链基础知识也有介绍。这里给出链接

Programming on Solana - An Introduction 。强烈推荐Solana上的开发者读一下。

本文也是基于该教学文章写的一篇开发流程的总结文章,这里再次感觉该文章的作者: paulx 。

一、准备工作

  • 安装最新的Rust

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    

    安装完成后可以运行rustc -V查看安装的版本。

  • 安装最新的Solana开发工具

    sh -c "$(curl -sSfL https://release.solana.com/v1.9.1/install)"
    

    安装完成后我们可以运行solana -V查看安装的版本。

二、新建Rust工程

  • cargo new escrow --lib

  • 新建Xargo.toml,内容为:

    [target.bpfel-unknown-unknown.dependencies.std]
    features = []
    
  • 编辑Cargo.toml,添加常用依赖,并且设定no-entrypoint特性,示例如下:

    [package]
    name = "escrow"
    version = "0.1.0"
    edition = "2021"
    
    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
    
    [features]
    no-entrypoint = []
    
    [dependencies]
    arrayref = "0.3.6"
    solana-program = "1.7.11"
    thiserror = "1.0"
    
    [lib]
    crate-type = ["cdylib", "lib"]
    

三、托管合约的主要流程

我们学习的教学文章为一个托管合约,主要是解决交易中的去信任问题。假定Alice和Bob需要相互交易资产,谁都不想首先发送资产,怕另一方拿钱就跑,这样就会形成一个死节。传统的解决方式是找一个可信的第三方,将资产交易第三方进行交易。然而,此处还是不完全可信的,因为第三方也许会和其中一方勾结。

而在区块链,智能合约就是天然的可信第三方。因为智能合约对双方都可见,所以是可信的。又因为智能合约是程序,是按既定编码执行的,不会掺杂其它因素,所以不会发生勾结问题。

这里补充一点点:上面那一段话在Solana上并不是完全适用。首先,Solana合约是可以升级的(虽然也可以关闭掉升级功能);其次,在Solana上还并未有浏览器开源验证这个功能,我们可能无法保证部署的合约就是我们看到的合约。

在本托管合约中,假定Alice要将资产(代币)X 交换为Bob的代币Y,它需要创建一个临时资产账号用来存放交易的X,并将这个X的所有权转给托管合约,同时设定交换得到的Y的数量。当Bob发起交易时,将相应数量的Y发送到Alice的账户,并且得到Alice存放在临时账号中的X。

注意:在Solana区块链中,智能合约是无状态的,不能保存任何数据。所有需要保存的数据均保存在账号的data字段中。

另外:关于Spl-token及账号相关的一些基础知识这里无法简单解释清楚,请读者自行阅读相应文章或者源教学文章。

我们计划只实现了其第一步的代码,Alice初始化一个交易账号并将自己的保存临时资产X的账号的所有权转给这个交易账号。完整实现请看源教学文章。

四、编写基本框架

基础设置已经有了,下面开始编写代码。如果我们先从主干(程序入口)编写起,那么你会遇到很多红色波浪线错误提示,所以这里我们先编写基本的枝叶,再用主干将它们串起来。

4.1、lib.rs

使用Cargo 新建Rust工程时,src/lib.rs已经帮我们建好了,我们只需要往里面添加内容就行了,可以忽略那个单元测试。

#[cfg(test)]
mod tests 
    #[test]
    fn it_works() 
        let result = 2 + 2;
        assert_eq!(result, 4);
    

4.2、error.rs

我们首先进行错误处理相关内容的编写,在src目录下新建error.rs,内容如下:

use thiserror::Error;
use solana_program::program_error::ProgramError;

#[derive(Error,Debug,Copy,Clone)]
pub enum EscrowError 
    // Invalid instruction
    #[error("Invalid Instruction")]
    InvalidInstruction,


impl From<EscrowError> for ProgramError 
    fn from(e: EscrowError) -> Self 
        ProgramError::Custom(e as u32)
    

注意:这里使用thiserror的原因原文中写的很明确,省去我们手动实现相关Trait。

最后手动实现了从EscrowError到ProgramError转换,因为Solana程序通常返回的为ProgramError。

编写完成后修改lib.rs,注册error模块。例如在第一行添加pub mod error;

4.3、instruction.rs

在相同目录下创建instruction.rs,我们先编写一个初始化指令。同时需要编写unpack 函数,用来将输入数据解析为一个指令。

以后再添加新的指令后继续在unpack函数中添加相应内容。注意unpack 函数返回的是一个 Result<Self, ProgramError>

use std::convert::TryInto;
use crate::error::EscrowError::InvalidInstruction;
use solana_program::program_error::ProgramError;

pub enum EscrowInstruction 
    /// 因为要在初始化里转移临时代币账号所有权,所以需要原owner签名,并且原owner也是初始化者
    /// 0. `[signer]` The account of the person initializing the escrow
    /// 1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
    /// 2. `[]` The initializer's token account for the token they will receive should the trade go through
    /// 3. `[writable]` The escrow account, it will hold all necessary info about the trade.
    /// 4. `[]` The rent sysvar
    /// 5. `[]` The token program
    InitEscrow 
        /// The amount party A expects to receive of token Y
        amount: u64
    


impl EscrowInstruction 

    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> 
        let (tag, rest) = input.split_first().ok_or(InvalidInstruction)?;
        
        Ok(match tag 
            0 => Self::InitEscrow 
                amount: Self::unpack_amount(rest)?,
            ,
            //注意这里的用法,InvalidInstruction转化为ProgramError时,使用了into
          	//因为我们在error.rs中已经实现了那个from,系统会自动帮我们实现into
            _ => return Err(InvalidInstruction.into()),
        )
    

    //这里学习Input 转化为u64
    fn unpack_amount(input: &[u8]) -> Result<u64, ProgramError> 
        let amount = input
            .get(..8)
            .and_then(|slice| slice.try_into().ok())
            .map(u64::from_le_bytes)
            .ok_or(InvalidInstruction)?;
        Ok(amount)
    

编写完成后记得在lib.rs中注册instruction模块

4.4、processor.rs

相同目录下创建processor.rs

注意:这里一般为固定的Processor结构体(只是约定,无强制力)。在该结构体上创建一个静态函数process来处理入口转发过来的参数。在该函数内部,首先解析指令,然后根据指令调用相应的处理函数。

注意:

  1. 它返回的是ProgramResult。
  2. 函数体中"?"操作符的使用,向上级调用传递错误。
use solana_program::
    account_info::next_account_info,AccountInfo,
    entrypoint::ProgramResult,
    program_error::ProgramError,
    msg,
    pubkey::Pubkey,
;
use crate::instruction::EscrowInstruction;

pub struct Processor;

impl Processor 
    pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult 
        let instruction = EscrowInstruction::unpack(instruction_data)?;
        
        match instruction 
            EscrowInstruction::InitEscrow amount => 
                msg!("Instruction: InitEscrow");
                Self::process_init_escrow(accounts,amount,program_id)
            
        
    

    fn process_init_escrow(
        accounts: &[AccountInfo],
        amount: u64,
        program_id: &Pubkey
    ) -> ProgramResult 
        let account_info_iter = &mut accounts.iter();
        let initializer = next_account_info(account_info_iter)?;
        if !initializer.is_signer 
            return Err(ProgramError::MissingRequiredSignature);
        
				// todo
        Ok(())
    

这里的process_init_escrow函数并没有编写完全。

别忘记在lib.rs中注册processor模块。

4.5、entrypoint.rs

相同目录下创建entrypoint.rs作为程序的入口,注意使用entrypoint宏来指定入口函数。

//! Program entrypoint

use crate::processor::Processor;
use solana_program::
    account_info::AccountInfo, 
    entrypoint, 
    entrypoint::ProgramResult,
    pubkey::Pubkey,
;

entrypoint!(process_instruction);
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult 
    Processor::process(program_id, accounts, instruction_data) 

lib.rs中注册entrypoint模块,为了以后我们的程序能方便的被别的程序导入,此时需要设定可关闭entrypoint特性。(这里原文中也只是指出了方法,是参考spl-token中的设置和编写而来的)。

#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;

4.6、state.rs

相同目录创建state.rs,文件用来定义状态保存对象并编写相应的程序处理序列化和反序列化(也就是将字节数组和数据结构相互转换)。

use solana_program::
    program_pack::IsInitialized, Pack, Sealed,
    program_error::ProgramError,
    pubkey::Pubkey,
;


pub struct Escrow 
    pub is_initialized: bool,
    pub initializer_pubkey: Pubkey,
    pub temp_token_account_pubkey: Pubkey,
    pub initializer_token_to_receive_account_pubkey: Pubkey,
    pub expected_amount: u64,


impl Sealed for Escrow 

impl IsInitialized for Escrow 
    fn is_initialized(&self) -> bool 
        self.is_initialized
    


use arrayref::array_mut_ref, array_ref, array_refs, mut_array_refs;

impl Pack for Escrow 
    const LEN: usize = 105;
    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> 
        let src = array_ref![src, 0, Escrow::LEN];
        let (
            is_initialized,
            initializer_pubkey,
            temp_token_account_pubkey,
            initializer_token_to_receive_account_pubkey,
            expected_amount,
        ) = array_refs![src, 1, 32, 32, 32, 8];
        let is_initialized = match is_initialized 
            [0] => false,
            [1] => true,
            _ => return Err(ProgramError::InvalidAccountData),
        ;

        Ok(Escrow 
            is_initialized,
            initializer_pubkey: Pubkey::new_from_array(*initializer_pubkey),
            temp_token_account_pubkey: Pubkey::new_from_array(*temp_token_account_pubkey),
            initializer_token_to_receive_account_pubkey: Pubkey::new_from_array(*initializer_token_to_receive_account_pubkey),
            expected_amount: u64::from_le_bytes(*expected_amount),
        )
    

    fn pack_into_slice(&self, dst: &mut [u8]) 
        let dst = array_mut_ref![dst, 0, Escrow::LEN];
        let (
            is_initialized_dst,
            initializer_pubkey_dst,
            temp_token_account_pubkey_dst,
            initializer_token_to_receive_account_pubkey_dst,
            expected_amount_dst,
        ) = mut_array_refs![dst, 1, 32, 32, 32, 8];

        let Escrow 
            is_initialized,
            initializer_pubkey,
            temp_token_account_pubkey,
            initializer_token_to_receive_account_pubkey,
            expected_amount,
         = self;

        is_initialized_dst[0] = *is_initialized as u8;
        initializer_pubkey_dst.copy_from_slice(initializer_pubkey.as_ref());
        temp_token_account_pubkey_dst.copy_from_slice(temp_token_account_pubkey.as_ref());
        initializer_token_to_receive_account_pubkey_dst.copy_from_slice(initializer_token_to_receive_account_pubkey.as_ref());
        *expected_amount_dst = expected_amount.to_le_bytes();
    

这里需要注意的有:

  • 我们的结构需要实现program_pack::IsInitialized, Pack, Sealed 这三个特型。
  • const LEN: usize = 105;这里结构的大小是根据各个字段的大小相加得到的,分别为1 + 32*3 + 8 = 105
  • unpack_from_slicepack_into_slice并不是直接被程序的其它部分调用的,Pack特型有两个默认函数,分别调用这两个函数。
  • 注意array_mut_ref, array_ref, array_refs, mut_array_refs这几个宏的用法,看名字就能猜到,分别为得到一个数组的可变引用,得到一个数组的引用 ,得到多个数组的引用,得到多个数组的可变引用。
  • 注意示例中从字节数组得到公钥的方法copy_from_slice
  • 示例中从字节数组得到u64采用了to_le_bytes左对齐的方式,Rust中还有类似的右对齐方式。但一般Solana中采用类C的左对齐方式。
  • 布尔值可以直接转换为u8,见*is_initialized as u8

最后注册state模块,同时删除单元测试的内容,此时整个lib.rs为:

pub mod error;
pub mod instruction;
pub mod state;
pub mod processor;
#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;

4.7、继续完成processor.rs

我们接下来继续完成processor.rs,因为我们要转代币账号所有权,需要调用spl-token的相关函数生成指令,所以我们需要在Cargo.toml中添加相关依赖。

spl-token = version = "3.1.1", features = ["no-entrypoint"]

接下来在process_init_escrow函数中补充如下片断:

...
fn process_init_escrow(
    accounts: &[AccountInfo],
    amount: u64,
    program_id: &Pubkey,
) -> ProgramResult 
    let account_info_iter = &mut accounts.iter();
    let initializer = next_account_info(account_info_iter)?;

    if !initializer.is_signer 
        return Err(ProgramError::MissingRequiredSignature);
    

    let temp_token_account = next_account_info(account_info_iter)?;

    let token_to_receive_account = next_account_info(account_info_iter)?;
    if *token_to_receive_account.owner != spl_token::id() 
        return Err(ProgramError::IncorrectProgramId);
    
  
    let escrow_account = next_account_info(account_info_iter)?;
    let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?;

    if !rent.is_exempt(escrow_account.lamports(), escrow_account.data_len()) 
        return Err(EscrowError::NotRentExempt.into());
    

    let mut escrow_info = Escrow::unpack_unchecked(&escrow_account.try_borrow_data()?)?;
    if escrow_info.is_initialized() 
        return Err(ProgramError::AccountAlreadyInitialized);
    
  
    escrow_info.is_initialized = true;
    escrow_info.initializer_pubkey = *initializer.key;
    escrow_info.temp_token_account_pubkey = *temp_token_account.key;
    escrow_info.initializer_token_to_receive_account_pubkey = *token_to_receive_account.key;
    escrow_info.expected_amount = amount;

    Escrow::pack(escrow_info, &mut escrow_account.try_borrow_mut_data()?)?;
		let (pda, _bump_seed) = Pubkey::find_program_address(&[b"escrow"], program_id);
  
    let token_program = next_account_info(account_info_iter)?;
    let owner_change_ix = spl_token::instruction::set_authority(
        token_program.key,
        temp_token_account.key,
        Some(&pda),
        spl_token::instruction::AuthorityType::AccountOwner,
        initializer.key,
        &[&initializer.key],
    )?;

    msg!("Calling the token program to transfer token account ownership...");
    invoke(
        &owner_change_ix,
        &[
            temp_token_account.clone(),
            initializer.clone(),
            token_program.clone(),
        ],
    )?;
    Ok(())

...

上面的代码主要添加的功能有:

  1. 验证那个用来接收代币的账号是否存在
  2. 用来验证交易账号是否免租金(这里请阅读相关文章了解租金免除的概念)
  3. 用来验证交易账号未初始化过(也就是只能初始化一次)。
  4. 将交易账号的保存的数据初始化并写回区块链(见 Escrow::pack 函数)
  5. 转让临时代币账号的所有权

同时修改

use crate::instruction::EscrowInstruction;

use crate::instruction::EscrowInstruction, error::EscrowError, state::Escrow;

并且将最开始的导入语句替换为:

use solana_program::
    account_info::next_account_info, AccountInfo,
    entrypoint::ProgramResult,
    program_error::ProgramError,
    msg,
    pubkey::Pubkey,
    program_pack::Pack, IsInitialized,
    sysvar::rent::Rent, Sysvar,
    program::invoke
;

4.8、在error.rs中添加新

以上是关于Solana区块链智能合约开发简要流程的主要内容,如果未能解决你的问题,请参考以下文章

Solana区块链智能合约开发简要流程

区块链干货:最佳的智能合约开发工具总结

Solana Rust 智能合约如何获得区块高度或 Unix 时间?

区块链 | 预言机从零开始使用Chainlink预言机- 智能合约中使用更安全的随机数-代码实战

区块链app软件开发公司_区块链挖矿软件开发多少钱?

区块链DAPP智能合约系统开发