Sol.College

Full lesson

Shubbu03-veil

Private Payments on Solana using Magicblock's Ephemeral Rollup

What it does

From the project README:

Private, scheduled payments protocol for Solana using MagicBlock Ephemeral Rollups (ER).
Original
Shubbu03/veil
SuperStack fork
Blockchains/Shubbu03-veil @ 2e6776c
Stars
0
Last push (original)
2026-05-21
Languages
TypeScript, Rust, CSS, JavaScript, Dockerfile
Detected stack
AnchorNext.jsweb3.js
Categories
Payments
Licence
MIT
Homepage
https://app.veil-dev.com/

Architecture

135 source files. By extension: .ts ×66, .tsx ×36, .rs ×33.

Top-level directorySource files
dashboard51
program44
coordinator24
docs10
sdk6

On-chain programs: program/programs/veil

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/shubbu03-veil/. 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.

Shubbu03/veil · program/programs/veil/src/instructions/schedule/pause_schedule.rs L13–36 @ 2e6776c · MIT · original
    #[account(        seeds = [b"veil_config"],        bump    )]    pub config: Account<'info, VeilConfig>,     #[account(        mut,        has_one = employer @ VeilProgramError::Unauthorized,    )]    pub schedule: Account<'info, ScheduleAccount>,} impl<'info> PauseSchedule<'info> {    pub fn pause_schedule(&mut self, pause: bool) -> Result<()> {        require!(!self.config.paused, VeilProgramError::Paused);         let clock = Clock::get()?;        let timestamp = clock.unix_timestamp;         if pause {            require!(                self.schedule.status == ScheduleStatus::Active,                VeilProgramError::ScheduleNotActive
  1. Line 14 seeds = [b"veil_config"],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  2. Line 15 bump: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 21 has_one = employer @ VeilProgramError::Unauthorized,: Checks that a stored pubkey field matches the passed account.
  4. Line 28 require!(!self.config.paused, VeilProgramError::Paused);: An explicit validation that aborts the instruction with an error.
  5. Line 30 let clock = Clock::get()?;: Reads on-chain time from the Clock sysvar.
Shubbu03/veil · program/programs/veil/src/instructions/schedule/claim_payment.rs L121–144 @ 2e6776c · MIT · original
        require!(!is_paid, VeilProgramError::AlreadyPaid);         // Verify amount doesn't exceed per_execution_amount        require!(            amount > 0 && amount <= self.schedule.per_execution_amount,            VeilProgramError::InsufficientFunds        );         // Transfer tokens from vault_ata to recipient_ata        // Use vault PDA as signer        let employer_key = self.vault.employer;        let token_mint_key = self.vault.token_mint;        let bump = self.vault.bump;        let seeds = &[b"vault", employer_key.as_ref(), token_mint_key.as_ref(), &[bump]];        let signer = &[&seeds[..]];         let cpi_accounts = Transfer {            from: self.vault_ata.to_account_info(),            to: self.recipient_ata.to_account_info(),            authority: self.vault.to_account_info(),        };        let cpi_program = self.token_program.to_account_info();        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);        token::transfer(cpi_ctx, amount)?;
  1. Line 121 require!(!is_paid, VeilProgramError::AlreadyPaid);: An explicit validation that aborts the instruction with an error.
  2. Line 133 let bump = self.vault.bump;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 134 let seeds = &[b"vault", employer_key.as_ref(), token_mint_key.as_ref(), &[bump]];: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 143 let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  5. Line 144 token::transfer(cpi_ctx, amount)?;: An SPL token transfer through the Token Program.
Shubbu03/veil · program/programs/veil/src/instructions/schedule/create_schedule.rs L13–36 @ 2e6776c · MIT · original
pub struct CreateSchedule<'info> {    #[account(mut)]    pub employer: Signer<'info>,     #[account(        seeds = [b"veil_config"],        bump    )]    pub config: Account<'info, VeilConfig>,     #[account(        mut,        seeds = [b"vault", employer.key().as_ref(), vault.token_mint.as_ref()],        bump = vault.bump,        has_one = employer @ VeilProgramError::Unauthorized,    )]    pub vault: Account<'info, VaultAccount>,     #[account(        init,        payer = employer,        space = ANCHOR_DISCRIMINATOR + ScheduleAccount::INIT_SPACE,        seeds = [b"schedule", vault.key().as_ref(), schedule_id.as_ref()],        bump
  1. Line 15 pub employer: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 18 seeds = [b"veil_config"],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  3. Line 19 bump: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  4. Line 27 has_one = employer @ VeilProgramError::Unauthorized,: Checks that a stored pubkey field matches the passed account.
  5. Line 32 init,: Creates and pays for a new account. Space and payer must be specified.
Shubbu03/veil · program/programs/veil/src/instructions/vault/withdraw.rs L61–84 @ 2e6776c · MIT · original
        );         // Transfer tokens from vault to employer        let employer_key = self.employer.key();        let token_mint_key = self.token_mint.key();        let bump = self.vault.bump;        let seeds = &[b"vault", employer_key.as_ref(), token_mint_key.as_ref(), &[bump]];        let signer = &[&seeds[..]];         let cpi_accounts = Transfer {            from: self.vault_ata.to_account_info(),            to: self.employer_ata.to_account_info(),            authority: self.vault.to_account_info(),        };        let cpi_program = self.token_program.to_account_info();        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);        token::transfer(cpi_ctx, amount)?;         // Update vault state        self.vault.available = self            .vault            .available            .checked_sub(amount)            .ok_or(VeilProgramError::InsufficientFunds)?;
  1. Line 66 let bump = self.vault.bump;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  2. Line 67 let seeds = &[b"vault", employer_key.as_ref(), token_mint_key.as_ref(), &[bump]];: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  3. Line 76 let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  4. Line 77 token::transfer(cpi_ctx, amount)?;: An SPL token transfer through the Token Program.
  5. Line 83 .checked_sub(amount): Uses checked arithmetic so overflow fails instead of wrapping.

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
docsnpm run devnext dev -p 3000
docsnpm run startnext start
docsnpm run buildnext build
docsnpm run lintnext lint
sdknpm run devtsc --watch
sdknpm run buildtsc
coordinatornpm run devtsx watch src/index.ts
coordinatornpm run startnode dist/index.js
coordinatornpm run buildtsc
coordinatornpm run testecho "No tests yet"
dashboardnpm run devnext dev --webpack
dashboardnpm run startnext start
dashboardnpm run buildnext build --webpack
dashboardnpm run linteslint
programnpm run lintprettier */*.js "*/**/*{.js,.ts}" --check
programanchor buildcompile programs and generate IDLs

What you could build from it