Solana入门实战

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

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

最后会生成密钥对,公钥和助记词,请注意保管好 Code

 

常用命令

更新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

Code

查看当前账户公钥

solana address

空投一个 solana 并查询 solana 余额

solana airdrop 1
solana balance

查询有programs

solana program show --programs

删除某个program

solana program close programid --bypass-warning

如果airdrop 限速了,也可以直接来官网领空投

https://faucet.solana.com/

 

账户

参考文档: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)

image-20241109174235080

 

点击左下角链接 Solana 网络,如果之前没有创建wallet 钱包,Playground 会自动为我们创建,或者选择导入私钥生成我们指定的钱包。界面的底部和右上角会展示连接的网络和钱包余额信息。

image-20241109174440532

 

将上面的代码粘贴到 src/lib.rs 里,然后点击 Build

编译完成后,我们可以在左侧第二个菜单进行deploy部署(部署成功后,就会变成Upgrade按钮),并且在下方会显示部署的详情及结果,在这还能看到 Programe ID

image-20241109174610542

 

接着修改 client.ts

image-20241109174644990

代码如下

// 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 之后,你就能在在浏览器中可以看到日志了

image-20241109160720354

 

本地部署调用

在program目录下执行编译命令,构建可在 Solana 集群部署的链上程序的 BPF 字节码文件

cargo build-bpf

编译过程中可能出现报错,注意观察报错信息,如果缺了什么库,就安装对应的库文件就好(比如bzip2)

成功之后你会在target/deploy目录下看到这个文件(第一次成功了没出现的话可以再编译一次)

project framework

 

随后我们可以用这个命令部署到solana devnet中

solana program deploy <hello_solana.so的路径>

code

 

部署成功之后通过如下命令查询你部署到solana网络上的程序(时间会有点久,如果报错就多试几次)

solana program show --programs

code

 

在 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 文件

code

写入如下代码,大致逻辑是读取密钥对,创建公钥,获取空投,触发交易

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

code

 

可以另外开一个客户端,用如下命令监控 solana 链上的日志

solana logs | grep "3MCZC3HuC3Yuz23m29HbxCYjq2k5FSXvhgF81oiTiFXN invoke" -A 3

它会持续监听,直到出现相关日志才会打印出来

然后我们执行 npm run start 命令,它就会调用 solana 链上的程序,此时 solana 链上也就会出现对应的日志了

code

 

Counter

framework of Persisting Data

这个图展示了案例项目结构,我们会编写 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;
    // 然后将 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 结构体的实例。

  • &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 区块链浏览器中查看详细信息

image-20241109175005748

 

点击 program id,还能看到程序账户和子账户(存储程序二进制文件)之间的关系。

image-20241109175106857

 

在 Client/tests 下写入测试脚本,测试脚本的主要内容如下

image-20241109175223635

 

    创建计数器对象

定义一个 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 按钮,就可以验证逻辑了

image-20241109175814853

 

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 输出如下

code

 

solana 监控日志如下

code

 

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 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)

code

solana也监控到相关日志

code

  • 原创
  • 学分: 5
  • 分类: Solana
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
加密鲸拓
加密鲸拓
现Golang 后台开发,Web3 技术爱好者,承接各类合作,欢迎联系