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中添加新的error类型

#[error("NotRentExempt")]
NotRentExempt,

至此,我们第一部分的代码就算编写完毕。

五、在本地编译部署

  1. 编译合约,打开终端切换到项目根目录,运行cargo build-bpf --manifest-path=./Cargo.toml --bpf-out-dir=dist/program并忽视那些警告(那是下一步使用的)。编译完成后会给出部署命令。

  2. 启动本地节点。打开一个终端运行solana-test-validator启动本地节点。

  3. 进行本地配置。另外打开一个终端,运行solana config get看是否指向了本地节点,如果不是,运行solana config set --url http://localhost:8899 进行设置。然后运行solana balance,你会发现你拥有 500000000 个SOL。-_- !!!

  4. 运行编译时给出的部署命令:

    solana program deploy ..../escrow/target/deploy/escrow.so
    

    最后得到一个程序ID,需要记下来,例如我们的为:HEptwBGd4ShMYP6vNCE6vsDmuG3bGzQCcRPHfapvNeys

六、编写测试脚本

6.1、预备工作

在正式测试我们的合约之前,我们还有许多预备工作要做,主要有:

1、创建Alice账号并领取空投SOL作为手续费
2、部署spl-token合约,这个已经默认包含在本地节点了,地址为:TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA。
3、部署spl-associated-token-account合约,默认已有,地址为:ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
4、发行X和Y两种代币。
5、创建Alice在X代币和Y代币的账号(主账号,这里使用唯一的代币关联地址)。
6、给Alice增发足够数量的X代币进行测试

回到工程根目录,运行yarn init。然后新建test目录,在test目录里创建prepair.js.代码如下:

const {
    Keypair,
    Transaction,
    LAMPORTS_PER_SOL,
    Connection,
    sendAndConfirmTransaction
} = require('@solana/web3.js');
const {Token,ASSOCIATED_TOKEN_PROGRAM_ID,TOKEN_PROGRAM_ID} = require('@solana/spl-token');

const rpcUrl = "http://localhost:8899 ";
const connection = new Connection(rpcUrl, 'confirmed');
const initSupplyTokenX = 100000000;

async function prepair() {
    //创建Alice账号领取空投
    const alice = Keypair.generate()
    signer = alice;
    console.log(showWallet(alice))
    let  aliceAirdropSignature = await connection.requestAirdrop(
        alice.publicKey,
        LAMPORTS_PER_SOL,
    );
    await connection.confirmTransaction(aliceAirdropSignature);
    let lamports = await connection.getBalance(alice.publicKey);
    console.log("Alice lamports:",lamports)
    //发行代币X
    let tokenX = await Token.createMint(
        connection,
        alice,
        alice.publicKey,
        null,
        9,
        TOKEN_PROGRAM_ID
    )
    console.log("tokenX:",tokenX.publicKey.toBase58())
    //创建Alice的X代币关联账号并且增发代币
    let alice_x = await createAssociatedAccout(tokenX,alice.publicKey,alice,true)
    let info = await tokenX.getAccountInfo(alice_x,"confirmed")
    info.owner = info.owner.toBase58()
    info.mint = info.mint.toBase58()
    info.address = info.address.toBase58()
    console.log("alice_x:",info)
    //创建tokenY
    let tokenY = await Token.createMint(
        connection,
        alice,
        alice.publicKey,
        null,
        9,
        TOKEN_PROGRAM_ID
    )
    console.log("tokenY:",tokenY.publicKey.toBase58())
    //创建alice在tokenY的关联账号
    let alice_y = await createAssociatedAccout(tokenY,alice.publicKey,alice,false)
    console.log("alice_y_publicKey:",alice_y.toBase58())
}

//创建关联地址并增发代币
async function createAssociatedAccout(tokenObj,owner,signer,isMint) {
    //第一步,计算关联地址
    let associatedAddress = await getAssociatedTokenAddress(
        TOKEN_PROGRAM_ID,
        tokenObj.publicKey,
        owner
    )
    //第二步 创建关联账号(此时ASSOCIATED_TOKEN_PROGRAM会自动进行初始化)
    let transaction = new Transaction()
    transaction.add(
        Token.createAssociatedTokenAccountInstruction(
          ASSOCIATED_TOKEN_PROGRAM_ID,
          TOKEN_PROGRAM_ID,
          tokenObj.publicKey,
          associatedAddress,
          owner,
          signer.publicKey,
        )
    );
    // 第三步 增发代币
    if(isMint) {
        transaction.add(
            Token.createMintToInstruction(
              TOKEN_PROGRAM_ID,
              tokenObj.publicKey,
              associatedAddress,  //注意这里是给关联地址增发
              owner,
              [],
              initSupplyTokenX,
            )
        )
    }
    // 第四步 发送交易
    await sendAndConfirmTransaction(
        connection,
        transaction,
        [signer]
    )
    return associatedAddress
}

async function getAssociatedTokenAddress(programId,mint,account) {
    let newAccount = await Token.getAssociatedTokenAddress(
        ASSOCIATED_TOKEN_PROGRAM_ID, //关联地址固定公钥
        programId,      // 代币合约公钥
        mint,            //mint(代币)标识/公钥
        account,            //玩家主账号 公钥
    )
    return newAccount
}

function showWallet(wallet) {
    let result = [wallet.publicKey.toBase58(),Buffer.from(wallet.secretKey).toString("hex")]
    return result
}

prepair().then(() => console.log("over"))

回到项目根目录,然后我们运行下面程序安装依赖:

yarn add @solana/web3.js
yarn add @solana/spl-token

最后我们运行node test/prepair.js,会得到类似如下输出:

[
  'A6Bu3xfaKFf9EoKrpviCF3K5szNcZLGJkLxPyAUqShJp',
  '49372f691baa9cb4f6d5f485e43b685adb26055cdc545728bd2ff808d0bf92ea870d687c5de0f7eac13cd6050b1c78e23345575ca4b2fc241d65705983015eb1'
]
Alice lamports: 1000000000
tokenX: FMYttGRGuYCrgqCRZLhLoUESqo9Sfe87DKdH7JLZGB6G
alice_x: {
  mint: 'FMYttGRGuYCrgqCRZLhLoUESqo9Sfe87DKdH7JLZGB6G',
  owner: 'A6Bu3xfaKFf9EoKrpviCF3K5szNcZLGJkLxPyAUqShJp',
  amount: <BN: 5f5e100>,
  delegateOption: 0,
  delegate: null,
  state: 1,
  isNativeOption: 0,
  isNative: false,
  delegatedAmount: <BN: 0>,
  closeAuthorityOption: 0,
  closeAuthority: null,
  address: '6fBN3uzsDKfG2nDLnpP4NknMocQX85AB1vqCWfXbW9os',
  isInitialized: true,
  isFrozen: false,
  rentExemptReserve: null
}
tokenY: 4URCvC1YZv5mPDekabWccaAofnoMZwiDofEfwt5E4jdU
alice_y_publicKey: Bu8Heft6Lsih32Z6yaVFQqVndDtzAmJdMS8friSLb59w

上面的结果中,最上面的数组为Alice的地址和私钥,接下来是它的SQL余额(用来显示我们账号创建成功,空投了SQL来支付手续费)。

接下来是我们发行的代币X的地址。

最后alice_x为我们的Alice在代币X上的关联地址在代币合约中的相关信息。

从上面的结果可以看出,Alice的地址为 A6Bu3xfaKFf9EoKrpviCF3K5szNcZLGJkLxPyAUqShJp,所以它的X代币的账号 alice_x 的 owner也是A6Bu3xfaKFf9EoKrpviCF3K5szNcZLGJkLxPyAUqShJp。Alice_x的mint(代币类型)正好是我们发行的代币X的地址:FMYttGRGuYCrgqCRZLhLoUESqo9Sfe87DKdH7JLZGB6G

上面的结果还可以看出,Alice_x的地址为6fBN3uzsDKfG2nDLnpP4NknMocQX85AB1vqCWfXbW9os,其余额为:0x5f5e100,换算成十进制刚好为100000000,同我们的initSupplyTokenX相吻合。Alice_x的其它属性可以自己看一下猜出来。

上面的输出信息不要清除了,我们接下来还要用到,如果一不小心删除了,重新运行一下程序会得到一个新的输出。

6.2、测试托管合约初始化

在我们的托管合约的第一部分中,Alice初始化一个托管账号其实包含如下几个顺序操作:

1、创建一个被token合约拥有的空的账号
2、将这个空的账号初始化为Alice的X代币账号(临时账号)
3、Alice将她的代币X从主账号转移到临时账号
4、创建一个被托管合约拥有的空账号
5、将这个空账号初始化为交易状态账号并且将Alice的临时X代币账号转移到PDA(程序派生账号)。

ps:合约部署时的地址其实在编译后是可以拿到的,使用solana address -k .../....so就可以获取了。

在Solana中,一个交易里可以包含多个指令(prepair.js中已经有示例)并执行。

注:前两步可以利用Solana的SDK合并执行,而不是全部用一个交易执行。

test目录下创建init.js,代码如下:

const {
    Keypair,
    PublicKey,
    Transaction,
    TransactionInstruction,
    SystemProgram,
    Connection,
    SYSVAR_RENT_PUBKEY,
    sendAndConfirmTransaction
} = require('@solana/web3.js');

const {Token,TOKEN_PROGRAM_ID} = require('@solana/spl-token');
const BufferLayout = require("buffer-layout");
const BN = require("bn.js");

const rpcUrl = "http://localhost:8899 ";
const connection = new Connection(rpcUrl, 'confirmed');
//我们的托管程序地址
const escrowProgramId = new PublicKey("HEptwBGd4ShMYP6vNCE6vsDmuG3bGzQCcRPHfapvNeys")
//从私钥中恢复alice的钱包
const alice_privateKey = "49372f691baa9cb4f6d5f485e43b685adb26055cdc545728bd2ff808d0bf92ea870d687c5de0f7eac13cd6050b1c78e23345575ca4b2fc241d65705983015eb1"
const alice = Keypair.fromSecretKey(Uint8Array.from(Buffer.from(alice_privateKey, 'hex')))
//从代币X地址中恢复代币X对象
const token_x = new PublicKey("FMYttGRGuYCrgqCRZLhLoUESqo9Sfe87DKdH7JLZGB6G")
const tokenX = new Token(connection,token_x,TOKEN_PROGRAM_ID,alice)
//Alice在代币X的关联账号(公钥)
const alice_x = new PublicKey("6fBN3uzsDKfG2nDLnpP4NknMocQX85AB1vqCWfXbW9os")
const alice_y = "Bu8Heft6Lsih32Z6yaVFQqVndDtzAmJdMS8friSLb59w"

const publicKey = (property) => {
    return BufferLayout.blob(32, property);
};
  
const uint64 = (property) => {
    return BufferLayout.blob(8, property);
};
const ESCROW_ACCOUNT_DATA_LAYOUT = BufferLayout.struct([
    BufferLayout.u8("isInitialized"),
    publicKey("initializerPubkey"),
    publicKey("initializerTempTokenAccountPubkey"),
    publicKey("initializerReceivingTokenAccountPubkey"),
    uint64("expectedAmount"),
]);

const SWAP_AMOUNT = 1000;    //计划交易的X数量
const expectedAmount = 1200; //期望得到的Y数量
const Escrow_Size = ESCROW_ACCOUNT_DATA_LAYOUT.span; //105,托管合约中交易账号数据大小,其实我们在合约state.rs中已经知道大小了

async function init() {
    //创建Alice在X代币的临时账号,这里使用SDK自动帮我们创建了。
    let temp_account = await tokenX.createAccount(alice.publicKey)
    //转移X代币指令
    const transaction = new Transaction().add(
        Token.createTransferInstruction(
          TOKEN_PROGRAM_ID,
          alice_x,
          temp_account,
          alice.publicKey,
          [],
          SWAP_AMOUNT,
        ),
    );

    const escrowAccount = Keypair.generate() //产生一个随机公/私钥对
    console.log("escrowAccount:",escrowAccount.publicKey.toBase58())
    //创建托管账号指令
    const createEscrowAccountIx = SystemProgram.createAccount({
        space: Escrow_Size,
        lamports: await connection.getMinimumBalanceForRentExemption(Escrow_Size, 'confirmed'),
        fromPubkey: alice.publicKey,
        newAccountPubkey: escrowAccount.publicKey,
        programId: escrowProgramId
    });
    transaction.add(createEscrowAccountIx)

    //初始化托管账号指令
    const initEscrowIx = new TransactionInstruction({
        programId: escrowProgramId,
        keys: [
            { pubkey: alice.publicKey, isSigner: true, isWritable: false },
            { pubkey: temp_account, isSigner: false, isWritable: true },
            { pubkey: alice_y, isSigner: false, isWritable: false },
            { pubkey: escrowAccount.publicKey, isSigner: false, isWritable: true },
            { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
            { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
        ],
        data: Buffer.from(Uint8Array.of(0, ...new BN(expectedAmount).toArray("le", 8)))
    })
    transaction.add(initEscrowIx)
    //发送交易
    await sendAndConfirmTransaction(
        connection,
        transaction,
        [alice,escrowAccount] //这里要创建escrowAccount,所以它必须签名
    )
    const encodedEscrowState = (await connection.getAccountInfo(escrowAccount.publicKey,'confirmed')).data;
    const decodedEscrowState = ESCROW_ACCOUNT_DATA_LAYOUT.decode(encodedEscrowState)
    let info = {
        isInitialized:decodedEscrowState.isInitialized === 1,
        initializerPubkey:new PublicKey(decodedEscrowState.initializerPubkey).toBase58(),
        initializerTempTokenAccountPubkey:new PublicKey(decodedEscrowState.initializerTempTokenAccountPubkey).toBase58(),
        initializerReceivingTokenAccountPubkey:new PublicKey(decodedEscrowState.initializerReceivingTokenAccountPubkey).toBase58(),
        expectedAmount:new BN(decodedEscrowState.expectedAmount, 10, "le").toNumber()
    }
    console.log("EscrowState:",info)
}

init().then(() => console.log("over"))

回到项目根目录,然后我们运行下面程序安装依赖:

yarn add buffer-layout
yarn add bn.js

最后我们运行node test/init.js,会得到类似如下输出:

escrowAccount: 6uNBMA2ixoKpGHdygvN1M1BsQE44tEpSqEcRehxTniKk
EscrowState: {
  isInitialized: true,
  initializerPubkey: 'A6Bu3xfaKFf9EoKrpviCF3K5szNcZLGJkLxPyAUqShJp',
  initializerTempTokenAccountPubkey: 'F6cLx73ZA56A6C54YJY4wGqPG9qr6FcZFB3H1sKLtMqq',
  initializerReceivingTokenAccountPubkey: 'Bu8Heft6Lsih32Z6yaVFQqVndDtzAmJdMS8friSLb59w',
  expectedAmount: 1200
}
over

上面的结果中,initializerPubkey 代表 Alice的账号地址,initializerTempTokenAccountPubkey代表Alice转移代币X到托管合约的地址,initializerReceivingTokenAccountPubkey 代表Alice 接收 代币Y的地址。

我们可以将上面得到的结果和第一次运行得到的结果相比较一下,可以看到是吻合的。

到此,我们完成了教学文章中的第一部分的学习,有兴趣的读者可以自行完成接下来第二部分的学习。再次感谢 paulx

上一篇:[NOI2019] 弹跳


下一篇:GBase 8c 管理平台操作指南(三)集群管理——扩容