Ana içeriğe geç

Yerel Rust Programı Yazma Rehberi

Anchor çerçevesini kullanmadan Solana programları yazmak için solana_program kütüphanesini kullanıyoruz. Bu, Rust'ta onchain programlar yazmak için temel kütüphanedir.

Yeni başlayanlar için, Anchor çerçevesi ile başlamak önerilir.

Program

Aşağıda, yeni bir hesap oluşturmak için tek bir talimat içeren basit bir Solana programı bulunmaktadır. Solana programının temel yapısını açıklamak için adım adım ilerleyeceğiz. İşte program Solana Playground üzerinde.

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!("Veriyi değiştirildi: {:?}!", data);
Ok(())
}

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

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

Giriş Noktası

Her Solana programı, programı çağırmak için kullanılan tek bir giriş noktası içerir. process_instruction fonksiyonu, giriş noktasına geçirilen verileri işlemek için kullanılır. Bu fonksiyon, aşağıdaki parametreleri gerektirir:

  • program_id - Geçerli programın adresi
  • accounts - Bir talimatı yürütmek için gereken hesaplar dizisi.
  • instruction_data - Belirli bir talimata özel serileştirilmiş veriler.
entrypoint!(process_instruction);

pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
...
}

Bu parametreler, bir talimat için gereken ayrıntılara karşılık gelir.

Talimatlar

Sadece bir giriş noktası olmasına rağmen, program yürütme instruction_data'ya bağlı olarak farklı yollar izleyebilir. Talimatları, programda ayrı bir talimatı temsil eden bir enum içinde değişkenler olarak tanımlamak yaygındır.

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

Giriş noktasına geçirilen instruction_data, karşılık gelen enum değişkenini belirlemek için serileştirilir.

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

Bir match ifadesi, tanımlanan talimatı işleme mantığını içeren fonksiyonu çağırmak için kullanılır. Bu fonksiyonlar genellikle talimat işleyiciler olarak adlandırılır.

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

Talimatı İşleme

Her programda, o talimatı gerçekleştirmek için gereken mantığı uygulayan belirli bir talimat işleyici fonksiyon bulunur.

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!("Veriyi değiştirildi: {:?}!", data);
Ok(())
}

Programa sağlanan hesaplara erişmek için, accounts argümanı aracılığıyla giriş noktasına geçirilen hesaplar listesini yinelemek için bir iteratör kullanın. next_account_info fonksiyonu, iteratördeki bir sonraki ögeyi erişmek için kullanılır.

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

Yeni bir hesap oluşturmak, Sistem Programı üzerindeki create_account talimatını çağırmayı gerektirir. Sistem Programı yeni bir hesap oluşturduğunda, yeni hesabın program sahibini atayabilir.

bilgi

Bu örnekte, Çapraz Program Çağrısı kullanarak Sistem Programını çağırıyor ve çalıştıran programı sahibi olarak belirterek yeni bir hesap oluşturuyoruz. Solana Hesap Modeli gereğince, yalnızca bir hesabın sahibi olarak atanmış program, hesap üzerindeki verileri değiştirmeye yetkilidir.

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, // ödeyici
new_account.key, // yeni hesap adresi
lamports, // kira
size as u64, // alan
program_id, // program sahibi adresi
),
&[signer.clone(), new_account.clone(), system_program.clone()],
)?;

Hesap başarıyla oluşturulduktan sonra, son adım yeni hesabın data alanına verileri serileştirmektir. Bu, hesabın verilerini başlatır ve giriş noktasına geçirilen datayı depolar.

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

Durum

Yapılar, bir program için özel bir veri hesap türünün formatını tanımlamak için kullanılır. Hesap verilerinin serileştirilmesi ve serileştirmeden çıkarılması genellikle Borsh kullanılarak yapılır.

Bu örnekte, NewAccount yapısı yeni bir hesapta depolanacak verinin yapısını tanımlar.

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

Tüm Solana hesapları, rastgele verileri bir bayt dizisi olarak depolamak için kullanılabilecek bir data alanı içerir. Bu esneklik, programların yeni hesaplar içinde özelleştirilmiş veri yapıları oluşturmalarına ve depolamalarına olanak tanır.

process_initialize fonksiyonunda, giriş noktasına geçirilen veri, NewAccount yapısının bir örneğini oluşturmak için kullanılır. Bu örnek serileştirilir ve yeni oluşturulan hesabın veri alanında depolanır.

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!("Veriyi değiştirildi: {:?}!", data);
Ok(())
}
...

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

İstemci

Yerel Rust'ta yazılmış Solana programlarıyla etkileşim, doğrudan TransactionInstruction oluşturmayı içerir.

Benzer şekilde, hesap verilerini almak ve serileştirmek, zincir üzerindeki programın veri yapıları ile uyumlu bir şema oluşturmayı gerektirir.

Desteklenen çeşitli istemci dilleri vardır. Ayrıntıları, Rust ve Javascript/Typescript için Solana İstemcileri altında bulabilirsiniz.

Aşağıda, yukarıdaki programdan initialize talimatını nasıl çağıracağımıza dair bir örneği inceleyeceğiz.

describe("Test", () => {
it("Initialize", async () => {
// Yeni hesap için anahtar çifti oluştur
const newAccountKp = new web3.Keypair();

const instructionIndex = 0;
const data = 42;

// Talimat veri tamponu oluştur
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(`Logları görmek için 'solana confirm -v ${txHash}' kullanın`);

// Hesabı Al
const newAccount = await pg.connection.getAccountInfo(
newAccountKp.publicKey,
);

// Hesap Verisini Serileştir
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"]] }],
]);

Talimatları Çağırma

Bir talimatı çağırmak için, zincir üzerindeki programla eşleşen bir TransactionInstruction oluşturmanız gerekir. Bu, aşağıdakileri belirtmeyi içerir:

  • Çağrılan program için program ID'si
  • Talimat tarafından gereken her hesap için AccountMeta
  • Talimat tarafından gereken talimat veri tamponu
// Yeni hesap için anahtar çifti oluştur
const newAccountKp = new web3.Keypair();

const instructionIndex = 0;
const data = 42;

// Talimat veri tamponu oluştur
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,
});

Öncelikle yeni bir anahtar çifti oluşturun. Bu anahtar çiftinin publicKey'i, initialize talimatı tarafından oluşturulacak yeni hesabın adresi olarak kullanılacaktır.

// Yeni hesap için anahtar çifti oluştur
const newAccountKp = new web3.Keypair();

Talimat oluşturulmadan önce, talimatın beklediği talimat veri tamponunu hazırlayın. Bu örnekte, tamponun ilk baytı program üzerindeki hangi talimatın çağrılacağını tanımlar. Ek olarak 8 bayt, initialize talimatı tarafından gerekli olan u64 tip veri için ayrılmıştır.

const instructionIndex = 0;
const data = 42;

// Talimat veri tamponu oluştur
const instructionData = Buffer.alloc(1 + 8);
instructionData.writeUInt8(instructionIndex, 0);
instructionData.writeBigUInt64LE(BigInt(data), 1);

Talimat veri tamponunu oluşturduktan sonra, bunu TransactionInstruction oluşturmak için kullanın. Bu, program ID'sini belirtmeyi ve talimatta yer alan her hesap için AccountMeta tanımlamayı içerir. Bu, her hesabın yazılabilir durumu ve işlemde bir imzacı olup olmadığını belirtmeyi içerir.

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,
});

Son olarak, talimatı yeni bir işleme ekleyin ve ağ tarafından işlenmesi için gönderin.

const transaction = new web3.Transaction().add(instruction);

const txHash = await web3.sendAndConfirmTransaction(
pg.connection,
transaction,
[pg.wallet.keypair, newAccountKp],
);
console.log(`Logları görmek için 'solana confirm -v ${txHash}' kullanın`);

Hesapları Alma

Hesap verilerini almak ve serileştirmek için, önce beklenen zincir üzerindeki hesap verileriyle uyumlu bir şemayı oluşturmalısınız.

class AccountData {
data = 0;
constructor(fields: { data: number }) {
if (fields) {
this.data = fields.data;
}
}
}

const AccountDataSchema = new Map([
[AccountData, { kind: "struct", fields: [["data", "u64"]] }],
]);

Daha sonra, hesabın adresini kullanarak AccountInfo'yu alın.

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

Son olarak, önceden tanımlanmış şemayı kullanarak AccountInfo'nun data alanını serileştirin.

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

console.log(Number(deserializedAccountData.data));