Sol.College

Full lesson

CapRail

Cap table of a private company on Solana. Admission, vesting and ROFR are enforced by the token itself — a Token-2022 mint with a transfer hook — not by the interface.

What it does

From the project README:

Cap table of a private company on Solana, where the shareholder agreement is enforced by the token itself. The equity token is a Token-2022 mint with a transfer hook: every transfer — from the panel, from a script, from any wallet — runs the company's policy and investor registry on chain, and a transfer to a wallet that is not admitted is refused by the network, atomically, with the reason in the transaction logs. The cap table is read from the chain, not maintained by hand.
Original
capcaprail/CapRail
SuperStack fork
Blockchains/CapRail @ 2ea5c96
Stars
0
Last push (original)
2026-10-27
Languages
TypeScript, Rust, Shell, CSS, HTML, JavaScript
Detected stack
Anchorweb3.js
Categories
Infrastructure & Tooling
Licence
Apache-2.0
Homepage
https://capcaprail.github.io/CapRail/

Architecture

240 source files. By extension: .ts ×169, .rs ×48, .tsx ×21, .js ×2.

Top-level directorySource files
apps124
packages55
programs48
tools13

On-chain programs: programs/caprail, programs/caprail-hook

Dapp: devnet deploy pending

⏳ Devnet deploy pending — goes live when the devnet wallet is topped up. This fork's own dapp is not deployed yet, so there is no program address or transaction to show. It is queued in dapp-queue.txt; once devnet SOL is available it is deployed, exercised with real devnet transactions, and this section switches to the live dapp at /dapps/caprail/. Until then, the lesson and step-by-step guide below are complete, and you can run it locally with the commands below.

Key code, explained

Excerpts chosen automatically where the code uses the most Solana concepts, pinned to the fork's commit.

capcaprail/CapRail · programs/caprail-hook/src/initialize.rs L13–36 @ 2ea5c96 · Apache-2.0 · original
    #[account(mut)]    pub payer: Signer<'info>,    pub mint: InterfaceAccount<'info, Mint>,    // Хто має право оголосити список для мінта — той, хто володіє емісією.    // У CapRail це `Company` PDA всередині `create_token`, до відкликання права.    pub authority: Signer<'info>,    // PDA цієї програми: саме тут його шукає Token-2022    // (`get_extra_account_metas_address(mint, hook_program)`).    /// CHECK: акаунт стандарту інтерфейсу хука, не Anchor-тип    #[account(        init,        payer = payer,        space = ExtraAccountMetaList::size_of(EXTRA_ACCOUNT_COUNT)?,        seeds = [EXTRA_ACCOUNT_METAS_SEED, mint.key().as_ref()],        bump,    )]    pub extra_account_meta_list: UncheckedAccount<'info>,    pub system_program: Program<'info, System>,} pub fn initialize_extra_account_meta_list_handler(    ctx: Context<InitializeExtraAccountMetaList>,) -> Result<()> {    require!(
  1. Line 14 pub payer: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 23 init,: Creates and pays for a new account. Space and payer must be specified.
  3. Line 26 seeds = [EXTRA_ACCOUNT_METAS_SEED, mint.key().as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 27 bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  5. Line 36 require!(: An explicit validation that aborts the instruction with an error.
capcaprail/CapRail · programs/caprail/src/instructions/init_platform.rs L13–36 @ 2ea5c96 · Apache-2.0 · original
// `authority` живе офлайн: це єдиний ключ продукту поза гаманцями, і панель цю// інструкцію не будує — її будує `tools/demo init-platform`.#[derive(Accounts)]pub struct InitPlatform<'info> {    // Підписує і платить той самий офлайн-ключ; окремого платника немає, бо    // немає й сервера, який міг би ним бути.    #[account(mut)]    pub authority: Signer<'info>,    // `init`, а не `init_if_needed`: повторний виклик має впасти, а не    // переписати комісію під уже виставленими пропозиціями.    #[account(        init,        payer = authority,        space = PlatformConfig::SPACE,        seeds = [PlatformConfig::SEED],        bump,    )]    pub platform: Account<'info, PlatformConfig>,    // Стейблкоїн оплати. Інтерфейс, а не Token-2022: справжні стейблкоїни    // здебільшого на класичній токен-програмі, а демонстраційний — на Token-2022;    // `accept_offer` платить тією програмою, якій належить цей мінт.    pub payment_mint: Box<InterfaceAccount<'info, Mint>>,    // Рахунок комісії — саме токен-рахунок того ж мінта, а не гаманець-власник    // (T034): `accept_offer` не виводить ATA і не тягне зайвий акаунт у кадр.
  1. Line 20 pub authority: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 21 // `init`, а не `init_if_needed`: повторний виклик має впасти, а не: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
  3. Line 24 init,: Creates and pays for a new account. Space and payer must be specified.
  4. Line 27 seeds = [PlatformConfig::SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  5. Line 28 bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
capcaprail/CapRail · programs/caprail/src/instructions/set_investor_status.rs L37–60 @ 2ea5c96 · Apache-2.0 · original
    #[account(mut)]    pub compliance_officer: Signer<'info>,    // Статуси допуску ставить лише комплаєнс-офіцер; адміністратор не може    // (FR-004), і це перевіряє зв'язок, а не інтерфейс.    #[account(has_one = compliance_officer @ CaprailError::Unauthorized)]    pub company: Account<'info, Company>,    #[account(has_one = company @ CaprailError::Unauthorized)]    pub token_config: Account<'info, TokenConfig>,    // Перший статус створює запис, наступні — оновлюють. Хук акаунтів не    // створює, тож без цієї інструкції гаманець просто «не допущений».    #[account(        init_if_needed,        payer = compliance_officer,        space = InvestorRecord::SPACE,        seeds = [            InvestorRecord::SEED,            token_config.mint.as_ref(),            args.wallet.as_ref(),        ],        bump,    )]    pub investor_record: Account<'info, InvestorRecord>,    pub system_program: Program<'info, System>,}
  1. Line 38 pub compliance_officer: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 41 #[account(has_one = compliance_officer @ CaprailError::Unauthorized)]: Checks that a stored pubkey field matches the passed account.
  3. Line 48 init_if_needed,: Creates the account only if it doesn't exist. Watch for re-initialisation bugs.
  4. Line 51 seeds = [: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  5. Line 56 bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
capcaprail/CapRail · programs/caprail/src/instructions/accept_offer.rs L1–24 @ 2ea5c96 · Apache-2.0 · original
use anchor_lang::prelude::*;use anchor_lang::solana_program::program::invoke_signed;use anchor_spl::associated_token::AssociatedToken;use anchor_spl::token_interface::{    spl_token_2022, transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,};use spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi; use crate::errors::CaprailError;use crate::events::OfferAccepted;use crate::hook::HOOK_PROGRAM_ID;use crate::state::{Offer, OfferStatus, PlatformConfig, TokenConfig}; // Угода на вторинному ринку (FR-012, FR-013): три перекази в одній інструкції —// токен продавець → покупець (делегатом `Offer` PDA, через хук), оплата// покупець → продавець за вирахуванням комісії, комісія → рахунок платформи.// Будь-яка відмова (хук, баланс, делегування) відкочує всю транзакцію, тож// половини угоди в мережі не буває (SC-006).#[derive(Accounts)]pub struct AcceptOffer<'info> {    #[account(mut)]    pub buyer: Signer<'info>,    #[account(        seeds = [PlatformConfig::SEED],
  1. Line 2 use anchor_lang::solana_program::program::invoke_signed;: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  2. Line 5 spl_token_2022, transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,: A checked token transfer, which also verifies the mint and decimals.
  3. Line 5 spl_token_2022, transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,: Targets the Token-2022 program instead of the original Token Program.
  4. Line 22 pub buyer: Signer<'info>,: Requires this account to have signed the transaction.
  5. Line 24 seeds = [PlatformConfig::SEED],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.

How to run, build and test

These commands come from the repo's own manifests (package.json scripts, Anchor.toml, Cargo, go.mod, Makefile). Check the README for prerequisites and environment variables.

DirectoryCommandRuns
.npm run devpnpm -r --parallel dev
.npm run buildpnpm -r build
.npm run testpnpm -r test
.npm run lintbiome check .
packages/dbnpm run buildtsc -p tsconfig.json
packages/dbnpm run testvitest run --passWithNoTests
packages/chainnpm run buildtsc -p tsconfig.json
packages/chainnpm run testvitest run --passWithNoTests
packages/sharednpm run buildtsc -p tsconfig.json
packages/sharednpm run testvitest run --passWithNoTests
packages/indexernpm run buildtsc -p tsconfig.json
packages/indexernpm run testvitest run --passWithNoTests
apps/webnpm run devvite
apps/webnpm run buildvite build
apps/webnpm run testvitest run --passWithNoTests
apps/apinpm run devnode --watch --env-file-if-exists=../../.env src/index.ts

What you could build from it

Used in lessons