Solana를 위한 타입 안전 Rust ↔ TypeScript 통신
Source: Dev.to

온‑체인 Rust와 오프‑체인 TypeScript를 모두 사용하는 Solana dApp 구축?
타입을 완벽하게 동기화하는 방법은 다음과 같습니다.
문제
Rust에서 구조체를 정의합니다:
#[account]
pub struct PlayerAccount {
pub wallet: Pubkey,
pub level: u16,
pub experience: u64,
}
그런 다음 TypeScript에서 수동으로 재현합니다:
export interface PlayerAccount {
wallet: PublicKey;
level: number;
experience: number;
}
무엇이 잘못될 수 있을까요?
- 필드 순서가 맞지 않음 → 역직렬화 실패
- 타입 크기 차이 → 데이터 손상
- 업데이트를 잊음 → 런타임 오류
솔루션: LUMOS
한 번 정의하고, 두 개를 생성합니다:
// schema.lumos
#[solana]
#[account]
struct PlayerAccount {
wallet: PublicKey,
level: u16,
experience: u64,
inventory: [String],
last_active: i64,
}
바인딩을 생성합니다:
lumos generate schema.lumos
전체 워크플로우
flowchart LR
A[schema.lumos] --> B[lumos generate]
B --> C[generated.rs / generated.ts]
C --> D[Rust Program (Anchor/Borsh)]
C --> E[Solana RPC (On‑chain)]
C --> F[TypeScript Client]
D <--> E
E <--> F
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐
│ schema.lumos │────▶│ lumos generate │────▶│ generated.rs/.ts │
└─────────────────┘ └──────────────────┘ └─────────────────────┘
│
┌─────────────────────────────────┼─────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Rust Program │◀────────────▶│ Solana RPC │◀────────────▶│ TypeScript │
│ (Anchor/Borsh) │ │ (On‑chain) │ │ Client │
└─────────────────┘ └─────────────────┘ └─────────────────┘
러스트 프로그램 사용법
use anchor_lang::prelude::*;
mod generated;
use generated::PlayerAccount;
/// The program entry point.
#[program]
pub mod game_program {
use super::*;
/// Initializes a new player account.
pub fn initialize(ctx: Context<Initialize>, wallet: Pubkey) -> Result<()> {
let player = &mut ctx.accounts.player;
player.wallet = wallet;
player.level = 1;
player.experience = 0;
player.inventory = Vec::new();
player.last_active = Clock::get()?.unix_timestamp;
Ok(())
}
/// Adds experience points to a player.
pub fn add_experience(ctx: Context<AddExperience>, amount: u64) -> Result<()> {
let player = &mut ctx.accounts.player;
player.experience = player
.experience
.checked_add(amount)
.expect("Experience overflow");
player.last_active = Clock::get()?.unix_timestamp;
Ok(())
}
}
/// Context for the `initialize` instruction.
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = 8 + PlayerAccount::LEN)]
pub player: Account<'info, PlayerAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
/// Context for the `add_experience` instruction.
#[derive(Accounts)]
pub struct AddExperience<'info> {
#[account(mut, has_one = wallet)]
pub player: Account<'info, PlayerAccount>,
pub signer: Signer<'info>,
}
Source: …
TypeScript 클라이언트 사용법
아래는 TypeScript 클라이언트에서 Solana 프로그램과 상호작용할 때 흔히 사용하는 세 가지 패턴입니다:
1. 계정 데이터 가져오기
import { Connection, PublicKey } from '@solana/web3.js';
import { PlayerAccount, PlayerAccountBorshSchema } from './generated';
/**
* Retrieves a `PlayerAccount` from the blockchain.
*
* @param connection – An active Solana `Connection`.
* @param playerPubkey – The public key of the player account.
* @returns The deserialized `PlayerAccount`.
* @throws If the account does not exist.
*/
export async function getPlayerAccount(
connection: Connection,
playerPubkey: PublicKey
): Promise<PlayerAccount> {
const accountInfo = await connection.getAccountInfo(playerPubkey);
if (!accountInfo) {
throw new Error('Player account not found');
}
// Anchor‑generated accounts prepend an 8‑byte discriminator.
const data = accountInfo.data.slice(8);
return PlayerAccountBorshSchema.decode(data);
}
2. 업데이트 구독하기
import { Connection, PublicKey } from '@solana/web3.js';
import { PlayerAccount, PlayerAccountBorshSchema } from './generated';
/**
* Registers a listener that fires whenever the player account changes.
*
* @param connection – An active Solana `Connection`.
* @param playerPubkey – The public key of the player account.
* @param callback – Called with the updated `PlayerAccount`.
* @returns The subscription ID (useful for later removal via `connection.removeAccountChangeListener`).
*/
export function subscribeToPlayer(
connection: Connection,
playerPubkey: PublicKey,
callback: (player: PlayerAccount) => void
): number {
return connection.onAccountChange(
playerPubkey,
(accountInfo) => {
const data = accountInfo.data.slice(8);
const player = PlayerAccountBorshSchema.decode(data);
callback(player);
},
'confirmed'
);
}
/* Example usage */
const subscriptionId = subscribeToPlayer(connection, playerPubkey, (player) => {
console.log(`Level: ${player.level}, XP: ${player.experience}`);
});
3. 트랜잭션 만들기
import * as anchor from '@coral-xyz/anchor';
import { PublicKey } from '@solana/web3.js';
/**
* Calls the `addExperience` instruction on the program.
*
* @param program – The Anchor `Program` instance.
* @param playerPubkey – The player account to update.
* @param amount – Amount of experience to add.
* @returns The transaction signature.
*/
export async function addExperience(
program: anchor.Program,
playerPubkey: PublicKey,
amount: number
): Promise<string> {
const txSignature = await program.methods
.addExperience(new anchor.BN(amount))
.accounts({ player: playerPubkey })
.rpc();
console.log(`Added ${amount} XP. TX: ${txSignature}`);
return txSignature;
}
이 스니펫들은 이미 Borsh 스키마(PlayerAccountBorshSchema)와 프로그램에 대한 Anchor IDL을 생성해 두었다는 전제하에 작성되었습니다. 프로젝트 구조에 맞게 import 경로를 조정하세요.
타입 매핑 참조
| LUMOS | Rust | TypeScript |
|---|---|---|
u8‑u64 | u8‑u64 | number |
u128 | u128 | bigint |
i8‑i64 | i8‑i64 | number |
bool | bool | boolean |
String | String | string |
PublicKey | Pubkey | PublicKey |
타입 개요
icKey
Pubkey
PublicKey
[T]
Vec
T[]
Option
Option
T \
Benefits
- ✅ Single Source of Truth – 한 번 정의하면 모든 곳에서 사용.
- ✅ Guaranteed Sync – Rust와 TypeScript가 항상 일치.
- ✅ Correct Borsh – 필드 순서와 크기가 보장됨.
- ✅ Zero Runtime Overhead – 생성된 코드는 수작업 코드와 동일.
- ✅ IDE Support – 완전한 TypeScript 자동완성 지원.
시작하기
cargo install lumos-cli
lumos generate schema.lumos
문서: [Link to docs]
GitHub: [Repository URL]
질문이 있나요? 아래에 남겨 주세요!

