Solana를 위한 타입 안전 Rust ↔ TypeScript 통신

발행: (2025년 12월 18일 오전 08:14 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

“Type‑Safe Rust ↔ TypeScript Communication for Solana”에 대한 표지 이미지

LUMOS 프로필 이미지
Rector Sol 프로필 이미지

온‑체인 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 경로를 조정하세요.

타입 매핑 참조

LUMOSRustTypeScript
u8u64u8u64number
u128u128bigint
i8i64i8i64number
boolboolboolean
StringStringstring
PublicKeyPubkeyPublicKey

타입 개요

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]

질문이 있나요? 아래에 남겨 주세요!

Back to Blog

관련 글

더 보기 »