Solana区块链开发完整教程:从环境搭建到智能合约部署,包含Hello Solana、Counter案例、转账实现等实用开发指南,助您掌握Solana生态开发技能
参考文档: HackQuest社区:https://www.hackquest.io/zh Solana中文文档: https://www.solana-cn.com/SolanaDocumention/home.html
更多更及时的文章请访问作者博客 原文链接:https://leapwhale.com/article/qjlj554s
不同的环境可以在这里查看不同的安装命令:https://solanacookbook.com/getting-started/installation.html
以linux为例,安装命令如下:
sh -c "$(curl -sSfL https://release.solana.com/v1.18.26/install)"
注意:要确保你的环境可以访问外网,wget google.com
要成功,不然你下面的步骤会走的比较坎坷🥲
安装完成之后通过如下命令生成本地 Account
solana-keygen new
最后会生成密钥对,公钥和助记词,请注意保管好
更新solana版本
solana-install update
生成密钥到指定地址
solana-keygen new --no-bip39-passphrase -o ./account.json
通过密钥对查看公钥
solana-keygen pubkey accounts/account1.json
设置密对
solana config set --keypair 地址
设置地址为开发网
solana config set --url https://api.devnet.solana.com
查询当前配置
solana config get
查看当前账户公钥
solana address
空投一个 solana 并查询 solana 余额
solana airdrop 1
solana balance
查询有programs
solana program show --programs
删除某个program
solana program close programid --bypass-warning
如果airdrop 限速了,也可以直接来官网领空投
参考文档:https://solanacookbook.com/zh/core-concepts/accounts.html
在Solana中有三类账户:
平时我们接触的比较多的是前两种,其中数据账户又分为两类:
每个账户都有一个地址(一般情况下是一个公钥)以及一个所有者(程序账户的地址)。 下面详细列出一个账户存储的完整字段列表。
字段 | 描述 |
---|---|
lamports | 这个账户拥有的lamport(兰波特)数量 |
owner | 这个账户的所有者程序 |
executable | 这个账户成是否可以处理指令 |
data | 这个账户存储的数据的字节码 |
rent_epoch | 下一个需要付租金的epoch(代) |
任何开发者都可以在Solana链上编写以及部署程序。Solana程序(在其他链上叫做智能合约),是所有链上活动的基础。 参考文档:https://solanacookbook.com/zh/core-concepts/programs.html
一般使用 Rust 编写 solana 程序,一般采用如下架构
文件 | 描述 |
---|---|
lib.rs | 注册模块 |
entrypoint.rs | 程序的入口点 |
instruction.rs | 程序的API, 对指令的数据进行序列化与反序列化 |
processor.rs | 程序的业务逻辑 |
state.rs | 程序对象,对状态进行反序列化 |
error.rs | 程序中制定的错误 |
Solana支持以下的几个环境:
集群环境 | RPC连接URL |
---|---|
Mainnet-beta | https://api.mainnet-beta.solana.com |
Testnet | https://api.testnet.solana.com |
Devnet | https://api.devnet.solana.com |
Localhost | 默认端口:8899(例如,http://localhost:8899,http://192.168.1.88:8899) |
要使用 Rust 编写 Solana 程序,需要用到 Solana 程序的标准库 solana_program。该标准库包含我们将用于开发 Solana 程序的模块和宏。如果您想深入了解solana_program crate,请查看solana_program crate文档。
对于基本程序,我们需要将 solana_program 库中的以下项目纳入作用域:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
msg
};
Solana 程序需要单个入口点来处理程序指令。入口点是使用entrypoint!声明的宏。
通过如下方式声明程序的入口点函数:
// 声明程序入口点函数
entrypoint!(process_instruction);
该指令处理函数就是 process_instruction
fn process_instruction(
// 当前的程序ID
program_id: &Pubkey,
// 该指令涉及到的账户集合
accounts: &[AccountInfo],
// 该指令的参数
instruction_data: &[u8],
) -> ProgramResult;
ProgramResult
是 solana 中定义的一个通用错误处理类型,它是solana_program中的一个结构体,代表着 Solana 程序中指令处理函数的返回值,该类型代表 Transaction 交易中指令的处理结果,成功时为单元类型(),即返回值为空,失败时返回值为ProgramError,它本身又是个枚举。
use std::result::Result as ResultGeneric;
pub type ProgramResult = ResultGeneric<(), ProgramError>;
在 ProgramError 中定义了 23 种常见的错误原因枚举值,也支持自定义的错误类型,如下:
pub enum ProgramError {
// 用户自定义错误类型
#[error("Custom program error: {0:#x}")]
Custom(u32),
// 参数无效
#[error("The arguments provided to a program instruction were invalid")]
InvalidArgument,
// 指令数据无效
#[error("An instruction's data contents was invalid")]
InvalidInstructionData,
// 账户数据无效
#[error("An account's data contents was invalid")]
InvalidAccountData,
// ……
}
创建一个文件夹 solana-tutorial
在里面创建一个hello-solana目录,进入hello-solana目录,创建一个src文件夹,进入文件夹,创建一个 rust项目
cargo new --lib program
在 cargo.toml 中引入如下库,注意,这里solana的版本最好和你安装的solana版本一致,不然可能会报错
[package]
name = "hello-solana"
version = "0.1.0"
edition = "2021"
[dependencies]
solana-program = "1.18.15"
[dev-dependencies]
solana-program-test = "1.18.15"
solana-sdk = "1.18.15"
[lib]
crate-type = ["cdylib", "lib"]
在 src/program/src/lib.rs 中写如下代码
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
};
// 定义智能程序的入口点函数
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey, // 智能程序的公钥
accounts: &[AccountInfo], // 一个包含所有相关账户信息的数组
instruction_data: &[u8], // 包含指令数据的字节数组
) -> ProgramResult {
msg!("Hello, Solana!");
Ok(())
}
打开 playground 链接:https://beta.solpg.io/
在项目管理菜单中点击 Create a new project 创建新项目solana_counter,填入项目名称,在Choose a framework 中选择 Native(Rust)
点击左下角链接 Solana 网络,如果之前没有创建wallet 钱包,Playground 会自动为我们创建,或者选择导入私钥生成我们指定的钱包。界面的底部和右上角会展示连接的网络和钱包余额信息。
将上面的代码粘贴到 src/lib.rs
里,然后点击 Build
编译完成后,我们可以在左侧第二个菜单进行deploy部署(部署成功后,就会变成Upgrade按钮),并且在下方会显示部署的详情及结果,在这还能看到 Programe ID
接着修改 client.ts
代码如下
// Client
console.log("My address:", pg.wallet.publicKey.toString());
const balance = await pg.connection.getBalance(pg.wallet.publicKey);
console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`);
const instruction = new web3.TransactionInstruction({
keys: [
{
pubkey: pg.wallet.publicKey,
isSigner: false,
isWritable: true,
},
],
programId: pg.PROGRAM_ID,
});
await web3.sendAndConfirmTransaction(
pg.connection,
new web3.Transaction().add(instruction),
[pg.wallet.keypair],
);
点击 run 之后,你就能在在浏览器中可以看到日志了
在program目录下执行编译命令,构建可在 Solana 集群部署的链上程序的 BPF 字节码文件
cargo build-bpf
编译过程中可能出现报错,注意观察报错信息,如果缺了什么库,就安装对应的库文件就好(比如bzip2)
成功之后你会在target/deploy目录下看到这个文件(第一次成功了没出现的话可以再编译一次)
随后我们可以用这个命令部署到solana devnet中
solana program deploy <hello_solana.so的路径>
部署成功之后通过如下命令查询你部署到solana网络上的程序(时间会有点久,如果报错就多试几次)
solana program show --programs
在 hello-solana层级目录下安装web3.js,这么做的目的是调用部署在 solana上面的代码(注意更新自己的node版本,不要太老)
npm install --save @solana/web3.js
但是这样在 package.json 中就只有 web3.js 这个库,我们还需要安装其他库, 修改 package.json 的代码如下
{
"name": "hello-solana",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "ts-node src/client/main.ts",
"clean": "npm run clean:program",
"build:program": "cargo build-bpf --manifest-path=./src/program/Cargo.toml --bpf-out-dir=dist/program",
"clean:program": "cargo clean --manifest-path=./src/program/Cargo.toml && rm -rf ./dist",
"test:program": "cargo test-bpf --manifest-path=./src/program/Cargo.toml"
},
"dependencies": {
"@solana/web3.js": "^1.93.0",
"mz": "^2.7.0"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/mz": "^2.7.2",
"ts-node": "^10.0.0",
"typescript": "^4.0.5"
},
"engines": {
"node": ">=14.0.0"
}
}
在 hello-solana/src 目录下创建 client/main.ts 文件
写入如下代码,大致逻辑是读取密钥对,创建公钥,获取空投,触发交易
import {
Keypair,
Connection,
PublicKey,
LAMPORTS_PER_SOL,
TransactionInstruction,
Transaction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import fs from 'mz/fs';
import path from 'path';
/*
Our keypair we used to create the on-chain Rust program
*/
const PROGRAM_KEYPAIR_PATH = path.join(
path.resolve(__dirname, '../../dist/program'),
'hello_solana-keypair.json'
);
async function main() {
console.log("Launching client...");
/*
Connect to Solana DEV net
*/
let connection = new Connection('https://api.devnet.solana.com', 'confirmed');
/*
Get our program's public key
*/
const secretKeyString = await fs.readFile(PROGRAM_KEYPAIR_PATH, {encoding: 'utf8'});
const secretKey = Uint8Array.from(JSON.parse(secretKeyString));
const programKeypair = Keypair.fromSecretKey(secretKey);
let programId: PublicKey = programKeypair.publicKey;
/*
Generate an account (keypair) to transact with our program
*/
const triggerKeypair = Keypair.generate();
const airdropRequest = await connection.requestAirdrop(
triggerKeypair.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(airdropRequest);
/*
Conduct a transaction with our program
*/
console.log('--Pinging Program ', programId.toBase58());
const instruction = new TransactionInstruction({
keys: [{pubkey: triggerKeypair.publicKey, isSigner: false, isWritable: true}],
programId,
data: Buffer.alloc(0),
});
await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[triggerKeypair],
);
}
main().then(
() => process.exit(),
err => {
console.error(err);
process.exit(-1);
},
);
运行 npm run build:program ,编译solana程序到指定为止
部署程序 solana program deploy ./dist/program/hello_solana.so
拿到如下 program Id
可以另外开一个客户端,用如下命令监控 solana 链上的日志
solana logs | grep "3MCZC3HuC3Yuz23m29HbxCYjq2k5FSXvhgF81oiTiFXN invoke" -A 3
它会持续监听,直到出现相关日志才会打印出来
然后我们执行 npm run start 命令,它就会调用 solana 链上的程序,此时 solana 链上也就会出现对应的日志了
这个图展示了案例项目结构,我们会编写 Solana 程序,并部署到开发网,
程序实现的逻辑是:每调用一次,就将该账户存储的值+1,实现了数据的存储与交互
部署完毕之后编写 ts 代码,调用对应的程序并验证逻辑
创建一个项目Solana-Counter
,
命令:cargo new Solana-Counter
package.json 文件如下
{
"name": "solana-counter",
"version": "1.0.0",
"description": "",
"scripts": {
"clean": "./scripts/cicd.sh clean",
"reset": "./scripts/cicd.sh reset",
"build": "./scripts/cicd.sh build",
"deploy": "./scripts/cicd.sh deploy",
"reset-and-build": "./scripts/cicd.sh reset-and-build",
"example:sum": "ts-node ./src/client/sum.ts",
"example:square": "ts-node ./src/client/square.ts"
},
"dependencies": {
"@solana/web3.js": "^1.33.0",
"borsh": "^0.7.0",
"mz": "^2.7.0",
"yaml": "^1.10.2"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/eslint": "^8.2.2",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/mz": "^2.7.2",
"@types/prettier": "^2.1.5",
"@types/yaml": "^1.9.7",
"@typescript-eslint/eslint-plugin": "^4.6.0",
"@typescript-eslint/parser": "^4.6.0",
"eslint": "^7.12.1",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.1.2",
"start-server-and-test": "^1.11.6",
"ts-node": "^10.0.0",
"typescript": "^4.0.5"
},
"engines": {
"node": ">=14.0.0"
}
}
在src目录下使用cargo创建Rust项目
cargo new --lib counter
cargo.toml
[package]
name = "counter"
version = "0.1.0"
edition = "2021"
[dependencies]
borsh = "0.9.3"
borsh-derive = "0.9.1"
solana-program = "1.18.15"
[dev-dependencies]
solana-program-test = "1.18.15"
solana-sdk = "1.18.15"
[lib]
crate-type = ["cdylib", "lib"]
我们实现 sum/src/lib.rs ,逻辑是读取 account 中的数据,然后将里面存储的 sum + 1
实现代码如下
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
// BorshSerialize、BorshDeserialize 这2个派生宏是为了实现(反)序列化操作
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u32,
}
// 定义智能程序的入口点函数
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey, // 智能程序的公钥
accounts: &[AccountInfo], // 一个包含所有相关账户信息的数组
instruction_data: &[u8], // 包含指令数据的字节数组
) -> ProgramResult {
// 账户迭代器
let accounts_iter = &mut accounts.iter();
// 获取调用者账户
let account = next_account_info(accounts_iter)?;
// 验证调用者身份
// The account must be owned by the program in order to modify its data
// 检查账户的 owner 是否与 program_id 匹配,如果不匹配则返回错误
if account.owner != program_id {
msg!("Account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}
msg!("Debug output:");
msg!("Account ID: {}", account.key);
msg!("Executable?: {}", account.executable);
msg!("Lamports: {:#?}", account.lamports);
msg!("Debug output complete.");
msg!("Adding 1 to sum...");
// 读取 account 中的数据
// 从 account.data.borrow() 返回的不可变引用中反序列化 CounterAccount 结构体。
let mut counter = CounterAccount::try_from_slice(&account.data.borrow())?;
counter.count += 1;
// 然后将 counter 序列化并写入到 account.data 中
counter.serialize(&mut *account.data.borrow_mut())?;
msg!("Current sum is now: {}", counter.count);
Ok(())
}
下面详细介绍一下读取和修改数据那部分代码
读取数据账户
let mut counter = CounterAccount::try_from_slice(&account.data.borrow())?;
这行代码的目的是从 Solana 数据账户中反序列化出 CounterAccount 结构体的实例。
通过如上方式,我们获取了 CounterAccount 数据账户进行了反序列化,并获取到它的可变借用。
修改数据账户
counter.count += 1;
counter.serialize(&mut *account.data.borrow_mut())?;
通过如上的方式,将 CounterAccount 结构体中的修改后的值递增,并将更新后的结构体序列化为字节数组,然后写入 Solana 账户的可变数据字段中。实现了在 Solana 程序中对计数器值进行更新和存储。
将上面的 Solana 程序粘贴到 Playground 中,然后编译部署
部署成功后,我们可以在 Solana 区块链浏览器中查看详细信息
点击 program id,还能看到程序账户和子账户(存储程序二进制文件)之间的关系。
在 Client/tests 下写入测试脚本,测试脚本的主要内容如下
创建计数器对象
定义一个 CounterAccount 类,并使用构造函数初始化对象实例。创建了一个 CounterAccountSchema 对象,该对象定义了 CounterAccount 类的序列化规则。接下来计算了序列化一个 CounterAccount 对象所需的字节数GREETING_SIZE,这个值将用于后续创建账户时确定账户空间的大小。
// 创建 keypair
const counterAccountKp = new web3.Keypair();
console.log(`counterAccountKp.publickey : ${counterAccountKp.publicKey}`)
const lamports = await pg.connection.getMinimumBalanceForRentExemption(
GREETING_SIZE
);
// 创建生成对应数据账户的指令
const createGreetingAccountIx = web3.SystemProgram.createAccount({
fromPubkey: pg.wallet.publicKey,
lamports,
newAccountPubkey: counterAccountKp.publicKey,
programId: pg.PROGRAM_ID,
space: GREETING_SIZE,
});
调用 Solana 程序
接下来我们创建如下指令,调用之前部署的 Solana 程序,并传入对应的数据账户counterAccountKp.publicKey来存储计数器状态,计数器程序会在该账户data的基础上累加,因此计数器从初始值0变为1。
const greetIx = new web3.TransactionInstruction({
keys: [
{
pubkey: counterAccountKp.publicKey,
isSigner: false,
isWritable: true,
},
],
programId: pg.PROGRAM_ID,
});
查看程序执行结果
调用getAccountInfo函数获取指定地址的数据,通过反序列化就可以把二进制数据转换成我们的计数器对象,此时它的值为1。
// 获取指定数据账户的信息
const counterAccountOnSolana = await pg.connection.getAccountInfo(
counterAccountKp.publicKey
);
// 反序列化
const deserializedAccountData = borsh.deserialize(
CounterAccountSchema,
CounterAccount,
counterAccountOnSolana.data
);
// 判断当前计数器是否累加
assert.equal(deserializedAccountData.count, 1);
完整代码如下
// No imports needed: web3, borsh, pg and more are globally available
/**
* CounterAccount 对象
*/
class CounterAccount {
count = 0;
constructor(fields: { count: number } | undefined = undefined) {
if (fields) {
this.count = fields.count;
}
}
}
/**
* CounterAccount 对象 schema 定义
*/
const CounterAccountSchema = new Map([
[CounterAccount, { kind: "struct", fields: [["count", "u32"]] }],
]);
/**
* 账户空间大小
*/
const GREETING_SIZE = borsh.serialize(
CounterAccountSchema,
new CounterAccount()
).length;
describe("Test", () => {
it("greet", async () => {
// 创建 keypair
const counterAccountKp = new web3.Keypair();
console.log(`counterAccountKp.publickey : ${counterAccountKp.publicKey}`)
const lamports = await pg.connection.getMinimumBalanceForRentExemption(
GREETING_SIZE
);
// 创建生成对应数据账户的指令
const createGreetingAccountIx = web3.SystemProgram.createAccount({
fromPubkey: pg.wallet.publicKey,
lamports,
newAccountPubkey: counterAccountKp.publicKey,
programId: pg.PROGRAM_ID,
space: GREETING_SIZE,
});
// 调用程序,计数器累加
const greetIx = new web3.TransactionInstruction({
keys: [
{
pubkey: counterAccountKp.publicKey,
isSigner: false,
isWritable: true,
},
],
programId: pg.PROGRAM_ID,
});
// 创建交易,包含如上2个指令
const tx = new web3.Transaction();
tx.add(createGreetingAccountIx, greetIx);
// 发起交易,获取交易哈希
const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [
pg.wallet.keypair,
counterAccountKp,
]);
console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
// 获取指定数据账户的信息
const counterAccountOnSolana = await pg.connection.getAccountInfo(
counterAccountKp.publicKey
);
// 反序列化
const deserializedAccountData = borsh.deserialize(
CounterAccountSchema,
CounterAccount,
counterAccountOnSolana.data
);
// 判断当前计数器是否累加
assert.equal(deserializedAccountData.count, 1);
});
});
然后点击 Test 按钮,就可以验证逻辑了
本案例中我们来实现一个可以接受数据并计算的 solana 程序
案例框架和之前的基本类似,同样的 cicd 脚本,同样的 package.json,只不过这次新增下面这个包
npm i @solana/buffer-layout buffer
还是一样的,在 src 目录下创建 rust 项目
cargo new --lib calculator
cargo.toml
配置如下
[package]
name = "calculator"
version = "0.1.0"
edition = "2021"
[dependencies]
borsh = "0.9.3"
borsh-derive = "0.9.1"
solana-program = "1.18.15"
[dev-dependencies]
solana-program-test = "1.18.15"
solana-sdk = "1.18.15"
[lib]
crate-type = ["cdylib", "lib"]
新建一个 calculator.rs,实现计算器的基本功能
use borsh::{BorshDeserialize, BorshSerialize};
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CalculatorInstructions {
operation: u32,
operating_value: u32,
}
impl CalculatorInstructions {
pub fn evaluate(self, value: u32) -> u32 {
match &self.operation {
1 => value + &self.operating_value,
2 => value - &self.operating_value,
3 => value * &self.operating_value,
_ => value * 0,
}
}
}
然后在 lib.rs 中引用这个结构体,并定义程序入口
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
use crate::calculator::CalculatorInstructions;
mod calculator;
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Calculator {
pub value: u32,
}
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let account = next_account_info(accounts_iter)?;
if account.owner != program_id {
msg!("Account does not have the correct program id");
return Err(ProgramError::IncorrectProgramId);
}
let mut calc = Calculator::try_from_slice(&account.data.borrow())?;
let calculator_instructions = CalculatorInstructions::try_from_slice(&instruction_data)?;
calc.value = calculator_instructions.evaluate(calc.value);
calc.serialize(&mut &mut account.data.borrow_mut()[..])?;
msg!("Value is now: {}", calc.value);
Ok(())
}
在项目根目录的 src 目录下创建一个 client 文件夹,在 client 里创建一个 util.ts
文件
内容如下,逻辑很简单,就是提供三个功能函数
import { Keypair } from '@solana/web3.js';
import fs from 'mz/fs';
import * as BufferLayout from '@solana/buffer-layout';
import { Buffer } from 'buffer';
export async function createKeypairFromFile(
filePath: string,
): Promise<Keypair> {
const secretKeyString = await fs.readFile(filePath, {encoding: 'utf8'});
const secretKey = Uint8Array.from(JSON.parse(secretKeyString));
return Keypair.fromSecretKey(secretKey);
}
export async function getStringForInstruction(
operation: number, operating_value: number) {
if (operation == 0) {
return "reset the example.";
} else if (operation == 1) {
return `add: ${operating_value}`;
} else if (operation == 2) {
return `subtract: ${operating_value}`;
} else if (operation == 3) {
return `multiply by: ${operating_value}`;
}
}
// 创建一个指令缓冲区
export async function createCalculatorInstructions(
operation: number, operating_value: number): Promise<Buffer> {
const bufferLayout: BufferLayout.Structure<any> = BufferLayout.struct(
[
BufferLayout.u32('operation'),
BufferLayout.u32('operating_value'),
]
);
const buffer = Buffer.alloc(bufferLayout.span);
bufferLayout.encode({
operation: operation,
operating_value: operating_value,
}, buffer);
return buffer;
}
创建一个 math.ts
文件,连接开发网,获取账户等功能和上面的逻辑一致,这里就只放出核心不一样的 pingProgram 函数部分。这里创建了计算指令,并将指令发送给了 Solana 程序
/*
Ping the program.
*/
export async function pingProgram(
operation: number, operatingValue: number) {
console.log(`All right, let's run it.`);
console.log(`Pinging our calculator program...`);
let calcInstructions = await createCalculatorInstructions(
operation, operatingValue
);
console.log(`We're going to ${await getStringForInstruction(operation, operatingValue)}`)
const instruction = new TransactionInstruction({
keys: [{pubkey: clientPubKey, isSigner: false, isWritable: true}],
programId,
data: calcInstructions,
});
await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[localKeypair],
);
console.log(`Ping successful.`);
}
/*
Run the example (main).
*/
export async function example(programName: string, accountSpaceSize: number) {
await connect();
await getLocalAccount();
await getProgram(programName);
await configureClientAccount(accountSpaceSize);
await pingProgram(1, 4); // Add 4
await pingProgram(2, 1); // Subtract 1
await pingProgram(3, 2); // Multiply by 2
}
最后创建一个 calculator.ts
文件,用于调用 math.ts
和之前的逻辑一样,先计算出实例空间,然后再调用程序
import * as borsh from 'borsh';
import * as math from './math';
/*
Account Data
*/
class Calculator {
value = 0;
constructor(fields: {value: number} | undefined = undefined) {
if (fields) {
this.value = fields.value;
}
}
}
const CalculatorSchema = new Map([
[Calculator, {kind: 'struct', fields: [['value', 'u32']]}],
]);
const CALCULATOR_SIZE = borsh.serialize(
CalculatorSchema,
new Calculator(),
).length;
/*
Instruction Data
*/
export class CalculatorInstructions {
operation = 0;
operating_value = 0;
constructor(fields: {operation: number, operating_value: number} | undefined = undefined) {
if (fields) {
this.operation = fields.operation;
this.operating_value = fields.operating_value;
}
}
}
export const CalculatorInstructionsSchema = new Map([
[CalculatorInstructions, {kind: 'struct', fields: [
['operation', 'u32'], ['operating_value', 'u32']
]}],
]);
export const CALCULATOR_INSTRUCTIONS_SIZE = borsh.serialize(
CalculatorInstructionsSchema,
new CalculatorInstructions(),
).length;
async function main() {
await math.example('calculator', CALCULATOR_SIZE);
}
main().then(
() => process.exit(),
err => {
console.error(err);
process.exit(-1);
},
);
#! /bin/bash
SOLANA_PROGRAMS=("calculator")
case $1 in
"reset")
rm -rf ./node_modules
for x in $(solana program show --programs | awk 'RP==0 {print $1}'); do
if [[ $x != "Program" ]];
then
solana program close $x --bypass-warning;
fi
done
for program in "${SOLANA_PROGRAMS[@]}"; do
cargo clean --manifest-path=./src/$program/Cargo.toml
done
rm -rf dist/program
;;
"clean")
rm -rf ./node_modules
for program in "${SOLANA_PROGRAMS[@]}"; do
cargo clean --manifest-path=./src/$program/Cargo.toml
done;;
"build")
for program in "${SOLANA_PROGRAMS[@]}"; do
cargo build-bpf --manifest-path=./src/$program/Cargo.toml --bpf-out-dir=./dist/program
done;;
"deploy")
for program in "${SOLANA_PROGRAMS[@]}"; do
cargo build-bpf --manifest-path=./src/$program/Cargo.toml --bpf-out-dir=./dist/program
solana program deploy dist/program/$program.so
done;;
"reset-and-build")
rm -rf ./node_modules
for x in $(solana program show --programs | awk 'RP==0 {print $1}'); do
if [[ $x != "Program" ]];
then
solana program close $x --bypass-warning;
fi
done
rm -rf dist/program
for program in "${SOLANA_PROGRAMS[@]}"; do
cargo clean --manifest-path=./src/$program/Cargo.toml
cargo build-bpf --manifest-path=./src/$program/Cargo.toml --bpf-out-dir=./dist/program
solana program deploy dist/program/$program.so
done
npm install
solana program show --programs
;;
esac
首先编译并运行程序
npm run reset-and-build
部署完毕获取到 program id 之后,监控日志
solana logs | grep "你的PROGRAMID invoke" -A 10
最后调用程序
npm run example
client 输出如下
solana 监控日志如下
创建一个项目transfer-sol
,package.json 文件如下
{
"name": "transfer-sol",
"version": "1.0.0",
"description": "",
"scripts": {
"clean": "./_cicd/cicd.sh clean",
"reset": "./_cicd/cicd.sh reset",
"build": "./_cicd/cicd.sh build",
"deploy": "./_cicd/cicd.sh deploy",
"reset-and-build": "./_cicd/cicd.sh reset-and-build",
"simulation": "ts-node ./client/main.ts"
},
"dependencies": {
"@solana/web3.js": "^1.33.0",
"buffer-layout": "^1.2.2"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/node": "^15.6.1",
"ts-node": "^10.0.0",
"typescript": "^4.2.4",
"prettier": "^2.3.0"
},
"engines": {
"node": ">=14.0.0"
}
}
先在本地生成两个账户
solana-keygen new --no-bip39-passphrase -o ./accounts/account1.json
solana-keygen new --no-bip39-passphrase -o ./accounts/account2.json
给账户发点空投(如果限速了就去官网领)
solana airdrop --keypair ./accounts/account1.json 2
solana airdrop --keypair ./accounts/account2.json 2
在 src 目录下创建 rust 项目
cargo new --lib program
cargo.toml
配置如下
[package]
name = "program"
version = "0.1.0"
edition = "2021"
[dependencies]
borsh = "0.9.3"
borsh-derive = "0.9.1"
solana-program = "1.18.15"
[dev-dependencies]
solana-program-test = "1.18.15"
solana-sdk = "1.18.15"
[lib]
crate-type = ["cdylib", "lib"]
lib.rs
实现代码如下
use {
std::convert::TryInto,
solana_program::{
account_info::{
next_account_info, AccountInfo
},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
},
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let payer = next_account_info(accounts_iter)?;
let payee = next_account_info(accounts_iter)?;
// 从input数组中解析出要转账的lamports数量
// 首先尝试获取前8个字节,然后尝试将这些字节转换为u64类型的金额
// 如果解析失败,将返回
let amount = input
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidInstructionData)?;
// let amount = i32::try_from_slice(input);
msg!("Received request to transfer {:?} lamports from {:?} to {:?}.",
amount, payer.key, payee.key);
msg!(" Processing transfer...");
// 使用invoke函数执行一个系统级别的转账指令
// 从payer账户向payee账户转账amount数量的lamports。
invoke(
&system_instruction::transfer(payer.key, payee.key, amount),
&[payer.clone(), payee.clone()],
)?;
msg!("Transfer completed successfully.");
Ok(())
}
在根目录下创建一个 client/main.ts
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import {readFileSync} from "fs";
import path from 'path';
const lo = require("buffer-layout");
// const BN = require("bn.js");
const SOLANA_NETWORK = "devnet";
let connection: Connection;
let programKeypair: Keypair;
let programId: PublicKey;
let account1Keypair: Keypair;
let account2Keypair: Keypair;
/**
* Helper functions.
*/
function createKeypairFromFile(path: string): Keypair {
return Keypair.fromSecretKey(
Buffer.from(JSON.parse(readFileSync(path, "utf-8")))
)
}
/**
* Here we are sending lamports using the Rust program we wrote.
* So this looks familiar. We're just hitting our program with the proper instructions.
*/
async function sendLamports(from: Keypair, to: PublicKey, amount: number) {
// 创建一个8字节的缓冲区data
// 使用buffer-layout库的ns64函数将amount(以lamports为单位)编码到data中。
let data = Buffer.alloc(8) // 8 bytes
// lo.ns64("value").encode(new BN(amount), data);
lo.ns64("value").encode(amount, data);
let ins = new TransactionInstruction({
keys: [
{pubkey: from.publicKey, isSigner: true, isWritable: true},
{pubkey: to, isSigner: false, isWritable: true},
{pubkey: SystemProgram.programId, isSigner: false, isWritable: false},
],
programId: programId,
data: data,
})
await sendAndConfirmTransaction(
connection,
new Transaction().add(ins),
[from]
);
}
async function main() {
connection = new Connection(
`https://api.${SOLANA_NETWORK}.solana.com`, 'confirmed'
);
programKeypair = createKeypairFromFile(
path.join(
path.resolve(__dirname, '../_dist/program'),
'program-keypair.json'
)
);
programId = programKeypair.publicKey;
account1Keypair = createKeypairFromFile(__dirname + "/../accounts/account1.json");
account2Keypair = createKeypairFromFile(__dirname + "/../accounts/account2.json");
// Account1 sends some SOL to Account2.
console.log("Account1 sends some SOL to Account1...");
console.log(` Account1's public key: ${account1Keypair.publicKey}`);
console.log(` Account2's public key: ${account2Keypair.publicKey}`);
// 1 SOL = 1_000_000_000 lamports
// 5000000 lamports = 0.005 SOL
await sendLamports(account1Keypair, account2Keypair.publicKey, 5000000);
}
main().then(
() => process.exit(),
err => {
console.error(err);
process.exit(-1);
},
);
还是复用之前的 cicd 脚本,执行编译命令
npm run reset-and-build
然后监控对应日志,并调用程序
npm run simulation
控制台输出日志如下,可以看到转账成功(两个账户原来都只有2个Sol)
solana也监控到相关日志
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!