
Steel로 Solana 프로그램을 작성하는 방법
목차
Steel은 최소한의 보일러플레이트와 최대한의 제어 기능으로 네이티브 Solana 프로그램을 작성할 수 있는 모듈식 경량 프레임워크입니다. Ore를 만든 Hardhat Chad가 개발했으며, 개발자 경험을 희생하지 않고 네이티브 Rust의 성능을 원하는 개발자를 위해 설계되었습니다.
이 글에서는 다음 내용을 알아봅니다.
- Steel의 개념과 Anchor 및 Pinocchio와의 관계
- 명령어를 정의하고 Steel 프로젝트를 구성하는 방법
- Steel을 사용해 커스텀 SPL 토큰을 생성하는 방법
solana-program-test로 프로그램을 테스트하는 방법
사전 요구 사항
이 가이드는 다음 내용에 익숙하다고 가정합니다.
- 기본 Rust 문법 및 툴체인
- Solana 개발 기본 사항(계정, 명령어, 프로그램)
- CLI 사용법(예: cargo, solana, curl)
기본적인 Solana 또는 Rust 프로그램을 작성할 수 있다면 Steel로 빌드할 준비가 된 것입니다.
Steel이란?
Steel은 Solana에서 프로그램을 빌드하기 위한 새로운 모듈식 프레임워크입니다. 개발자가 더 적은 보일러플레이트로 프로그램을 작성할 수 있으며 Anchor보다 규칙이 덜 엄격합니다.
Steel은 매크로와 Cross-Program Invocation(CPI) 헬퍼를 제공하여 프레임워크 없이 네이티브 방식에 가깝게 Solana 프로그램 개발 속도를 높입니다. 네이티브에 가까운 성능과 더 나은 개발자 경험을 함께 얻을 수 있습니다.
Steel이 제공하는 몇 가지 매크로와 헬퍼를 살펴보겠습니다.
Steel 매크로
Steel이 제공하는 매크로는 다음과 같습니다.
account!
account! 매크로는 Steel에서 Account 타입을 정의하고 AccountValidation 트레이트에 접근할 수 있게 합니다. 이 트레이트는 개발 중 계정 상태를 검증하는 헬퍼를 제공합니다.
instruction!
instruction! 매크로는 Steel에서 Instruction 타입을 정의하고 to_bytes 함수에 접근할 수 있게 합니다. 이 함수는 api/src/sdk에서 사용됩니다.
Steel의 다른 매크로로는 error와 event가 있습니다. 이름에서 알 수 있듯 각각 오류와 이벤트에 사용됩니다.
Steel CPI 헬퍼
Steel은 개발자가 프로그램을 개발할 때 필요한 대부분의 Cross-Program Invocation(CPI) 헬퍼 함수를 제공합니다. 여기에는 create_account, transfer 등을 포함한 system_program의 명령어가 있습니다.
또한 mint_to, burn, create_associated_token_account 등을 포함한 spl_token_program / spl_associated_token_program의 명령어도 제공합니다.
Steel에서 spl_token_program와 spl_associated_token_program의 CPI 헬퍼에 접근하려면 spl 기능 플래그를 활성화해야 합니다.
CU 최적화
Steel이 수행하는 작업 때문에 CU 효율이 높다고 생각할 수 있습니다. 하지만 실제로는 Steel이 하지 않는 작업 덕분에 효율적입니다. Steel 프레임워크는 가볍고 Solana 프로그램에 오버헤드를 거의 또는 전혀 추가하지 않습니다. 따라서 네이티브 Rust로 작성한 Solana 프로그램만큼 최적화되어 있으며, 기본 데이터 직렬화 도구로 bytemuck를 사용해 효율을 더욱 높입니다.
Steel과 Anchor 비교
Anchor는 안전한 Solana 프로그램을 빠르게 빌드하도록 설계된 강력한 프레임워크입니다. 계정 (역)직렬화와 명령어 데이터 등의 보일러플레이트를 줄이고, 필수 보안 검사를 수행하며, 클라이언트 라이브러리를 자동 생성하고, 광범위한 테스트 환경을 제공하여 개발 프로세스를 간소화합니다.
Steel과 Anchor의 가장 큰 차이점은 무엇인가요?
Anchor는 모든 숙련도의 Solana 개발자가 Solana 프로그램을 빠르게 작성할 수 있는 초보자 친화적인 스마트 컨트랙트 프레임워크입니다. 직관적이고 편리한 개발자 경험에 중점을 두기 때문에 많은 Solana 개발자가 사용합니다.
하지만 이러한 단순성에는 대가가 따릅니다.
Anchor에는 Solana 프로그램 바이너리를 더 비대하게 만드는 오버헤드가 누적되어 온체인 성능에 부정적인 영향을 줍니다. 예를 들어 Solana 프로그램 배포와 명령어 호출 비용이 증가합니다.
Solana는 빠르고 효율적이므로 Anchor가 Solana 프로그램에 오버헤드를 추가하더라도 대부분은 이를 체감하지 못합니다. 하지만 Ore와 Code-vm처럼 더 복잡한 프로그램을 개발하는 경우에는 오버헤드로 인해 온체인에서 사용할 수 없게 될 수 있습니다.
일반적으로 이런 Solana 프로그램은 네이티브 Rust로 빌드합니다. 하지만 유지관리자는 그 과정이 얼마나 어려운지 알고 있으며, Anchor만큼 사용하기 편하면서 네이티브 Rust처럼 성능이 뛰어난 프레임워크가 필요합니다.
Steel과 Anchor의 장점 및 트레이드오프
Anchor는 Solana 프로그램에 오버헤드를 추가하지만 Solana 생태계에서 가장 뛰어난 개발자 경험을 제공하며, 신규 Solana 개발자에게 여전히 권장되는 프레임워크입니다.
Anchor의 문법은 이해하기 쉽고 Interface Definition Language(IDL)를 제공합니다. 덕분에 JavaScript 같은 다른 언어로 Solana 프로그램을 쉽게 테스트하고 Solana 프로그램과 통신하는 클라이언트 측 애플리케이션도 개발할 수 있습니다.
Anchor IDL은 매우 강력하여 Codama 같은 도구에서 Solana 프로그램용 클라이언트, CLI, 문서를 자동으로 생성하는 데 사용할 수 있습니다.
Steel 프레임워크는 현재 IDL 기능을 제공하지 않습니다. 문법은 개발자 친화적이지만 개발자가 Rust에 상당히 익숙해야 합니다.
Anchor는 신규 개발자에게 권장되지만, 매크로와 자체 문법으로 Solana 프로그램 개발의 내부 동작을 감추기 때문에 숙련된 개발자에게는 제약이 될 수 있습니다.
반면 Steel은 Solana 프로그램의 가장 원초적인 수준까지 개발자가 모두 접근할 수 있게 합니다. 테스트가 기본적으로 Rust로 작성되므로 이러한 세밀한 제어는 테스트 과정에서 특히 유용하며, 직접적인 디버깅 경험을 제공합니다.
Steel이 훌륭한 스마트 컨트랙트 프레임워크인 이유는 네이티브 Rust를 최소한으로 감싼 래퍼라는 점과 오버헤드를 유발하는 추가 문법이 없다는 점에 있습니다.
간단히 말해 Steel은 네이티브 Rust의 성능과 효율을 유지하면서 개발자 친화성을 높인 버전입니다.
Steel과 Pinocchio 비교
Pinocchio는 Rust로 Solana 프로그램을 만들기 위한 의존성 없는 라이브러리입니다. Febo가 사이드 프로젝트로 작성했으며 이후 정식 Anza 프로젝트가 되었습니다. SVM 로더가 프로그램 입력 매개변수를 바이트 배열로 직렬화한 뒤 프로그램의 진입점에 전달하는 방식을 활용하여 입력을 읽는 zero-copy 타입을 정의합니다.
간단히 말해 Pinocchio는 외부 크레이트에 의존하지 않고 동적 타입 사용을 피하는 더 가벼운 solana_program 버전입니다.
Pinocchio가 출시된 이후 그 정체에 대한 오해가 많았습니다. Pinocchio 라이브러리는 solana_program 라이브러리를 대체하기 위한 것이며 Anchor나 Steel의 경쟁 제품이 아닙니다. 이 프레임워크들을 더 가볍게 만들어 보완합니다.
대부분이 Pinocchio 프로그램이라고 부르는 것은 solana_program 대신 pinocchio에 의존하는 네이티브 Rust 코드일 뿐입니다.
Steel로 토큰을 생성하는 방법
Steel의 작동 방식을 보여주기 위해 SPL 토큰을 생성하는 간단한 Solana 프로그램을 작성하겠습니다. 영상으로 배우는 것을 선호한다면 다음 동영상을 시청하세요.
사전 요구 사항
- Rust/Cargo
- Solana
- Steel
Rust 설치
Rust는 공식 Rust 웹사이트 또는 CLI를 통해 설치할 수 있습니다.
curl --proto '=https' --tlsv1.2 -sSf <https://sh.rustup.rs> | shSolana Tool Suite 설치
Steel에는 Solana Tool Suite도 필요합니다. 이 글을 작성하는 시점의 최신 릴리스인 2.2.15는 macOS와 Linux에서 다음 명령어로 설치할 수 있습니다.
sh -c "$(curl -sSfL <https://release.anza.xyz/v2.2.14/install>)"Windows 사용자는 다음 명령어로 Solana Tool Suite를 설치할 수 있습니다.
cmd /c "curl <https://release.anza.xyz/v2.2.14/agave-install-init-x86_64-pc-windows-msvc.exe> --output C:\\agave-install-tmp\\agave-install-init.exe --create-dirs"하지만 대신 Windows Subsystem for Linux(WSL)를 사용하는 것을 강력히 권장합니다. 듀얼 부팅이나 별도의 가상 머신 설정 없이 Windows 컴퓨터에서 Linux 환경을 실행할 수 있습니다. 이 방법을 선택했다면 Linux 설치 안내의 curl 명령어를 따르세요.
개발자는 v2.2.15을 원하는 버전의 릴리스 태그로 바꾸거나 stable, beta, edge 채널 이름을 사용할 수 있습니다.
설치한 후 solana –-version을 실행하여 원하는 solana 버전이 설치되었는지 확인하세요.
Steel 설치
다음을 실행하여 Cargo로 Steel을 설치할 수 있습니다.
cargo install steel-cliSteel 프로젝트 생성
다음을 실행하기만 하면 Steel 프로젝트를 만들 수 있습니다.
// creates a new Steel project named `create-token`
steel new token
// enter directory
cd create-tokentoken 디렉터리는 다음과 같아야 합니다.
Cargo.toml (workspace)
⌙ api
⌙ Cargo.toml
⌙ src
⌙ consts.rs
⌙ error.rs
⌙ instruction.rs
⌙ lib.rs
⌙ sdk.rs
⌙ state
⌙ mod.rs
⌙ account_1.rs
⌙ account_2.rs
⌙ program
⌙ Cargo.toml
⌙ src
⌙ lib.rs
⌙ instruction_1.rs
⌙ instruction_2.rs
Steel 프로젝트의 기본 레이아웃에는 api와 program라는 두 폴더가 있습니다.
api에는 Solana 프로그램을 구현할 때 사용할 state, errors 등의 타입이 있으며, program 폴더에는 프로그램 로직이 있습니다.
Steel로 프로그램을 개발할 때는 program 폴더가 api 폴더에 의존하므로 api 폴더부터 시작하는 것이 좋습니다.
state, const, error 모듈 제거
api 폴더에는 create-token 프로젝트에서 사용하지 않을 state, const, error 등의 모듈이 있으므로 제거하겠습니다.
다음 명령어를 실행하여 Steel 모듈을 제거할 수 있습니다.
# you should be at the root of the `create-token` project
# enter the api/src directory
cd api/src
# delete the modules we don't need
rm -rf state [consts.rs](<http://consts.rs/>) [error.rs](<http://error.rs/>)모듈을 삭제한 후에는 api/src/lib.rs 파일이 해당 모듈을 호출하므로 이를 업데이트해야 합니다.
api/src/lib.rs을 다음과 같이 업데이트하세요.
pub mod instruction;
pub mod sdk;
pub mod prelude {
pub use crate::instruction::*;
pub use crate::sdk::*;
}
use steel::*;
// TODO Set program id
declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35");Steel에서 명령어 정의
Steel에서 명령어는 api/src/instructions.rs에 정의합니다. Steel 프로그램의 모든 명령어는 enum에 정의되며, 각 명령어는 struct입니다.
모든 명령어를 포함하는 enum은 다음과 같습니다.
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
Initialize = 0,
Add = 1
}
While each instruction typically looks like this:
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Initialize {}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Add {
pub amount: [u8; 8]
}Initialize처럼 인수가 필요 없는 명령어에는 필드가 없습니다.
데이터가 필요한 명령어는 바이트 표현을 사용합니다. 예를 들어 Add::amount is [u8; 8]은 u64에 매핑됩니다.
명령어 enum과 명령어 struct를 정의한 후 instruction! 매크로에 전달해야 합니다. 첫 번째 인수는 명령어 enum이고 두 번째 인수는 명령어 struct입니다.
instruction!(CreateTokenInstruction, Initialize);
instruction!(CreateTokenInstruction, Add);create-token 프로그램에는 네 개의 인수를 받는 명령어 하나가 있으므로 api/src/instructions은 다음과 같아야 합니다.
use steel::*;
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum CreateTokenInstruction {
Create = 0,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Create {
pub name: [u8; 32],
pub symbol: [u8; 8],
pub uri: [u8; 128],
pub decimals: u8,
}
instruction!(CreateTokenInstruction, Create);Create에서 name, symbol, uri 필드는 고정 크기 바이트 배열로 표현되는 문자열입니다.
name: [u8; 16] — 최대 16바이트의 이름symbol: [u8; 8] — 심볼은 일반적으로 짧음uri: [u8; 128] — URI는 일반적으로 더 김
이 크기는 문자가 아니라 예상되는 최대 바이트 길이에 따라 달라집니다. 예를 들어 멀티바이트 UTF-8 문자에는 더 많은 공간이 필요할 수 있습니다.
토큰의 소수 자릿수는 1바이트에 들어가므로 decimals은 단순한 u8입니다.
SDK 업데이트
api/src에는 sdk.rs이라는 파일이 있습니다. 프로그램 로직을 구현할 때는 사용하지 않지만 테스트나 Rust 클라이언트 코드를 실행할 때 사용합니다. 이 파일에는 Steel 프로그램의 모든 명령어를 개별적으로 빌드하는 함수가 있습니다. 이 프로그램에는 명령어가 하나뿐이므로 SDK 함수도 하나만 필요합니다. 따라서 api/src/sdk.rs은 다음과 같아야 합니다.
use steel::*;
use crate::prelude::*;
pub fn create(
user: Pubkey,
mint: Pubkey,
name: [u8; 32],
symbol: [u8; 8],
uri: [u8; 128],
decimals: u8,
) -> Instruction {
let metadata = Pubkey::find_program_address(
&[
"metadata".as_bytes(),
mpl_token_metadata::ID.as_ref(),
mint.as_ref(),
],
&mpl_token_metadata::ID,
)
.0;
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(mint, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(mpl_token_metadata::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::rent::ID, false),
],
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),
}
}create이라는 함수 하나가 있으며 다섯 개의 인수를 받습니다. user은 이 명령어를 호출할 계정의 공개 키이고, mint은 token mint을 나타낼 계정의 공개 키입니다. name, symbol, uri, decimals은 api/src/instructions::Create에 정의한 프로그램 로직을 구현할 때 사용할 데이터입니다.
토큰 메타데이터를 저장해야 하며 이를 위해 Metaplex Metadata 프로그램을 사용하겠습니다. 먼저 다음을 추가합니다.
let metadata = Pubkey::find_program_address(
&[
"metadata".as_bytes(),
mpl_token_metadata::ID.as_ref(),
mint.as_ref(),
],
&mpl_token_metadata::ID,
)
.0;이 코드 블록에서는 토큰 메타데이터를 저장할 Program Derived Address(PDA)를 가져옵니다. 필요한 주소를 파생하려면 다음 시드가 필요합니다.
- “metadata” 문자열의 바이트 표현(즉,
"metadata".as_bytes()) - 슬라이스로 표현한 메타데이터 프로그램의 프로그램 ID(즉,
mpl_token_metadata::ID.as_ref()) - 슬라이스로 표현한 민트 공개 키(즉,
mint.as_ref())
이 입력들이 함께 시드를 구성합니다. Pubkey::find_program::address의 두 번째 인수로는 Metadata 프로그램의 프로그램 ID만 필요합니다.
마지막 코드 블록에서는 이 명령어를 나타내는 Instruction 타입을 반환합니다.
Instruction 타입은 다음과 같습니다.
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(mint, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(mpl_token_metadata::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::rent::ID, false),
],
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),
} Instruction 타입은 다음 세 필드가 있는 struct입니다.
program_idaccountsdata
이 블록에서는 프로그램의 명령어에 맞는 Instruction 인스턴스를 선언합니다.
api/src/lib.rs에서 program_id을 가져오려면 다음을 사용하세요.
program_id: crate::ID accounts 필드는 계정 메타데이터(즉, Vec<AccountMeta>)의 벡터이므로 이 명령어에서 사용할 모든 계정을 선언해야 합니다.
accounts: vec![
AccountMeta::new(user, true),
AccountMeta::new(mint, true),
AccountMeta::new(metadata, false),
AccountMeta::new_readonly(spl_token::ID, false),
AccountMeta::new_readonly(mpl_token_metadata::ID, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::rent::ID, false),
],마지막으로 data 필드는 이 명령어에 사용할 인수를 바이트로 나타냅니다.
data: Create {
name,
symbol,
uri,
decimals,
}
.to_bytes(),이제 api 폴더 작업을 마쳤습니다.
다음으로 필요한 의존성을 추가한 뒤 program 폴더로 이동하겠습니다.
Steel 의존성 추가
지금 steel build을 실행하여 프로그램을 컴파일하면 다음 오류가 발생해야 합니다.
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> api/src/sdk.rs:16:13
|
16 | mpl_token_metadata::ID.as_ref(),
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> api/src/sdk.rs:19:10
|
19 | &mpl_token_metadata::ID,
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
--> api/src/sdk.rs:29:39
|
29 | AccountMeta::new_readonly(spl_token::ID, false),
| ^^^^^^^^^ use of undeclared crate or module `spl_token`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> api/src/sdk.rs:30:39
|
30 | AccountMeta::new_readonly(mpl_token_metadata::ID, false),
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
이는 프로그램에 필요한 spl_token와 mpl_token_metadata 크레이트가 누락되었음을 의미합니다.
누락된 크레이트를 추가하려면 /Cargo.toml 파일에 다음을 추가하세요.
// /Cargo.toml
[workspace.dependencies]
...
...
mpl-token-metadata = "5.1.0"
spl-token = { version = "8.0.0", features = ["no-entrypoint"] }
In /api/Cargo.toml add:
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true/api/Cargo.toml에는 다음을 추가하세요.
// /api/Cargo.toml
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true이제 steel build을 실행하면 의존성 오류가 사라져야 합니다.
하지만 program 폴더가 의존하는 api 폴더의 코드를 삭제했으므로 다음과 같은 오류가 계속 표시됩니다.
error[E0599]: no variant or associated item named `Initialize` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
--> program/src/lib.rs:18:33
|
18 | CreateTokenInstruction::Initialize => process_initialize(accounts, data)?,
| ^^^^^^^^^^ variant or associated item not found in `CreateTokenInstruction`
error[E0599]: no variant or associated item named `Add` found for enum `create_token_api::instruction::CreateTokenInstruction` in the current scope
--> program/src/lib.rs:19:33
|
19 | CreateTokenInstruction::Add => process_add(accounts, data)?,
| ^^^ variant or associated item not found in `CreateTokenInstruction`
걱정하지 마세요. 다음 섹션에서 이 오류를 해결하겠습니다.
Steel로 프로그램 로직 구현
Steel 프로젝트에는 기본적으로 api와 program라는 두 폴더가 있습니다. 방금 api 폴더에 프로그램에 필요한 타입을 정의했습니다. 이제 program 폴더에서 프로그램 로직을 구현해야 합니다.
먼저 /program/lib.rs을 다음과 같이 업데이트하세요.
mod create;
use create::*;
use create_token_api::prelude::*;
use steel::*;
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
data: &[u8],
) -> ProgramResult {
let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;
match ix {
CreateTokenInstruction::Create => process_create(accounts, data)?,
}
Ok(())
}
entrypoint!(process_instruction);이 파일에서는 메인 process_instruction 함수를 정의하고 entrypoint! 매크로에 전달합니다. 이 매크로는 Solana 런타임이 프로그램 로직을 호출하는 데 필요한 보일러플레이트를 생성합니다.
process_instruction 함수 안에는 살펴봐야 할 중요한 코드 블록이 두 개 있습니다.
let (ix, data) = parse_instruction(&create_token_api::ID, program_id, data)?;parse_instruction은 명령어 데이터에서 명령어를 파싱합니다. 즉, 프로그램에 전달된 데이터로 호출할 명령어를 결정할 수 있습니다.
Ok()인 경우 instruction(ix)과 instruction data(data)의 튜플을 반환합니다.
match ix {
CreateTokenInstruction::Create => process_create(accounts, data)?,
}parse_instruction에서 instruction(ix)을 가져오면 match을 사용해 호출할 명령어를 선택합니다. 프로그램에 명령어가 하나뿐이므로 match arm도 하나뿐입니다.
이제 프로그램이 호출될 때 올바른 명령어를 실행하도록 로직을 설정했습니다. 하지만 process_create과 create mod는 아직 없으므로 생성하겠습니다.
터미널에서 다음을 실행하세요.
// you should be at the root of your project
// enter the program/src directory
cd program/src
// delete add.rs and initialize.rs
rm -rf add.rs initialize.rs
// create create.rs
touch create.rs 이제 program/src/create.rs을 다음과 같이 업데이트하세요.
use create_token_api::prelude::*;
use solana_program::{msg, program_pack::Pack};
use steel::*;
pub fn process_create(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
// Load accounts.
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// validate
user_info.is_signer()?;
mint_info.is_empty()?.is_signer()?;
metadata_info.is_empty()?.is_writable()?;
token_program.is_program(&spl_token::ID)?;
token_metadata_program.is_program(&mpl_token_metadata::ID)?;
system_program.is_program(&system_program::ID)?;
rent_sysvar.is_sysvar(&sysvar::rent::ID)?;
// create mint account
create_account(
user_info,
mint_info,
system_program,
spl_token::state::Mint::LEN,
&token_program.key,
)?;
msg!("create account");
let args = Create::try_from_bytes(data)?;
let name = bytes_to_string::<32>(&args.name)?;
let symbol = bytes_to_string::<8>(&args.symbol)?;
let uri = bytes_to_string::<128>(&args.uri)?;
let decimals = args.decimals;
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;
msg!("initialize mint");
// create metadata account
mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
__program: token_metadata_program,
metadata: metadata_info,
mint: mint_info,
mint_authority: user_info,
payer: user_info,
update_authority: (user_info, true),
system_program,
rent: Some(rent_sysvar),
__args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
data: mpl_token_metadata::types::DataV2 {
name,
symbol,
uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
is_mutable: true,
collection_details: None,
},
}
.invoke()?;
msg!("metadata account created");
Ok(())
}여기서 어떤 일이 일어나는지 살펴보겠습니다.
// Load accounts.
let [user_info, mint_info, metadata_info, token_program, token_metadata_program, system_program, rent_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};이 코드 블록에서는 명령어에 필요한 계정을 로드합니다. 전달된 계정이 정의된 계정과 일치하지 않으면 이 블록에서 ProgramError::NotEnoughAccountKeys 오류가 발생합니다.
자세히 살펴보면 계정 이름 지정 방식에서 다음 패턴을 확인할 수 있습니다.
- “일반” 계정은
info으로 끝남 - 프로그램 계정은
program으로 끝남 - Sysvar는
sysvar으로 끝남
이는 Steel에서 권장하는 계정 이름 지정 방식입니다. 프로그램에는 실질적인 영향이 없으므로 다른 방식을 선택해도 됩니다.
다음으로 이 코드 블록에서 계정을 검증합니다.
// validate
user_info.is_signer()?; // user is a signer
mint_info.is_empty()?.is_signer()?; // mint is empty and is a signer
metadata_info.is_empty()?.is_writable()?; // metadata is empty and is writable
token_program.is_program(&spl_token::ID)?; // token program == spl_token::ID
token_metadata_program.is_program(&mpl_token_metadata::ID)?; // token meatadata == mpl_token_metadata::ID
system_program.is_program(&system_program::ID)?; // system program == system_program::ID
rent_sysvar.is_sysvar(&sysvar::rent::ID)?; // rent sysvar == sysvar::rent::IDSteel은 연결해서 사용할 수 있는 간단한 계정 검증 헬퍼를 제공합니다.
다음으로 create_account 헬퍼를 사용해 mint 계정을 생성합니다.
// create mint account
create_account(
user_info,
mint_info,
system_program,
spl_token::state::Mint::LEN,
&token_program.key,
)?;mint 계정을 생성한 후 명령어 데이터를 바이트에서 Rust 타입으로 역직렬화합니다.
let args = Create::try_from_bytes(data)?;
let name = bytes_to_string::<32>(&args.name)?;
let symbol = bytes_to_string::<8>(&args.symbol)?;
let uri = bytes_to_string::<128>(&args.uri)?;
let decimals = args.decimals;첫 번째 줄은 &[u8] 타입의 명령어 데이터를 api/instructions.rs/Create으로 변환합니다. 다음 세 줄은 바이트인 Create의 필드를 bytes_to_string 헬퍼로 문자열로 변환합니다.
또한 bytes_to_string은 정확한 길이의 문자열을 생성하여 컴퓨트 유닛을 절약하는 const 제네릭 매개변수(즉, ::<32>)를 받습니다.
// initialize mint
initialize_mint(
mint_info,
user_info,
Some(user_info),
token_program,
rent_sysvar,
decimals,
)?;다음으로 initialize_mint 헬퍼 함수로 mint 계정을 초기화합니다.
// create metadata account
mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
__program: token_metadata_program,
metadata: metadata_info,
mint: mint_info,
mint_authority: user_info,
payer: user_info,
update_authority: (user_info, true),
system_program,
rent: Some(rent_sysvar),
__args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
data: mpl_token_metadata::types::DataV2 {
name,
symbol,
uri,
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
is_mutable: true,
collection_details: None,
},
}
.invoke()?;여기서는 토큰 민트를 위한 metadata 계정을 생성했습니다. 이 계정에는 컬렉션의 이름, 심볼, 생성자 등의 정보가 포함됩니다.
create.rs 파일 작업을 마쳤으므로 steel build을 실행하겠습니다.
다음 오류가 표시되어야 합니다.
error[E0433]: failed to resolve: use of undeclared crate or module `spl_token`
--> program/src/create.rs:28:9
|
28 | spl_token::state::Mint::LEN,
| ^^^^^^^^^ use of undeclared crate or module `spl_token`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> program/src/create.rs:69:5
|
69 | mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> program/src/create.rs:78:17
|
78 | __args: mpl_token_metadata::instructions::CreateMetadataAccountV3Instru...
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`
error[E0433]: failed to resolve: use of undeclared crate or module `mpl_token_metadata`
--> program/src/create.rs:79:19
|
79 | data: mpl_token_metadata::types::DataV2 {
| ^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `mpl_token_metadata`이 오류는 의존성 문제가 있음을 나타냅니다. /program/Cargo.toml 파일을 다음과 같이 편집하여 업데이트할 수 있습니다.
[dependencies]
...
...
mpl-token-metadata.workspace = true
spl-token.workspace = true이제 steel build을 다시 실행하면 마지막 오류 하나가 발생합니다.
error[E0425]: cannot find function `initialize_mint` in this scope
--> program/src/create.rs:57:5
|
57 | initialize_mint(
| ^^^^^^^^^^^^^^^ not found in this scopeinitialize_mint 헬퍼 함수에 접근하려면 Steel의 spl 기능이 필요하기 때문에 이 오류가 발생합니다. 따라서 /Cargo.toml 파일에서 import를 업데이트해야 합니다.
[workspace.dependencies]
...
...
steel = { version = "3.0", features = ["spl"] }이제 steel build을 실행하면 오류 없이 프로그램이 컴파일되어야 합니다.
여기까지 잘 따라오셨습니다!
마지막 단계로 프로그램을 테스트해야 합니다.
Steel 프로그램 테스트
Steel 테스트는 기본적으로 Rust로 작성합니다. Steel은 테스트에 solana-program-test을 사용하지만 원하는 경우 liteSVM이나 mollusk를 사용할 수 있습니다.
테스트는 /program/tests/test.rs에 작성합니다.
먼저 다음과 같이 업데이트하겠습니다.
use create_token_api::prelude::*;
use solana_program::hash::Hash;
use solana_program_test::{processor, BanksClient, ProgramTest};
use solana_sdk::{
program_pack::Pack, signature::Keypair, signer::Signer, transaction::Transaction,
};
use steel::*;
async fn setup() -> (BanksClient, Keypair, Hash) {
let mut program_test = ProgramTest::new(
"create_token_program",
create_token_api::ID,
processor!(create_token_program::process_instruction),
);
program_test.add_program("token_metadata", mpl_token_metadata::ID, None);
program_test.prefer_bpf(true);
program_test.start().await
}
#[tokio::test]
async fn run_test() {
// Setup test
let (mut banks, payer, blockhash) = setup().await;
let mint_keypair = Keypair::new();
let name = string_to_bytes::<32>("ANATOLY").unwrap();
let symbol = string_to_bytes::<8>("MERT").unwrap();
let uri = string_to_bytes::<128>("blah blah blah").unwrap();
let decimals = 9;
// Submit create transaction.
let ix = create(
payer.pubkey(),
mint_keypair.pubkey(),
name,
symbol,
uri,
decimals,
);
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&payer.pubkey()),
&[&payer, &mint_keypair],
blockhash,
);
let res = banks.process_transaction(tx).await;
assert!(res.is_ok());
let serialized_mint_data = banks
.get_account(mint_keypair.pubkey())
.await
.unwrap()
.unwrap()
.data;
let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();
assert!(mint_data.is_initialized);
assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());
assert_eq!(mint_data.decimals, decimals);
}
테스트 파일은 setup과 run_test이라는 두 함수로 구성됩니다.
setup 함수에서는 다음 세 가지 중요한 작업을 수행합니다.
- 기본적으로
create_token_program프로그램이 추가된ProgramTest인스턴스를 생성합니다 - 사용 중인 Metaplex 토큰 프로그램이 기본적으로
ProgramTest에 포함되지 않으므로token_metadata프로그램을ProgramTest인스턴스에 추가합니다 start메서드로ProgramTest인스턴스를 시작합니다. 이 메서드는 (BanksClient,Keypair,Hash) 튜플을 반환합니다
async fn setup() -> (BanksClient, Keypair, Hash) {
let mut program_test = ProgramTest::new(
"create_token_program",
create_token_api::ID,
processor!(create_token_program::process_instruction),
);
program_test.add_program("token_metadata", mpl_token_metadata::ID, None);
program_test.prefer_bpf(true);
program_test.start().await
}run_test의 첫 부분에서는 setup 함수를 호출하고 토큰 민트를 위한 Keypair을 생성합니다.
// Setup test
let (mut banks, payer, blockhash) = setup().await;
let mint_keypair = Keypair::new();다음으로 명령어 데이터를 준비합니다.
create 명령어는 바이트 표현을 기대하므로 string_to_bytes 헬퍼를 사용해 문자열을 바이트로 변환합니다.
let name = string_to_bytes::<32>("ANATOLY").unwrap();
let symbol = string_to_bytes::<8>("MERT").unwrap();
let uri = string_to_bytes::<128>("blah blah blah").unwrap();
let decimals = 9;앞서 api 폴더에서 프로그램 로직에는 사용하지 않은 함수를 구현했던 것을 기억하시나요? api/src/sdk.rs의 create 함수입니다.
아래 코드 블록에서 먼저 같은 함수를 호출해 Instruction 인스턴스를 생성합니다. 이를 Transaction::new_signed_with_payer을 사용해 Transaction 인스턴스에 전달한 다음, 트랜잭션을 banks.process_transaction(tx).await;에 전달해 처리합니다.
assert!(res.is_ok());은 트랜잭션이 처리되었음을 확인합니다.
// Submit create transaction.
let ix = create(
payer.pubkey(),
mint_keypair.pubkey(),
name,
symbol,
uri,
decimals,
);
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&payer.pubkey()),
&[&payer, &mint_keypair],
blockhash,
);
let res = banks.process_transaction(tx).await;
assert!(res.is_ok());지금까지 테스트 환경(ProgramTest)에서 명령어를 실행했습니다.
이제 올바르게 실행되었는지 테스트하겠습니다.
// get serialized data of mint account
let serialized_mint_data = banks
.get_account(mint_keypair.pubkey())
.await
.unwrap()
.unwrap()
.data;
// unpack the mint account data to get the SPL Mint information
let mint_data = spl_token::state::Mint::unpack(&serialized_mint_data).unwrap();
// check if the mint account was initilized
assert!(mint_data.is_initialized);
// check if the mint authority matches the one we set
assert_eq!(mint_data.mint_authority.unwrap(), payer.pubkey());
// check if the decimals match
assert_eq!(mint_data.decimals, decimals);metadata 계정에 저장된 데이터 등 다른 항목을 확인하는 assertion을 더 작성할 수 있지만, 여기서는 간단히 마치겠습니다.
직접 추가해 보세요. 도움이 필요하다면 이 Steel 테스트용 Solana 개발자 가이드를 참고하세요.
테스트 파일 작업을 마쳤으므로 테스트 명령어인 steel test을 실행하겠습니다.
안타깝게도 mpl_token_metadata 프로그램의 소스 코드/ELF 파일이 없으므로 실패합니다.
걱정하지 마세요. 다음을 실행하면 해결할 수 있습니다.
// you have to be at the root of your project
// create a folder called fixtures in program/tests
// ProgramTest is going to check this folder for the ELF file for token metadata
mkdir program/tests/fixtures
// dump the ELF file for the Metaplex metadata program in the fixtures folder
solana program dump metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s program/tests/fixtures/token_metadata.so이제 steel test을 실행하면 다음 결과가 표시되어야 합니다.
running 1 test
[2025-06-08T14:46:12.240628000Z INFO solana_program_test] "create_token_program" SBF program from /Users/perelyn/helius/create-token/target/deploy/create_token_program.so, modified 3 seconds, 112 ms, 833 µs and 660 ns ago
[2025-06-08T14:46:12.242336000Z INFO solana_program_test] "token_metadata" SBF program from tests/fixtures/token_metadata.so, modified 1 minute, 49 seconds, 247 ms, 367 µs and 400 ns ago
[2025-06-08T14:46:12.381492000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 invoke [1]
[2025-06-08T14:46:12.382734000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2025-06-08T14:46:12.383272000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.383298000Z DEBUG solana_runtime::message_processor::stable_log] Program log: create account
[2025-06-08T14:46:12.383562000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
[2025-06-08T14:46:12.383783000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Instruction: InitializeMint
[2025-06-08T14:46:12.386049000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2968 of 192320 compute units
[2025-06-08T14:46:12.386068000Z DEBUG solana_runtime::message_processor::stable_log] Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
[2025-06-08T14:46:12.386099000Z DEBUG solana_runtime::message_processor::stable_log] Program log: initialize mint
[2025-06-08T14:46:12.386409000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [2]
[2025-06-08T14:46:12.387342000Z DEBUG solana_runtime::message_processor::stable_log] Program log: IX: Create Metadata Accounts v3
[2025-06-08T14:46:12.387576000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.387588000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.387999000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Allocate space for the account
[2025-06-08T14:46:12.388226000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388264000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.388306000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Assign the account to the owning program
[2025-06-08T14:46:12.388851000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [3]
[2025-06-08T14:46:12.388873000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2025-06-08T14:46:12.392769000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 37330 of 185782 compute units
[2025-06-08T14:46:12.392790000Z DEBUG solana_runtime::message_processor::stable_log] Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s success
[2025-06-08T14:46:12.392842000Z DEBUG solana_runtime::message_processor::stable_log] Program log: metadata account created
[2025-06-08T14:46:12.395012000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 consumed 51973 of 200000 compute units
[2025-06-08T14:46:12.395031000Z DEBUG solana_runtime::message_processor::stable_log] Program z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35 success
test run_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s다시 한번 축하합니다!
같은 출력이 표시되면 프로그램이 테스트를 통과한 것입니다.
결론
Steel은 스마트하고 성능에 최적화된 Solana 프로그램을 빌드하기 위한 모듈식 경량 개발 프레임워크입니다. 이 글에서는 Steel의 작동 방식을 설명하고 Anchor 및 Pinocchio와 비교했으며, Steel로 새 토큰을 생성하는 과정을 살펴봤습니다.
추가 자료
Steel과 Solana 프로그램 개발을 계속 학습하려면 다음 자료를 살펴보세요.
- Steel GitHub 저장소
- Solana 개발 부트캠프(GitHub)
- Solana 개발 부트캠프(동영상)
- Blueshift — 자체 온체인 프로그램을 작성하는 방법을 배워보세요
관련 아티클
Helius 구독하기
최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요


