要在不使用 Anchor 框架的情况下编写 Solana 程序,我们使
用solana_program
crate。This is the base library for writing onchain programs in Rust.
For beginners, it is recommended to start with the Anchor framework.
程序 #
下面是一个简单的 Solana 程序,包含一个创建新账户的指令。 我们将逐步讲解 Solana 程序的基本结构。 以下是在 Solana Playground 上的程序。
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,
}
客户端 #
Interacting with Solana programs written in native Rust involves directly
building the
TransactionInstruction
.
同样,获取和反序列化账户数据需要创建与链上程序数据结构兼容的模式。
支持多种客户端语言。 你可以在文档的 Solana 客户端部分找 到详细信息和Javascript/Typescript。
下面,我们将演示如何调用上述程序中的initialize
指令。
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);
最后,使用预定义的模式反序列化AccountInfo
的data
字段。
const deserializedAccountData = borsh.deserialize(
AccountDataSchema,
AccountData,
newAccount.data,
);
console.log(Number(deserializedAccountData.data));