Solana 基础
安装
不同的环境可以在这里查看不同的安装命令: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中有三类账户:
- 数据账户,用来存储数据
- 程序账户,用来存储可执行程序(程序编译后会自动生成)
- 原生账户,指Solana上的原生程序,例如"System","Stake",以及"Vote"。
平时我们接触的比较多的是前两种,其中数据账户又分为两类:
- 系统所有账户(也就是我们自己用的账户)
- 程序派生账户(PDA)(基于自己账户派生出的新账户)
每个账户都有一个地址(一般情况下是一个公钥)以及一个所有者(程序账户的地址)。 下面详细列出一个账户存储的完整字段列表。
字段 | 描述 |
---|---|
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 };
- AccountInfo:account_info 模块中的一个结构体,允许我们访问帐户信息。
- entrypoint:声明程序入口点的宏,类似于 Rust 中的 main 函数。
- ProgramResult:entrypoint 模块中的返回值类型。
- Pubkey:pubkey 模块中的一个结构体,允许我们将地址作为公钥访问。
- msg:一个允许我们将消息打印到程序日志的宏,类似于 Rust 中的 println宏。
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, // …… }
Hello Solana
初始化项目
创建一个文件夹 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"]
Solana程序
在 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 验证
打开 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 链上也就会出现对应的日志了
Counter
这个图展示了案例项目结构,我们会编写 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"]
Solana 程序
我们实现 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; // &mut &mut account.data.borrow_mut()[..] 表示创建了一个指向 account.data 的可变切片 // 然后将 counter 序列化并写入到 account.data 中 counter.serialize(&mut &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 结构体的实例。
- &account.data:获取账户的数据字段的引用。在 Solana 中,账户的数据字段data存储着与账户关联的实际数据,对于程序账户而言,它是程序的二进制内容,对于数据账户而言,它就是存储的数据。
- borrow():使用该方法获取data数据字段的可借用引用。并通过&account.data.borrow()方式得到账户数据字段的引用。
- CounterAccount::try_from_slice(...):调用 try_from_slice 方法,它是 BorshDeserializetrait 的一个方法,用于从字节序列中反序列化出一个结构体的实例。这里 CounterAccount 实现了 BorshDeserialize,所以可以使用这个方法。
- ?:是一个错误处理操作符,如果try_from_slice返回错误,整个表达式将提前返回,将错误传播给调用方。
通过如上方式,我们获取了 CounterAccount 数据账户进行了反序列化,并获取到它的可变借用。
修改数据账户
counter.count += 1; counter.serialize(&mut *account.data.borrow_mut())?;
- 首先对 CounterAccount 结构体中的 count 字段进行递增操作。
- &mut *account.data.borrow_mut():通过 borrow_mut() 方法获取账户数据字段的可变引用,然后使用 * 解引用操作符获取该data字段的值,并通过 &mut 将其转换为可变引用。
- serialize 函数方法,它是 BorshSerialize trait 的一个方法,用于将结构体序列化为字节数组。
- ?:是一个错误处理操作符,如果 serialize 方法返回错误,整个表达式将提前返回,将错误传播给调用方。
通过如上的方式,将 CounterAccount 结构体中的修改后的值递增,并将更新后的结构体序列化为字节数组,然后写入 Solana 账户的可变数据字段中。实现了在 Solana 程序中对计数器值进行更新和存储。
Playground 验证
将上面的 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 Keypair (counterAccountKp) 用于存储计数器的状态。
- 使用 Solana API 获取在链上创建相应账户所需的最小 lamports,即Solana 链上存储该账户所要支付的最小押金rent。
- 构建createGreetingAccountIx指令,在链上创建我们指定的counterAccountKp.publicKey账户,并指定了账户的大小。
调用 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 按钮,就可以验证逻辑了
Advanced math
本案例中我们来实现一个可以接受数据并计算的 solana 程序
案例框架和之前的基本类似,同样的 cicd 脚本,同样的 package.json,只不过这次新增下面这个包
npm i @solana/buffer-layout buffer
Solana 程序
还是一样的,在 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); }, );
CICD 脚本
#! /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
创建一个项目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
solana 程序
在 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"]
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也监控到相关日志
作者:加密鲸拓
版权:此文章版权归 加密鲸拓 所有,如有转载,请注明出处!