솔라나 문서프로그램 개발하기Rust 프로그램

Rust 프로그램 구조

Rust로 작성된 Solana 프로그램은 최소한의 구조적 요구사항만 있어 코드 구성 방식에 유연성을 제공합니다. 유일한 요구사항은 프로그램이 entrypoint를 가져야 한다는 것인데, 이는 프로그램 실행이 시작되는 지점을 정의합니다.

프로그램 구조

파일 구조에 대한 엄격한 규칙은 없지만, Solana 프로그램은 일반적으로 다음과 같은 공통 패턴을 따릅니다:

  • entrypoint.rs: 들어오는 명령어를 라우팅하는 엔트리포인트를 정의합니다.
  • state.rs: 프로그램 상태(계정 데이터)를 정의합니다.
  • instructions.rs: 프로그램이 실행할 수 있는 명령어를 정의합니다.
  • processor.rs: 각 명령어에 대한 비즈니스 로직을 구현하는 명령어 핸들러(함수)를 정의합니다.
  • error.rs: 프로그램이 반환할 수 있는 사용자 정의 오류를 정의합니다.

예를 들어, 토큰 프로그램을 참조하세요.

예제 프로그램

여러 명령어가 있는 네이티브 Rust 프로그램을 구축하는 방법을 보여주기 위해, 두 가지 명령어를 구현하는 간단한 카운터 프로그램을 살펴보겠습니다:

  1. InitializeCounter: 초기값으로 새 계정을 생성하고 초기화합니다.
  2. IncrementCounter: 기존 계정에 저장된 값을 증가시킵니다.

간단하게 하기 위해, 프로그램은 단일 lib.rs 파일에 구현될 것입니다. 실제로는 더 큰 프로그램을 여러 파일로 분할하는 것이 좋을 수 있습니다.

파트 1: 프로그램 작성하기

카운터 프로그램을 만들어 봅시다. 시작 값으로 카운터를 초기화하고 증가시킬 수 있는 프로그램을 만들 것입니다.

새 프로그램 만들기

먼저, Solana 프로그램을 위한 새로운 Rust 프로젝트를 만들어 보겠습니다.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

기본 src/lib.rsCargo.toml 파일이 보일 것입니다.

Cargo.tomledition 필드를 2021로 업데이트하세요. 그렇지 않으면 프로그램을 빌드할 때 오류가 발생할 수 있습니다.

의존성 추가하기

이제 Solana 프로그램을 구축하는 데 필요한 의존성을 추가해 보겠습니다. 핵심 SDK를 위한 solana-program와 직렬화를 위한 borsh가 필요합니다.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

Borsh를 사용해야 하는 필수 요건은 없습니다. 그러나 Solana 프로그램에서 일반적으로 사용되는 직렬화 라이브러리입니다.

crate-type 구성하기

Solana 프로그램은 동적 라이브러리로 컴파일되어야 합니다. Cargo가 프로그램을 빌드하는 방법을 구성하기 위해 [lib] 섹션을 추가하세요.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

이 구성을 포함하지 않으면 프로그램을 빌드할 때 target/deploy 디렉토리가 생성되지 않습니다.

프로그램 진입점 설정하기

모든 Solana 프로그램에는 프로그램이 호출될 때 실행되는 함수인 진입점이 있습니다. 프로그램에 필요한 임포트를 추가하고 진입점을 설정하는 것부터 시작해 보겠습니다.

lib.rs에 다음 코드를 추가하세요:

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

entrypoint 매크로는 input 데이터를 process_instruction 함수의 매개변수로 역직렬화하는 작업을 처리합니다.

Solana 프로그램 entrypoint는 다음과 같은 함수 시그니처를 가집니다. 개발자는 자유롭게 entrypoint 함수의 구현을 만들 수 있습니다.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

프로그램 상태 정의하기

이제 카운터 계정에 저장될 데이터 구조를 정의해 보겠습니다. 이 데이터는 계정의 data 필드에 저장됩니다.

다음 코드를 lib.rs에 추가하세요:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

명령어 열거형 정의하기

프로그램이 실행할 수 있는 명령어를 정의해 보겠습니다. 각 변형이 서로 다른 명령어를 나타내는 열거형을 사용할 것입니다.

다음 코드를 lib.rs에 추가하세요:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

명령어 역직렬화 구현하기

이제 instruction_data(원시 바이트)를 우리의 CounterInstruction 열거형 변형 중 하나로 역직렬화해야 합니다. Borsh의 try_from_slice 메서드가 이 변환을 자동으로 처리합니다.

Borsh 역직렬화를 사용하도록 process_instruction 함수를 업데이트하세요:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

명령어를 핸들러로 라우팅하기

이제 명령어를 적절한 핸들러 함수로 라우팅하도록 메인 process_instruction 함수를 업데이트해 보겠습니다.

이러한 라우팅 패턴은 Solana 프로그램에서 일반적입니다. instruction_data는 명령어를 나타내는 열거형의 변형으로 역직렬화된 다음, 적절한 핸들러 함수가 호출됩니다. 각 핸들러 함수는 해당 명령어에 대한 구현을 포함합니다.

process_instruction 함수를 업데이트하고 InitializeCounterIncrementCounter 명령어에 대한 핸들러를 추가하는 다음 코드를 lib.rs에 추가하세요:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

초기화 핸들러 구현하기

새 카운터 계정을 생성하고 초기화하는 핸들러를 구현해 보겠습니다. Solana에서는 System Program만이 계정을 생성할 수 있으므로, 교차 프로그램 호출(Cross Program Invocation, CPI)을 사용하여 우리 프로그램에서 다른 프로그램을 호출할 것입니다.

우리 프로그램은 CPI를 통해 System Program의 create_account 명령어를 호출합니다. 새 계정은 우리 프로그램을 소유자로 생성되어, 우리 프로그램이 계정에 쓰기 작업을 하고 데이터를 초기화할 수 있는 권한을 갖게 됩니다.

다음 코드를 lib.rs에 추가하여 process_initialize_counter 함수를 업데이트하세요:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

이 명령어는 데모 목적으로만 제공됩니다. 프로덕션 프로그램에 필요한 보안 및 유효성 검사가 포함되어 있지 않습니다.

증가 핸들러 구현하기

이제 기존 카운터를 증가시키는 핸들러를 구현해 보겠습니다. 이 명령어는 다음과 같은 작업을 수행합니다:

  • counter_account에 대한 계정 data 필드를 읽습니다
  • 이를 CounterAccount 구조체로 역직렬화합니다
  • count 필드를 1 증가시킵니다
  • CounterAccount 구조체를 다시 계정의 data 필드로 직렬화합니다

다음 코드를 lib.rs에 추가하여 process_increment_counter 함수를 업데이트하세요:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

이 명령어는 데모 목적으로만 제공됩니다. 프로덕션 프로그램에 필요한 보안 및 유효성 검사가 포함되어 있지 않습니다.

완성된 프로그램

축하합니다! 모든 Solana 프로그램이 공유하는 기본 구조를 보여주는 완전한 Solana 프로그램을 구축했습니다:

  • 진입점(Entrypoint): 프로그램 실행이 시작되는 위치를 정의하고 모든 수신 요청을 적절한 명령어 핸들러로 라우팅합니다
  • 명령어 처리(Instruction Handling): 명령어와 관련 핸들러 함수를 정의합니다
  • 상태 관리(State Management): 계정 데이터 구조를 정의하고 프로그램 소유 계정에서 상태를 관리합니다
  • Cross Program Invocation (CPI): 새로운 프로그램 소유 계정을 생성하기 위해 System Program을 호출합니다

다음 단계는 프로그램을 테스트하여 모든 것이 올바르게 작동하는지 확인하는 것입니다.

새 프로그램 만들기

먼저, Solana 프로그램을 위한 새로운 Rust 프로젝트를 만들어 보겠습니다.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

기본 src/lib.rsCargo.toml 파일이 보일 것입니다.

Cargo.tomledition 필드를 2021로 업데이트하세요. 그렇지 않으면 프로그램을 빌드할 때 오류가 발생할 수 있습니다.

의존성 추가하기

이제 Solana 프로그램을 구축하는 데 필요한 의존성을 추가해 보겠습니다. 핵심 SDK를 위한 solana-program와 직렬화를 위한 borsh가 필요합니다.

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

Borsh를 사용해야 하는 필수 요건은 없습니다. 그러나 Solana 프로그램에서 일반적으로 사용되는 직렬화 라이브러리입니다.

crate-type 구성하기

Solana 프로그램은 동적 라이브러리로 컴파일되어야 합니다. Cargo가 프로그램을 빌드하는 방법을 구성하기 위해 [lib] 섹션을 추가하세요.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

이 구성을 포함하지 않으면 프로그램을 빌드할 때 target/deploy 디렉토리가 생성되지 않습니다.

프로그램 진입점 설정하기

모든 Solana 프로그램에는 프로그램이 호출될 때 실행되는 함수인 진입점이 있습니다. 프로그램에 필요한 임포트를 추가하고 진입점을 설정하는 것부터 시작해 보겠습니다.

lib.rs에 다음 코드를 추가하세요:

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

entrypoint 매크로는 input 데이터를 process_instruction 함수의 매개변수로 역직렬화하는 작업을 처리합니다.

Solana 프로그램 entrypoint는 다음과 같은 함수 시그니처를 가집니다. 개발자는 자유롭게 entrypoint 함수의 구현을 만들 수 있습니다.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

프로그램 상태 정의하기

이제 카운터 계정에 저장될 데이터 구조를 정의해 보겠습니다. 이 데이터는 계정의 data 필드에 저장됩니다.

다음 코드를 lib.rs에 추가하세요:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

명령어 열거형 정의하기

프로그램이 실행할 수 있는 명령어를 정의해 보겠습니다. 각 변형이 서로 다른 명령어를 나타내는 열거형을 사용할 것입니다.

다음 코드를 lib.rs에 추가하세요:

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

명령어 역직렬화 구현하기

이제 instruction_data(원시 바이트)를 우리의 CounterInstruction 열거형 변형 중 하나로 역직렬화해야 합니다. Borsh의 try_from_slice 메서드가 이 변환을 자동으로 처리합니다.

Borsh 역직렬화를 사용하도록 process_instruction 함수를 업데이트하세요:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

명령어를 핸들러로 라우팅하기

이제 명령어를 적절한 핸들러 함수로 라우팅하도록 메인 process_instruction 함수를 업데이트해 보겠습니다.

이러한 라우팅 패턴은 Solana 프로그램에서 일반적입니다. instruction_data는 명령어를 나타내는 열거형의 변형으로 역직렬화된 다음, 적절한 핸들러 함수가 호출됩니다. 각 핸들러 함수는 해당 명령어에 대한 구현을 포함합니다.

process_instruction 함수를 업데이트하고 InitializeCounterIncrementCounter 명령어에 대한 핸들러를 추가하는 다음 코드를 lib.rs에 추가하세요:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

초기화 핸들러 구현하기

새 카운터 계정을 생성하고 초기화하는 핸들러를 구현해 보겠습니다. Solana에서는 System Program만이 계정을 생성할 수 있으므로, 교차 프로그램 호출(Cross Program Invocation, CPI)을 사용하여 우리 프로그램에서 다른 프로그램을 호출할 것입니다.

우리 프로그램은 CPI를 통해 System Program의 create_account 명령어를 호출합니다. 새 계정은 우리 프로그램을 소유자로 생성되어, 우리 프로그램이 계정에 쓰기 작업을 하고 데이터를 초기화할 수 있는 권한을 갖게 됩니다.

다음 코드를 lib.rs에 추가하여 process_initialize_counter 함수를 업데이트하세요:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

이 명령어는 데모 목적으로만 제공됩니다. 프로덕션 프로그램에 필요한 보안 및 유효성 검사가 포함되어 있지 않습니다.

증가 핸들러 구현하기

이제 기존 카운터를 증가시키는 핸들러를 구현해 보겠습니다. 이 명령어는 다음과 같은 작업을 수행합니다:

  • counter_account에 대한 계정 data 필드를 읽습니다
  • 이를 CounterAccount 구조체로 역직렬화합니다
  • count 필드를 1 증가시킵니다
  • CounterAccount 구조체를 다시 계정의 data 필드로 직렬화합니다

다음 코드를 lib.rs에 추가하여 process_increment_counter 함수를 업데이트하세요:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

이 명령어는 데모 목적으로만 제공됩니다. 프로덕션 프로그램에 필요한 보안 및 유효성 검사가 포함되어 있지 않습니다.

완성된 프로그램

축하합니다! 모든 Solana 프로그램이 공유하는 기본 구조를 보여주는 완전한 Solana 프로그램을 구축했습니다:

  • 진입점(Entrypoint): 프로그램 실행이 시작되는 위치를 정의하고 모든 수신 요청을 적절한 명령어 핸들러로 라우팅합니다
  • 명령어 처리(Instruction Handling): 명령어와 관련 핸들러 함수를 정의합니다
  • 상태 관리(State Management): 계정 데이터 구조를 정의하고 프로그램 소유 계정에서 상태를 관리합니다
  • Cross Program Invocation (CPI): 새로운 프로그램 소유 계정을 생성하기 위해 System Program을 호출합니다

다음 단계는 프로그램을 테스트하여 모든 것이 올바르게 작동하는지 확인하는 것입니다.

Cargo.toml
lib.rs
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]

파트 2: 프로그램 테스트하기

이제 카운터 프로그램을 테스트해 보겠습니다. 클러스터에 배포하지 않고도 프로그램을 테스트할 수 있는 LiteSVM 테스트 프레임워크를 사용할 것입니다.

테스트 의존성 추가하기

먼저 테스트에 필요한 의존성을 추가해 보겠습니다. 테스트를 위해 litesvmsolana-sdk를 사용할 것입니다.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

테스트 모듈 생성

이제 프로그램에 테스트 모듈을 추가해 보겠습니다. 기본 구조와 임포트부터 시작하겠습니다.

다음 코드를 lib.rs에 프로그램 코드 바로 아래에 추가하세요:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

#[cfg(test)] 속성은 이 코드가 테스트를 실행할 때만 컴파일되도록 보장합니다.

테스트 환경 초기화

LiteSVM으로 테스트 환경을 설정하고 지불자 계정에 자금을 지원해 보겠습니다.

LiteSVM은 Solana 런타임 환경을 시뮬레이션하여 실제 클러스터에 배포하지 않고도 프로그램을 테스트할 수 있게 해줍니다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

프로그램 로드

이제 프로그램을 빌드하고 테스트 환경에 로드해야 합니다. cargo build-sbf 명령을 실행하여 프로그램을 빌드하세요. 이렇게 하면 target/deploy 디렉토리에 counter_program.so 파일이 생성됩니다.

Terminal
$
cargo build-sbf

Cargo.toml에서 edition2021로 설정되어 있는지 확인하세요.

빌드 후에는 프로그램을 로드할 수 있습니다.

test_counter_program 함수를 업데이트하여 프로그램을 테스트 환경에 로드하세요.

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

테스트를 실행하기 전에 cargo build-sbf를 실행하여 .so 파일을 생성해야 합니다. 테스트는 컴파일된 프로그램을 로드합니다.

초기화 명령어 테스트

시작 값으로 새 카운터 계정을 생성하여 초기화 명령어를 테스트해 보겠습니다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

초기화 확인

초기화 후, 카운터 계정이 예상된 값으로 올바르게 생성되었는지 확인해 봅시다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

증가 명령어 테스트

이제 증가 명령어를 테스트하여 카운터 값을 올바르게 업데이트하는지 확인해 봅시다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

최종 결과 확인

마지막으로, 업데이트된 카운터 값을 확인하여 증가가 올바르게 작동했는지 확인해 봅시다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

다음 명령어로 테스트를 실행하세요. --nocapture 플래그는 테스트 출력을 표시합니다.

Terminal
$
cargo test -- --nocapture

예상 출력:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43

테스트 의존성 추가하기

먼저 테스트에 필요한 의존성을 추가해 보겠습니다. 테스트를 위해 litesvmsolana-sdk를 사용할 것입니다.

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

테스트 모듈 생성

이제 프로그램에 테스트 모듈을 추가해 보겠습니다. 기본 구조와 임포트부터 시작하겠습니다.

다음 코드를 lib.rs에 프로그램 코드 바로 아래에 추가하세요:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

#[cfg(test)] 속성은 이 코드가 테스트를 실행할 때만 컴파일되도록 보장합니다.

테스트 환경 초기화

LiteSVM으로 테스트 환경을 설정하고 지불자 계정에 자금을 지원해 보겠습니다.

LiteSVM은 Solana 런타임 환경을 시뮬레이션하여 실제 클러스터에 배포하지 않고도 프로그램을 테스트할 수 있게 해줍니다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

프로그램 로드

이제 프로그램을 빌드하고 테스트 환경에 로드해야 합니다. cargo build-sbf 명령을 실행하여 프로그램을 빌드하세요. 이렇게 하면 target/deploy 디렉토리에 counter_program.so 파일이 생성됩니다.

Terminal
$
cargo build-sbf

Cargo.toml에서 edition2021로 설정되어 있는지 확인하세요.

빌드 후에는 프로그램을 로드할 수 있습니다.

test_counter_program 함수를 업데이트하여 프로그램을 테스트 환경에 로드하세요.

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

테스트를 실행하기 전에 cargo build-sbf를 실행하여 .so 파일을 생성해야 합니다. 테스트는 컴파일된 프로그램을 로드합니다.

초기화 명령어 테스트

시작 값으로 새 카운터 계정을 생성하여 초기화 명령어를 테스트해 보겠습니다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

초기화 확인

초기화 후, 카운터 계정이 예상된 값으로 올바르게 생성되었는지 확인해 봅시다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

증가 명령어 테스트

이제 증가 명령어를 테스트하여 카운터 값을 올바르게 업데이트하는지 확인해 봅시다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

최종 결과 확인

마지막으로, 업데이트된 카운터 값을 확인하여 증가가 올바르게 작동했는지 확인해 봅시다.

다음 코드를 lib.rs에 추가하여 test_counter_program 함수를 업데이트하세요:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

다음 명령어로 테스트를 실행하세요. --nocapture 플래그는 테스트 출력을 표시합니다.

Terminal
$
cargo test -- --nocapture

예상 출력:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"

파트 3: 프로그램 호출하기

이제 프로그램을 호출하기 위한 클라이언트 스크립트를 추가해 봅시다.

클라이언트 예제 생성하기

배포된 프로그램과 상호작용할 Rust 클라이언트를 만들어 보겠습니다.

Terminal
$
mkdir examples
$
touch examples/client.rs

다음 구성을 Cargo.toml에 추가하세요:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

클라이언트 의존성을 설치하세요:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

클라이언트 코드 구현하기

이제 배포된 프로그램을 호출할 클라이언트를 구현해 보겠습니다.

keypair 파일에서 프로그램 ID를 가져오기 위해 다음 명령을 실행하세요:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

클라이언트 코드를 examples/client.rs에 추가하고 program_id를 이전 명령의 출력으로 대체하세요:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
examples/client.rs
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
use std::str::FromStr;
use counter_program::CounterInstruction;
#[tokio::main]
async fn main() {
// Replace with your actual program ID from deployment
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
// Connect to local cluster
let rpc_url = String::from("http://localhost:8899");
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
// Generate a new keypair for paying fees
let payer = Keypair::new();
// Request airdrop of 1 SOL for transaction fees
println!("Requesting airdrop...");
let airdrop_signature = client
.request_airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to request airdrop");
// Wait for airdrop confirmation
loop {
if client
.confirm_transaction(&airdrop_signature)
.unwrap_or(false)
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
println!("Airdrop confirmed");
println!("\nInitializing counter...");
let counter_keypair = Keypair::new();
let initial_value = 100u64;
// Serialize the initialize instruction data
let instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
let blockhash = client
.get_latest_blockhash()
.expect("Failed to get blockhash");
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter initialized!");
println!("Transaction: {}", signature);
println!("Counter address: {}", counter_keypair.pubkey());
}
Err(err) => {
eprintln!("Failed to initialize counter: {}", err);
return;
}
}
println!("\nIncrementing counter...");
// Serialize the increment instruction data
let increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter incremented!");
println!("Transaction: {}", signature);
}
Err(err) => {
eprintln!("Failed to increment counter: {}", err);
}
}
}

클라이언트 예제 생성하기

배포된 프로그램과 상호작용할 Rust 클라이언트를 만들어 보겠습니다.

Terminal
$
mkdir examples
$
touch examples/client.rs

다음 구성을 Cargo.toml에 추가하세요:

Cargo.toml
[[example]]
name = "client"
path = "examples/client.rs"

클라이언트 의존성을 설치하세요:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

클라이언트 코드 구현하기

이제 배포된 프로그램을 호출할 클라이언트를 구현해 보겠습니다.

keypair 파일에서 프로그램 ID를 가져오기 위해 다음 명령을 실행하세요:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

클라이언트 코드를 examples/client.rs에 추가하고 program_id를 이전 명령의 출력으로 대체하세요:

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"
solana-client = "2.2.0"
tokio = "1.47.1"
[[example]]
name = "client"
path = "examples/client.rs"

파트 4: 프로그램 배포하기

이제 프로그램과 클라이언트가 준비되었으니 프로그램을 빌드하고, 배포하고, 호출해 보겠습니다.

프로그램 빌드하기

먼저 프로그램을 빌드해 보겠습니다.

Terminal
$
cargo build-sbf

이 명령어는 프로그램을 컴파일하고 target/deploy/에 두 개의 중요한 파일을 생성합니다:

counter_program.so # The compiled program
counter_program-keypair.json # Keypair for the program ID

다음 명령어를 실행하여 프로그램의 ID를 확인할 수 있습니다:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

출력 예시:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

로컬 validator 시작하기

개발을 위해 로컬 테스트 validator를 사용할 것입니다.

먼저, Solana CLI를 localhost로 구성합니다:

Terminal
$
solana config set -ul

출력 예시:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed

이제 별도의 터미널에서 테스트 validator를 시작합니다:

Terminal
$
solana-test-validator

프로그램 배포하기

validator가 실행 중인 상태에서 로컬 클러스터에 프로그램을 배포합니다:

Terminal
$
solana program deploy ./target/deploy/counter_program.so

출력 예시:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

프로그램 ID와 함께 solana program show 명령어를 사용하여 배포를 확인할 수 있습니다:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

출력 예시:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuM
Authority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1
Last Deployed In Slot: 16
Data Length: 82696 (0x14308) bytes
Balance: 0.57676824 SOL

클라이언트 실행하기

로컬 validator가 계속 실행 중인 상태에서 클라이언트를 실행하세요:

Terminal
$
cargo run --example client

예상 출력:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing counter...
Counter incremented!
Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS

로컬 validator가 실행 중일 때, 출력된 트랜잭션 서명을 사용하여 Solana Explorer에서 트랜잭션을 확인할 수 있습니다. Solana Explorer에서 클러스터를 "Custom RPC URL"로 설정해야 하며, 기본값은 http://localhost:8899solana-test-validator가 실행 중인 주소입니다.

Is this page helpful?