如何编写原生 Rust 程序

要在不使用 Anchor 框架的情况下编写 Solana 程序,我们使 用solana_program crate。This is the base library for writing onchain programs in Rust.

对于初学者,建议从 Anchor 框架开始。

程序 #

下面是一个简单的 Solana 程序,包含一个创建新账户的指令。 我们将逐步讲解 Solana 程序的基本结构。 以下是在 Solana Playground 上的程序。

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction::create_account,
    sysvar::Sysvar,
};
 
entrypoint!(process_instruction);
 
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = Instructions::try_from_slice(instruction_data)?;
    match instruction {
        Instructions::Initialize { data } => process_initialize(program_id, accounts, data),
    }
}
 
pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
 
    let new_account = next_account_info(accounts_iter)?;
    let signer = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;
 
    let account_data = NewAccount { data };
    let size = account_data.try_to_vec()?.len();
    let lamports = (Rent::get()?).minimum_balance(size);
 
    invoke(
        &create_account(
            signer.key,
            new_account.key,
            lamports,
            size as u64,
            program_id,
        ),
        &[signer.clone(), new_account.clone(), system_program.clone()],
    )?;
 
    account_data.serialize(&mut *new_account.data.borrow_mut())?;
    msg!("Changed data to: {:?}!", data);
    Ok(())
}
 
#[derive(BorshSerialize, BorshDeserialize)]
pub enum Instructions {
    Initialize { data: u64 },
}
 
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct NewAccount {
    pub data: u64,
}
 

入口点 #

每个 Solana 程序都包含一个用于调用程序 的入口点 。 然后使用 process_instruction 函数处理传递到入口点的数据。 此函数需要以下参数:

  • program_id - 当前执行程序的地址
  • accounts - 执行指令所需的账户数组
  • instruction_data - 特定于指令的序列化数据
entrypoint!(process_instruction);
 
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    ...
}

这些参数对应于交易中每个指令所需的详细 信息。

指令 #

虽然只有一个入口点,但程序执行可以根据instruction_data遵循不同的路径。 通常将 指令定义为枚举中 的变体,每个变体代表程序中的一个独特指令。

#[derive(BorshSerialize, BorshDeserialize)]
pub enum Instructions {
    Initialize { data: u64 },
}

传递到入口点的instruction_data被反序列化以确定其对应的枚举变体。

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = Instructions::try_from_slice(instruction_data)?;
    match instruction {
        Instructions::Initialize { data } => process_initialize(program_id, accounts, data),
    }
}

然后使用 match 语句调用包含 处理识别指令逻辑的函数。 这些函数通常称 为指令处理器

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = Instructions::try_from_slice(instruction_data)?;
    match instruction {
        Instructions::Initialize { data } => process_initialize(program_id, accounts, data),
    }
}
 
pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
    ...
    Ok(())
}

处理指令 #

对于程序中的每个指令,都存在一个特定的指令处理器函数来实现执行该指令所需的逻辑。

pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
 
    let new_account = next_account_info(accounts_iter)?;
    let signer = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;
 
    let account_data = NewAccount { data };
    let size = account_data.try_to_vec()?.len();
    let lamports = (Rent::get()?).minimum_balance(size);
 
    invoke(
        &create_account(
            signer.key,
            new_account.key,
            lamports,
            size as u64,
            program_id,
        ),
        &[signer.clone(), new_account.clone(), system_program.clone()],
    )?;
 
    account_data.serialize(&mut *new_account.data.borrow_mut())?;
    msg!("Changed data to: {:?}!", data);
    Ok(())
}

要访问提供给程序的账户,请使 用迭代器遍历通 过accounts参数传递到入口点的账户列表。 使用 next_account_info 函数访问迭代器中的下一个项目。

let accounts_iter = &mut accounts.iter();
 
let new_account = next_account_info(accounts_iter)?;
let signer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;

创建新账户需要调用系统程序中的 create_account 指令。 当系统程序创建新账户时,它可以重新分配新账户的程序所有者。

在此示例中,我们使用跨程序调用调用系统程序,创建一个以执行 程序为owner的新账户。 作为 Solana 账户模型的一部分,只有被指定为账 户owner的程序才允许修改账户上的数据。

let account_data = NewAccount { data };
let size = account_data.try_to_vec()?.len();
let lamports = (Rent::get()?).minimum_balance(size);
 
invoke(
    &create_account(
        signer.key,      // payer
        new_account.key, // new account address
        lamports,        // rent
        size as u64,     // space
        program_id,      // program owner address
    ),
    &[signer.clone(), new_account.clone(), system_program.clone()],
)?;

成功创建账户后,最后一步是将数据序列化到新账户的data字段中。 这实际上初始化了 账户数据,存储传递到程序入口点的data

account_data.serialize(&mut *new_account.data.borrow_mut())?;

状态 #

结构体用于定义程序自定义数据账户类型的格式。 账户数据的序列化和反序列化通常使用 Borsh

在此示例中,NewAccount结构体定义了要存储在新账户中的数据结构。

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct NewAccount {
    pub data: u64,
}

所有 Solana 账户都包含一个 data 字段,可 以用作字节数组存储任意数据。 这种灵活性使程序能够在新账户中创建和存储自定义数据 结构。

process_initialize函数中,传递到入口点的数据用于创建NewAccount结构体的实 例。 此实例被序列化并存储在新创建账户的数据字段中。

pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
 
    let account_data = NewAccount { data };
 
    invoke(
        ...
    )?;
 
    account_data.serialize(&mut *new_account.data.borrow_mut())?;
    msg!("Changed data to: {:?}!", data);
    Ok(())
}
...
 
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct NewAccount {
    pub data: u64,
}

客户端 #

与用原生 Rust 编写的 Solana 程序交互涉及直接构建 TransactionInstruction

同样,获取和反序列化账户数据需要创建与链上程序数据结构兼容的模式。

Info

支持多种客户端语言。 你可以在文档的 Solana 客户端部分找 到详细信息Javascript/Typescript

下面,我们将演示如何调用上述程序中的initialize指令。

native.test.ts
describe("Test", () => {
  it("Initialize", async () => {
    // Generate keypair for the new account
    const newAccountKp = new web3.Keypair();
 
    const instructionIndex = 0;
    const data = 42;
 
    // Create instruction data buffer
    const instructionData = Buffer.alloc(1 + 8);
    instructionData.writeUInt8(instructionIndex, 0);
    instructionData.writeBigUInt64LE(BigInt(data), 1);
 
    const instruction = new web3.TransactionInstruction({
      keys: [
        {
          pubkey: newAccountKp.publicKey,
          isSigner: true,
          isWritable: true,
        },
        {
          pubkey: pg.wallet.publicKey,
          isSigner: true,
          isWritable: true,
        },
        {
          pubkey: web3.SystemProgram.programId,
          isSigner: false,
          isWritable: false,
        },
      ],
      programId: pg.PROGRAM_ID,
      data: instructionData,
    });
 
    const transaction = new web3.Transaction().add(instruction);
 
    const txHash = await web3.sendAndConfirmTransaction(
      pg.connection,
      transaction,
      [pg.wallet.keypair, newAccountKp],
    );
    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
 
    // Fetch Account
    const newAccount = await pg.connection.getAccountInfo(
      newAccountKp.publicKey,
    );
 
    // Deserialize Account Data
    const deserializedAccountData = borsh.deserialize(
      AccountDataSchema,
      AccountData,
      newAccount.data,
    );
 
    console.log(Number(deserializedAccountData.data));
  });
});
 
class AccountData {
  data = 0;
  constructor(fields: { data: number }) {
    if (fields) {
      this.data = fields.data;
    }
  }
}
 
const AccountDataSchema = new Map([
  [AccountData, { kind: "struct", fields: [["data", "u64"]] }],
]);

调用指令 #

要调用指令,必须手动构建与链上程序对应的TransactionInstruction。 这涉及指定:

  • 被调用程序的程序 ID
  • 每个指令所需账户的AccountMeta
  • 指令所需的指令数据缓冲区
// Generate keypair for the new account
const newAccountKp = new web3.Keypair();
 
const instructionIndex = 0;
const data = 42;
 
// Create instruction data buffer
const instructionData = Buffer.alloc(1 + 8);
instructionData.writeUInt8(instructionIndex, 0);
instructionData.writeBigUInt64LE(BigInt(data), 1);
 
const instruction = new web3.TransactionInstruction({
  keys: [
    {
      pubkey: newAccountKp.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: pg.wallet.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: web3.SystemProgram.programId,
      isSigner: false,
      isWritable: false,
    },
  ],
  programId: pg.PROGRAM_ID,
  data: instructionData,
});

首先,创建一个新的密钥对。 首先,创建一个新的密钥对。此密钥对的公钥将用 作initialize指令创建的新账户的地址。

// Generate keypair for the new account
const newAccountKp = new web3.Keypair();

在构建指令之前,准备指令所需的指令数据缓冲区。 在此示例中,缓冲区的第一个字节标 识要在程序上调用的指令。 额外的 8 个字节分配给u64类型数据,这是initialize指 令所需的。

const instructionIndex = 0;
const data = 42;
 
// Create instruction data buffer
const instructionData = Buffer.alloc(1 + 8);
instructionData.writeUInt8(instructionIndex, 0);
instructionData.writeBigUInt64LE(BigInt(data), 1);

创建指令数据缓冲区后,使用它构建TransactionInstruction。 这涉及指定程序 ID 并 定义每个指令所涉及账户的 AccountMeta。 这意味着指定每个账户是 否可写以及是否需要作为交易的签名者。

const instruction = new web3.TransactionInstruction({
  keys: [
    {
      pubkey: newAccountKp.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: pg.wallet.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: web3.SystemProgram.programId,
      isSigner: false,
      isWritable: false,
    },
  ],
  programId: pg.PROGRAM_ID,
  data: instructionData,
});

最后,将指令添加到新交易中并发送以供网络处理。

const transaction = new web3.Transaction().add(instruction);
 
const txHash = await web3.sendAndConfirmTransaction(
  pg.connection,
  transaction,
  [pg.wallet.keypair, newAccountKp],
);
console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);

获取账户 #

要获取和反序列化账户数据,你需要首先创建一个方案来匹配预期的链上账户数据。

class AccountData {
  data = 0;
  constructor(fields: { data: number }) {
    if (fields) {
      this.data = fields.data;
    }
  }
}
 
const AccountDataSchema = new Map([
  [AccountData, { kind: "struct", fields: [["data", "u64"]] }],
]);

然后使用账户的地址获取AccountInfo

const newAccount = await pg.connection.getAccountInfo(newAccountKp.publicKey);

最后,使用预定义的模式反序列化AccountInfodata字段。

const deserializedAccountData = borsh.deserialize(
  AccountDataSchema,
  AccountData,
  newAccount.data,
);
 
console.log(Number(deserializedAccountData.data));