Type-Safe Rust ↔ TypeScript Communication for Solana

Published: (December 17, 2025 at 06:14 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

Cover image for “Type‑Safe Rust ↔ TypeScript Communication for Solana”

LUMOS profile image
Rector Sol profile image

Building a Solana dApp with both on‑chain Rust and off‑chain TypeScript?

Here’s how to keep your types perfectly synchronized.

The Problem

You define a struct in Rust:

#[account]
pub struct PlayerAccount {
    pub wallet: Pubkey,
    pub level: u16,
    pub experience: u64,
}

Then manually recreate it in TypeScript:

export interface PlayerAccount {
  wallet: PublicKey;
  level: number;
  experience: number;
}

What could go wrong?

  • Field order mismatch → deserialization fails
  • Type‑size differences → corrupted data
  • Forgotten updates → runtime errors

The Solution: LUMOS

Define once, generate both:

// schema.lumos
#[solana]
#[account]
struct PlayerAccount {
    wallet: PublicKey,
    level: u16,
    experience: u64,
    inventory: [String],
    last_active: i64,
}

Generate the bindings:

lumos generate schema.lumos

Complete Workflow

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          │
              └─────────────────┘              └─────────────────┘              └─────────────────┘

Rust Program Usage

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>,
}

TypeScript Client Usage

Below are three common patterns for interacting with a Solana program from a TypeScript client:

1. Fetch Account Data

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. Subscribe to Updates

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. Build Transactions

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;
}

These snippets assume you have already generated the Borsh schema (PlayerAccountBorshSchema) and the Anchor IDL for your program. Adjust import paths as needed for your project structure.

Type Mapping Reference

LUMOSRustTypeScript
u8u64u8u64number
u128u128bigint
i8i64i8i64number
boolboolboolean
StringStringstring
PublicKeyPubkeyPublicKey

Types Overview

icKey
Pubkey
PublicKey

[T]
Vec
T[]

Option
Option
T \

Benefits

  • Single Source of Truth – Define types once.
  • Guaranteed Sync – Rust and TypeScript always match.
  • Correct Borsh – Field order and sizes are guaranteed.
  • Zero Runtime Overhead – Generated code is identical to hand‑written code.
  • IDE Support – Full TypeScript autocomplete.

Get Started

cargo install lumos-cli
lumos generate schema.lumos

Documentation: [Link to docs]
GitHub: [Repository URL]

Questions? Drop them below!

Back to Blog

Related posts

Read more »