첫 번째 Solana 프로그램 빌드하기

이 퀵스타트는 anchor init로 생성된 Anchor 프레임워크 스타터 프로젝트를 사용합니다. 프로젝트를 로컬에 생성하고, 테스트를 실행하고, 프로그램을 빌드하며, Anchor가 생성하는 프로그램 코드를 살펴봅니다.

사전 요구 사항

시작하기 전에 Solana 개발 도구를 설치하세요. 설치 항목에는 Rust, Solana CLI, Anchor CLI가 포함됩니다.

이 템플릿에는 Anchor CLI 버전 1.1.2 이상을 사용하세요. 설치된 버전을 확인하려면:

Terminal
$
anchor --version

프로젝트 생성

터미널에서 다음 명령어를 실행하세요:

Terminal
$
anchor init my-program
$
cd my-program

스타터 프로젝트에는 programs/my-program 아래에 Solana 프로그램이 하나 포함되어 있습니다. 이 프로그램에는 두 가지 명령어가 있습니다: 카운터 계정을 초기화하는 명령어와 카운터를 증가시키는 명령어입니다.

템플릿의 일부 코드는 일반적인 Solana 프로그램 패턴을 보여줍니다: PDA 계정 주소 도출, SOL을 전송하기 위한 Cross Program Invocation(CPI) 수행, 그리고 조건 실패 시 명령어를 중단하기 위한 커스텀 오류 검사 사용.

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

프로그램 빌드

anchor build를 실행하여 스타터 프로그램을 컴파일하세요:

Terminal
$
anchor build

컴파일된 프로그램은 target/deploy/my_program.so에 저장됩니다. 프로그램이 배포되면 이 .so 파일의 내용이 온체인 계정에 저장됩니다.

테스트 실행

기본 테스트를 실행합니다:

Terminal
$
anchor test

이 템플릿의 Anchor.toml는 Rust 테스트 명령을 사용합니다:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

테스트는 컴파일된 프로그램을 LiteSVM에 로드하고, 페이어를 생성하여 initializeincrement 명령어를 전송한 후, 카운터 계정 상태를 확인합니다.

anchor test를 실행하면 프로그램도 함께 컴파일되므로, 로컬에서 테스트할 때 먼저 anchor build를 실행할 필요가 없습니다.

프로그램 배포

로컬 테스트는 가장 빠른 피드백 루프입니다. devnet과 같은 네트워크에 배포할 준비가 되었다면, 먼저 빌드한 후 클러스터에 배포하세요.

Solana 프로그램을 배포하려면 SOL이 필요합니다. 프로그램이 계정에 저장되고, 해당 계정은 사용하는 공간에 대한 비용을 지불해야 하기 때문입니다. devnet에서는 Solana Faucet을 이용하거나 Solana CLI로 무료 devnet SOL을 요청할 수 있습니다:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

소스 파일

src 디렉터리에는 Solana 프로그램이 포함되어 있습니다. Anchor의 Program Structure 문서에서는 declare_id!, #[program], #[derive(Accounts)], #[account] 등 여기서 사용된 핵심 매크로를 설명합니다. 이 섹션에서는 템플릿 파일들을 살펴봅니다.

lib.rs

lib.rs는 프로그램 진입점입니다. 소스 파일을 연결하고, 프로그램 주소를 정의하며, 사용자가 호출할 수 있는 프로그램 명령어를 정의합니다.

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

동일한 프로그램 주소가 설정과 코드 모두에 나타납니다. Anchor.toml는 Anchor에게 클러스터에 배포하거나 호출할 주소를 알려줍니다. *rsdeclare_id!*는 보안 검사를 위해 프로그램 내에서 프로그램 주소를 정의합니다.

constants.rs

constants.rs는 공유 값을 한 곳에 보관합니다. 이 템플릿에서 *rsCOUNTER_SEED*는 카운터 PDA를 도출하고, *rsHELLO_WORLD_LAMPORTS*는 초기화 시 전송되며, *rsMAX_COUNT*는 증가 전에 확인됩니다.

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs는 두 가지 상수를 사용합니다:

  • COUNTER_SEED는 카운터 PDA 주소를 도출합니다.
  • HELLO_WORLD_LAMPORTS는 지불자에서 카운터 계정으로 전송되는 금액을 설정합니다.
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rsMAX_COUNT를 카운터의 상한값으로 사용합니다. 현재 카운트가 이미 최댓값에 도달한 경우, *rsrequire!*는 *rsCounterOverflow*를 반환하고 계정 데이터는 변경되지 않습니다.

state.rs

state.rs는 프로그램이 생성하고 소유하는 계정에 대한 커스텀 데이터 타입을 정의합니다. 프로그램은 해당 데이터를 생성, 초기화 및 업데이트하는 명령어를 정의하지만, 카운터 데이터는 프로그램 자체에 저장되지 않습니다. 이는 고유한 주소를 가진 별도의 계정에 저장됩니다.

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rsCounter 계정 데이터를 정의합니다. 명령어 파일은 계정을 생성하고 업데이트할 때 해당 타입을 사용합니다:

  • Counter::INIT_SPACEstate.rs에 정의된 필드에 맞게 계정 크기를 조정합니다.
  • countauthority는 계정이 초기화될 때 기록되는 필드 값입니다.
  • count += 1는 유효성 검사를 통과한 후 저장된 카운터 값을 업데이트합니다.

error.rs

error.rs는 프로그램의 커스텀 오류를 정의합니다. 이 템플릿에서 오류는 호출자가 카운터를 업데이트할 권한이 없거나 카운터가 이미 *rsMAX_COUNT*에 도달했을 때 명령어 핸들러가 중단되는 방식을 보여줍니다.

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs는 명령어가 반환할 수 있는 오류를 명시합니다:

  • ErrorCode::Unauthorized는 서명자가 카운터 계정에 저장된 권한자가 아닐 때 반환됩니다.
  • ErrorCode::CounterOverflow는 카운터가 이미 *rsMAX_COUNT*에 도달했을 때 반환됩니다.

instructions.rs

instructions.rs는 명령어 파일들을 프로그램 크레이트에 연결하여 lib.rsinitializeincrement 명령어 코드에 접근할 수 있도록 합니다. 각 명령어 파일은 해당 명령어에 필요한 계정과 Anchor가 해당 계정을 검증한 후 실행되는 핸들러 로직을 정의합니다.

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs는 카운터 계정을 생성하는 데 필요한 계정을 정의하고, 계정의 초기 값을 기록합니다. #[derive(Accounts)] 구조체는 Anchor account constraints를 사용하여 어떤 계정이 필요한지, 새로운 카운터 계정이 어떻게 생성되는지를 지정합니다.

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

Initialize 구조체는 사용자가 initialize 명령어를 호출할 때 포함되어야 하는 계정을 정의합니다. Anchor는 핸들러가 실행되기 전에 이 계정들을 검증합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Payer Account

payer 계정은 카운터 계정 생성 비용을 지불합니다. Signer<'info> 타입은 지불자가 트랜잭션에 서명해야 함을 의미하며, *rs#[account(mut)]*는 lamport가 차감되므로 지불자 계정이 변경될 수 있음을 의미합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Counter Account

counter 계정은 state.rsCounter 데이터를 저장합니다. *rsinit*는 핸들러가 실행되기 전에 이 계정을 생성하도록 Anchor에 지시하고, *rspayer = payer*는 생성 비용을 지불할 계정을 Anchor에 알려줍니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

계정 크기

space 제약 조건은 Anchor에게 얼마나 많은 계정 데이터를 할당할지 알려줍니다. Anchor는 먼저 8바이트 식별자를 저장한 다음, Counter 필드에 필요한 바이트를 저장합니다. 식별자를 통해 Anchor는 계정 데이터를 역직렬화하기 전에 해당 계정을 Counter 계정으로 인식할 수 있습니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

카운터 주소

seedsbump 제약 조건은 카운터 계정의 예상 PDA 주소를 정의합니다. Anchor는 제공된 counter 계정이 해당 주소와 일치하는지 검증합니다. 이 템플릿은 PDA를 사용하므로 사용자가 프로그램 ID와 seed로부터 카운터 주소를 도출할 수 있어, 카운터 주소가 결정론적으로 지정됩니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

새 계정을 생성하는 데 System Program을 사용하기 때문에 system_program 계정이 필요합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

핸들러 함수

handle_initialize 함수는 Anchor가 *rsInitialize*의 계정을 검증한 후 실행됩니다. ctx 값은 핸들러가 검증된 계정에 접근할 수 있도록 합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

초기 데이터

핸들러는 새 카운터 계정에 초기값을 기록합니다. 카운트는 *rs0*에서 시작하며, 지불자는 이후 카운터를 증가시킬 수 있는 권한을 갖게 됩니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

전송 계정

이 전송 CPI는 CPI가 다른 프로그램에 계정을 전달하는 방법을 보여주기 위해 포함되었습니다. Transfer 구조체는 System Program 전송에 사용되는 계정을 나열합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

CPI Context

*rsCpiContext::new*는 호출할 프로그램과 해당 프로그램에 전달되는 계정을 결합합니다. 이것이 CPI의 기본 구조입니다: 호출할 프로그램을 선택하고, 해당 프로그램이 필요로 하는 계정을 수집한 다음, 두 가지 모두를 호출에 전달합니다. 여기서 호출되는 프로그램은 System Program입니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Transfer 호출

*rsanchor_lang::system_program::transfer*는 System Program transfer 명령어를 호출합니다. 이 템플릿에서 transfer는 프로그램에서 다른 프로그램을 호출하는 간단한 예시입니다. transfer CPI가 실패하면 initialize 명령어도 함께 실패합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

메시지 로깅

*rsmsg!*는 프로그램 로그에 메시지를 기록합니다. *rsOk(())*는 명령어가 성공적으로 반환되었음을 나타냅니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

Initialize 구조체는 사용자가 initialize 명령어를 호출할 때 포함되어야 하는 계정을 정의합니다. Anchor는 핸들러가 실행되기 전에 이 계정들을 검증합니다.

Payer Account

payer 계정은 카운터 계정 생성 비용을 지불합니다. Signer<'info> 타입은 지불자가 트랜잭션에 서명해야 함을 의미하며, *rs#[account(mut)]*는 lamport가 차감되므로 지불자 계정이 변경될 수 있음을 의미합니다.

Counter Account

counter 계정은 state.rsCounter 데이터를 저장합니다. *rsinit*는 핸들러가 실행되기 전에 이 계정을 생성하도록 Anchor에 지시하고, *rspayer = payer*는 생성 비용을 지불할 계정을 Anchor에 알려줍니다.

계정 크기

space 제약 조건은 Anchor에게 얼마나 많은 계정 데이터를 할당할지 알려줍니다. Anchor는 먼저 8바이트 식별자를 저장한 다음, Counter 필드에 필요한 바이트를 저장합니다. 식별자를 통해 Anchor는 계정 데이터를 역직렬화하기 전에 해당 계정을 Counter 계정으로 인식할 수 있습니다.

카운터 주소

seedsbump 제약 조건은 카운터 계정의 예상 PDA 주소를 정의합니다. Anchor는 제공된 counter 계정이 해당 주소와 일치하는지 검증합니다. 이 템플릿은 PDA를 사용하므로 사용자가 프로그램 ID와 seed로부터 카운터 주소를 도출할 수 있어, 카운터 주소가 결정론적으로 지정됩니다.

System Program

새 계정을 생성하는 데 System Program을 사용하기 때문에 system_program 계정이 필요합니다.

핸들러 함수

handle_initialize 함수는 Anchor가 *rsInitialize*의 계정을 검증한 후 실행됩니다. ctx 값은 핸들러가 검증된 계정에 접근할 수 있도록 합니다.

초기 데이터

핸들러는 새 카운터 계정에 초기값을 기록합니다. 카운트는 *rs0*에서 시작하며, 지불자는 이후 카운터를 증가시킬 수 있는 권한을 갖게 됩니다.

전송 계정

이 전송 CPI는 CPI가 다른 프로그램에 계정을 전달하는 방법을 보여주기 위해 포함되었습니다. Transfer 구조체는 System Program 전송에 사용되는 계정을 나열합니다.

CPI Context

*rsCpiContext::new*는 호출할 프로그램과 해당 프로그램에 전달되는 계정을 결합합니다. 이것이 CPI의 기본 구조입니다: 호출할 프로그램을 선택하고, 해당 프로그램이 필요로 하는 계정을 수집한 다음, 두 가지 모두를 호출에 전달합니다. 여기서 호출되는 프로그램은 System Program입니다.

Transfer 호출

*rsanchor_lang::system_program::transfer*는 System Program transfer 명령어를 호출합니다. 이 템플릿에서 transfer는 프로그램에서 다른 프로그램을 호출하는 간단한 예시입니다. transfer CPI가 실패하면 initialize 명령어도 함께 실패합니다.

메시지 로깅

*rsmsg!*는 프로그램 로그에 메시지를 기록합니다. *rsOk(())*는 명령어가 성공적으로 반환되었음을 나타냅니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs는 기존 카운터 계정을 업데이트하는 데 필요한 계정들을 정의합니다. 핸들러는 서명자가 저장된 권한자인지 확인하고, 카운트가 지정된 *rsMAX_COUNT*에 도달하지 않았는지 검사한 후 카운트를 증가시킵니다.

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

계정 컨텍스트

Increment 구조체는 사용자가 increment 명령을 호출할 때 포함되어야 하는 계정들을 정의합니다. Anchor는 핸들러가 실행되기 전에 이 계정들을 검증합니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

카운터 계정

counter 계정은 Counter 데이터를 저장합니다. mut 제약 조건은 핸들러가 저장된 카운트를 업데이트할 수 있도록 하며, *rsseeds*와 bump 제약 조건은 카운터 PDA 주소를 검증합니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

권한자 서명자

authority 계정은 트랜잭션에 서명해야 합니다. 핸들러는 이후 이 서명자가 카운터 계정에 저장된 권한자와 일치하는지 확인합니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

핸들러 함수

handle_increment 함수는 Anchor가 *rsIncrement*의 계정을 검증한 후 실행됩니다. ctx 값은 핸들러가 검증된 계정에 접근할 수 있게 해줍니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

권한 확인

첫 번째 확인은 서명자가 이 카운터를 업데이트할 권한이 있는지 확인합니다. 서명자의 주소가 *rscounter.authority*와 일치하지 않으면, 명령은 *rsErrorCode::Unauthorized*와 함께 중단됩니다. 이는 애플리케이션 수준의 권한 부여를 보여줍니다: 프로그램이 카운터 데이터를 소유하지만, 어떤 서명자가 해당 데이터를 변경할 수 있는지에 대한 규칙을 구현합니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

최대 카운트 확인

두 번째 확인은 카운터가 *rsMAX_COUNT*를 초과하지 않도록 합니다. 카운터가 이미 한도에 도달한 경우, 명령은 *rsErrorCode::CounterOverflow*와 함께 중단됩니다. 이 한도는 템플릿에서 설정된 인위적인 규칙으로, 계정 데이터가 변경되기 전에 사용자 정의 오류가 명령을 중단시키는 방법을 확인할 수 있습니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

카운트 업데이트

두 확인을 모두 통과한 후에만 핸들러가 계정 데이터를 업데이트합니다. 이 줄은 저장된 카운터 값에 1을 더합니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

로그 메시지

*rsmsg!*는 업데이트된 카운트를 프로그램 로그에 기록합니다. *rsOk(())*는 명령이 성공적으로 반환되었음을 나타냅니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

계정 컨텍스트

Increment 구조체는 사용자가 increment 명령을 호출할 때 포함되어야 하는 계정들을 정의합니다. Anchor는 핸들러가 실행되기 전에 이 계정들을 검증합니다.

카운터 계정

counter 계정은 Counter 데이터를 저장합니다. mut 제약 조건은 핸들러가 저장된 카운트를 업데이트할 수 있도록 하며, *rsseeds*와 bump 제약 조건은 카운터 PDA 주소를 검증합니다.

권한자 서명자

authority 계정은 트랜잭션에 서명해야 합니다. 핸들러는 이후 이 서명자가 카운터 계정에 저장된 권한자와 일치하는지 확인합니다.

핸들러 함수

handle_increment 함수는 Anchor가 *rsIncrement*의 계정을 검증한 후 실행됩니다. ctx 값은 핸들러가 검증된 계정에 접근할 수 있게 해줍니다.

권한 확인

첫 번째 확인은 서명자가 이 카운터를 업데이트할 권한이 있는지 확인합니다. 서명자의 주소가 *rscounter.authority*와 일치하지 않으면, 명령은 *rsErrorCode::Unauthorized*와 함께 중단됩니다. 이는 애플리케이션 수준의 권한 부여를 보여줍니다: 프로그램이 카운터 데이터를 소유하지만, 어떤 서명자가 해당 데이터를 변경할 수 있는지에 대한 규칙을 구현합니다.

최대 카운트 확인

두 번째 확인은 카운터가 *rsMAX_COUNT*를 초과하지 않도록 합니다. 카운터가 이미 한도에 도달한 경우, 명령은 *rsErrorCode::CounterOverflow*와 함께 중단됩니다. 이 한도는 템플릿에서 설정된 인위적인 규칙으로, 계정 데이터가 변경되기 전에 사용자 정의 오류가 명령을 중단시키는 방법을 확인할 수 있습니다.

카운트 업데이트

두 확인을 모두 통과한 후에만 핸들러가 계정 데이터를 업데이트합니다. 이 줄은 저장된 카운터 값에 1을 더합니다.

로그 메시지

*rsmsg!*는 업데이트된 카운트를 프로그램 로그에 기록합니다. *rsOk(())*는 명령이 성공적으로 반환되었음을 나타냅니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

테스트 파일

programs/my-program/tests/test_initialize.rs는 Rust 통합 테스트입니다. 로컬 validator를 시작하지 않습니다. 대신, 컴파일된 .so 파일을 LiteSVM에 로드하고, 프로그램을 호출하는 트랜잭션을 빌드한 후, 각 트랜잭션 이후 카운터 계정을 읽습니다. 이 테스트는 호출할 프로그램의 프로그램 ID를 지정하고, instruction data를 제공하며, 필요한 계정을 전달하는 방식으로 Solana 트랜잭션에 대한 명령어를 빌드합니다.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

Initialize 계정 컨텍스트는 initialize 명령어에 필요한 계정을 정의합니다. 테스트는 동일한 계정들을 생성된 my_program::accounts::Initialize 헬퍼에 전달합니다:

  • payer는 *rspayer: payer.pubkey()*으로 전달됩니다.
  • counter는 *rscounter*으로 전달됩니다.
  • system_program는 *rssystem_program::ID*으로 전달됩니다.

my_program::instruction::Initialize {}.data()는 instruction data를 생성합니다. 여기서 명령어 인수가 인코딩되지만, 이 initialize 명령어는 인수가 필요하지 않습니다.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

Increment 계정 컨텍스트는 increment 명령어에 필요한 계정을 정의합니다. 테스트는 동일한 계정들을 생성된 my_program::accounts::Increment 헬퍼에 전달합니다:

  • counter는 *rscounter*으로 전달됩니다.
  • authority는 *rsauthority: payer.pubkey()*으로 전달됩니다.

my_program::instruction::Increment {}.data()는 instruction data를 생성합니다. 여기서 명령어 인수가 인코딩되지만, 이 increment 명령어는 인수가 필요하지 않습니다.

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

프로젝트 구성

루트 프로젝트 파일은 Anchor와 Cargo에게 프로그램을 빌드, 테스트 및 배포하는 방법을 알려줍니다. 전체 참조 문서는 Anchor.toml 구성Anchor CLI에 대한 Anchor 문서를 참조하세요.

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

목차

페이지 편집