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來處理入口轉發過來的引數,在該函式內部,首先決議指令,然后根據指令呼叫相應的處理函式,
注意:
- 它回傳的是ProgramResult,
- 函式體中"?"運算子的使用,向上級呼叫傳遞錯誤,
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_slice與pack_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(())
}
...
上面的代碼主要添加的功能有:
- 驗證那個用來接收代幣的賬號是否存在
- 用來驗證交易賬號是否免租金(這里請閱讀相關文章了解租金免除的概念)
- 用來驗證交易賬號未初始化過(也就是只能初始化一次),
- 將交易賬號的保存的資料初始化并寫回區塊鏈(見 Escrow::pack 函式)
- 轉讓臨時代幣賬號的所有權
同時修改
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,
至此,我們第一部分的代碼就算撰寫完畢,
五、在本地編譯部署
-
編譯合約,打開終端切換到專案根目錄,運行
cargo build-bpf --manifest-path=./Cargo.toml --bpf-out-dir=dist/program并忽視那些警告(那是下一步使用的),編譯完成后會給出部署命令, -
啟動本地節點,打開一個終端運行
solana-test-validator啟動本地節點, -
進行本地配置,另外打開一個終端,運行
solana config get看是否指向了本地節點,如果不是,運行solana config set --url http://localhost:8899進行設定,然后運行solana balance,你會發現你擁有 500000000 個SOL,-_- !!! -
運行編譯時給出的部署命令:
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主成分分析(降維)
