主頁 > 區塊鏈 > Solana區塊鏈智能合約開發簡要流程

Solana區塊鏈智能合約開發簡要流程

2021-12-23 07:40:27 區塊鏈

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/390409.html

標籤:區塊鏈

上一篇:PCA主成分分析(降維)

下一篇:三位一體集大乘---論互聯網金融戰略

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • JAVA使用 web3j 進行token轉賬

    最近新學習了下區塊鏈這方面的知識,所學不多,給大家分享下。 # 1. 關于web3j web3j是一個高度模塊化,反應性,型別安全的Java和Android庫,用于與智能合約配合并與以太坊網路上的客戶端(節點)集成。 # 2. 準備作業 jdk版本1.8 引入maven <dependency> < ......

    uj5u.com 2020-09-10 03:03:06 more
  • 以太坊智能合約開發框架Truffle

    前言 部署智能合約有多種方式,命令列的瀏覽器的渠道都有,但往往跟我們程式員的風格不太相符,因為我們習慣了在IDE里寫了代碼然后打包運行看效果。 雖然現在IDE中已經存在了Solidity插件,可以撰寫智能合約,但是部署智能合約卻要另走他路,沒辦法進行一個快捷的部署與測驗。 如果團隊管理的區塊節點多、 ......

    uj5u.com 2020-09-10 03:03:12 more
  • 谷歌二次驗證碼成為區塊鏈專用安全碼,你怎么看?

    前言 谷歌身份驗證器,前些年大家都比較陌生,但隨著國內互聯網安全的加強,它越來越多地出現在大家的視野中。 比較廣泛接觸的人群是國際3A游戲愛好者,游戲盜號現象嚴重+國外賬號安全應用廣泛,這類游戲一般都會要求用戶系結名為“兩步驗證”、“雙重驗證”等,平臺一般都推薦用谷歌身份驗證器。 后來區塊鏈業務風靡 ......

    uj5u.com 2020-09-10 03:03:17 more
  • 密碼學DAY1

    目錄 ##1.1 密碼學基本概念 密碼在我們的生活中有著重要的作用,那么密碼究竟來自何方,為何會產生呢? 密碼學是網路安全、資訊安全、區塊鏈等產品的基礎,常見的非對稱加密、對稱加密、散列函式等,都屬于密碼學范疇。 密碼學有數千年的歷史,從最開始的替換法到如今的非對稱加密演算法,經歷了古典密碼學,近代密 ......

    uj5u.com 2020-09-10 03:03:50 more
  • 密碼學DAY1_02

    目錄 ##1.1 ASCII編碼 ASCII(American Standard Code for Information Interchange,美國資訊交換標準代碼)是基于拉丁字母的一套電腦編碼系統,主要用于顯示現代英語和其他西歐語言。它是現今最通用的單位元組編碼系統,并等同于國際標準ISO/IE ......

    uj5u.com 2020-09-10 03:04:50 more
  • 密碼學DAY2

    ##1.1 加密模式 加密模式:https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html ECB ECB : Electronic codebook, 電子密碼本. 需要加密的訊息按照塊密碼的塊大小被分為數個塊,并對每個塊進 ......

    uj5u.com 2020-09-10 03:05:42 more
  • NTP時鐘服務器的特點(京準電子)

    NTP時鐘服務器的特點(京準電子) NTP時鐘服務器的特點(京準電子) 京準電子官V——ahjzsz 首先對時間同步進行了背景介紹,然后討論了不同的時間同步網路技術,最后指出了建立全球或區域時間同步網存在的問題。 一、概 述 在通信領域,“同步”概念是指頻率的同步,即網路各個節點的時鐘頻率和相位同步 ......

    uj5u.com 2020-09-10 03:05:47 more
  • 標準化考場時鐘同步系統推進智能化校園建設

    標準化考場時鐘同步系統推進智能化校園建設 標準化考場時鐘同步系統推進智能化校園建設 安徽京準電子科技官微——ahjzsz 一、背景概述隨著教育事業的快速發展,學校建設如雨后春筍,隨之而來的學校教育、管理、安全方面的問題成了學校管理人員面臨的最大的挑戰,這些問題同時也是學生家長所擔心的。為了讓學生有更 ......

    uj5u.com 2020-09-10 03:05:51 more
  • 位元幣入門

    引言 位元幣基本結構 位元幣基礎知識 1)哈希演算法 2)非對稱加密技術 3)數字簽名 4)MerkleTree 5)哪有位元幣,有的是UTXO 6)位元幣挖礦與共識 7)區塊驗證(共識) 總結 引言 上一篇我們已經知道了什么是區塊鏈,此篇說一下區塊鏈的第一個應用——位元幣。其實先有位元幣,后有的區塊 ......

    uj5u.com 2020-09-10 03:06:15 more
  • 北斗對時服務器(北斗對時設備)電力系統應用

    北斗對時服務器(北斗對時設備)電力系統應用 北斗對時服務器(北斗對時設備)電力系統應用 京準電子科技官微(ahjzsz) 中國北斗衛星導航系統(英文名稱:BeiDou Navigation Satellite System,簡稱BDS),因為是目前世界范圍內唯一可以大面積提供免費定位服務的系統,所以 ......

    uj5u.com 2020-09-10 03:06:20 more
最新发布
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:46:47 more
  • Hyperledger Fabric 使用 CouchDB 和復雜智能合約開發

    在上個實驗中,我們已經實作了簡單智能合約實作及客戶端開發,但該實驗中智能合約只有基礎的增刪改查功能,且其中的資料管理功能與傳統 MySQL 比相差甚遠。本文將在前面實驗的基礎上,將 Hyperledger Fabric 的默認資料庫支持 LevelDB 改為 CouchDB 模式,以實作更復雜的資料... ......

    uj5u.com 2023-04-16 07:28:31 more
  • .NET Core 波場鏈離線簽名、廣播交易(發送 TRX和USDT)筆記

    Get Started NuGet You can run the following command to install the Tron.Wallet.Net in your project. PM> Install-Package Tron.Wallet.Net 配置 public reco ......

    uj5u.com 2023-04-14 08:08:00 more
  • DKP 黑客分析——不正確的代幣對比率計算

    概述: 2023 年 2 月 8 日,針對 DKP 協議的閃電貸攻擊導致該協議的用戶損失了 8 萬美元,因為 execute() 函式取決于 USDT-DKP 對中兩種代幣的余額比率。 智能合約黑客概述: 攻擊者的交易:0x0c850f,0x2d31 攻擊者地址:0xF38 利用合同:0xf34ad ......

    uj5u.com 2023-04-07 07:46:09 more
  • Defi開發簡介

    Defi開發簡介 介紹 Defi是去中心化金融的縮寫, 是一項旨在利用區塊鏈技術和智能合約創建更加開放,可訪問和透明的金融體系的運動. 這與傳統金融形成鮮明對比,傳統金融通常由少數大型銀行和金融機構控制 在Defi的世界里,用戶可以直接從他們的電腦或移動設備上訪問廣泛的金融服務,而不需要像銀行或者信 ......

    uj5u.com 2023-04-05 08:01:34 more
  • solidity簡單的ERC20代幣實作

    // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; import "hardhat/console.sol"; //ERC20 同質化代幣,每個代幣的本質或性質都是相同 //ETH 是原生代幣,它不是ERC20代幣, ......

    uj5u.com 2023-03-21 07:56:29 more
  • solidity 參考型別修飾符memory、calldata與storage 常量修飾符C

    在solidity語言中 參考型別修飾符(參考型別為存盤空間不固定的數值型別) memory、calldata與storage,它們只能修飾參考型別變數,比如字串、陣列、位元組等... memory 適用于方法傳參、返參或在方法體內使用,使用完就會清除掉,釋放記憶體 calldata 僅適用于方法傳參 ......

    uj5u.com 2023-03-08 07:57:54 more
  • solidity注解標簽

    在solidity語言中 注釋符為// 注解符為/* 內容*/ 或者 是 ///內容 注解中含有這幾個標簽給予我們使用 @title 一個應該描述合約/介面的標題 contract, library, interface @author 作者的名字 contract, library, interf ......

    uj5u.com 2023-03-08 07:57:49 more
  • 評價指標:相似度、GAS消耗

    【代碼注釋自動生成方法綜述】 這些評測指標主要來自機器翻譯和文本總結等研究領域,可以評估候選文本(即基于代碼注釋自動方法而生成)和參考文本(即基于手工方式而生成)的相似度. BLEU指標^[^?88^^?^]^:其全稱是bilingual evaluation understudy.該指標是最早用于 ......

    uj5u.com 2023-02-23 07:27:39 more
  • 基于NOSTR協議的“公有制”版本的Twitter,去中心化社交軟體Damus

    最近,一個幽靈,Web3的幽靈,在網路游蕩,它叫Damus,這玩意詮釋了什么叫做病毒式營銷,滑稽的是,一個Web3產品卻在Web2的產品鏈上瘋狂傳銷,各方大佬紛紛為其背書,到底發生了什么?Damus的葫蘆里,賣的是什么藥? 注冊和簡單實用 很少有什么產品在用戶注冊環節會有什么噱頭,但Damus確實出 ......

    uj5u.com 2023-02-05 06:48:39 more