Sol.College

Full lesson

cascade

hourly payment platform based on solana

What it does

From the project README:

Cascade is an hourly payment platform designed to pay employees on an hourly basis. It leverages the Solana blockchain to provide fast, transparent, and reliable payments for workers, ensuring that employees are compensated in real-time for the hours they work.
Original
praxzy/cascade
SuperStack fork
Blockchains/cascade @ 015b43e
Stars
1
Last push (original)
2026-01-23
Languages
TypeScript, Rust, CSS, Just, JavaScript, Shell
Detected stack
AnchorNext.js
Categories
Payments
Licence
Apache-2.0

Architecture

284 source files. By extension: .tsx ×166, .ts ×104, .rs ×11, .js ×3.

Top-level directorySource files
src247
anchor32
(root)5

On-chain programs: anchor/programs/cascade

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/cascade/. 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.

praxzy/cascade · anchor/programs/cascade/src/instructions/employer_emergency_withdraw.rs L13–36 @ 015b43e · Apache-2.0 · original
    );     // Check if 30 days have passed since employee's last activity    let employee_inactive_duration = clock.unix_timestamp - stream.employee_last_activity_at;    let thirty_days: i64 = 30 * 24 * 60 * 60;     require!(        employee_inactive_duration >= thirty_days,        ErrorCode::EmployeeStillActive    );     let remaining_balance = stream        .total_deposited        .checked_sub(stream.withdrawn_amount)        .ok_or(ErrorCode::InsufficientBalance)?;     // Transfer remaining balance back to employer    let employer_key = stream.employer.key();    let employee_key = stream.employee.key();    let seeds = &[        b"stream",        employer_key.as_ref(),        employee_key.as_ref(),        &[stream.bump],
  1. Line 16 let employee_inactive_duration = clock.unix_timestamp - stream.employee_last_activity_at;: Reads on-chain time from the Clock sysvar.
  2. Line 19 require!(: An explicit validation that aborts the instruction with an error.
  3. Line 26 .checked_sub(stream.withdrawn_amount): Uses checked arithmetic so overflow fails instead of wrapping.
  4. Line 32 let seeds = &[: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  5. Line 36 &[stream.bump],: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
praxzy/cascade · anchor/programs/cascade/src/instructions/withdraw.rs L37–60 @ 015b43e · Apache-2.0 · original
    // Transfer tokens from vault to employee using PDA signer    let employer_key = stream.employer.key();    let employee_key = stream.employee.key();    let seeds = &[        b"stream",        employer_key.as_ref(),        employee_key.as_ref(),        &[stream.bump],    ];    let signer = &[&seeds[..]];     let cpi_accounts = Transfer {        from: ctx.accounts.vault.to_account_info(),        to: ctx.accounts.employee_token_account.to_account_info(),        authority: stream.to_account_info(),    };    let cpi_program = ctx.accounts.token_program.to_account_info();    let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts).with_signer(signer);    token::transfer(cpi_ctx, amount)?;     // Update stream state    stream.withdrawn_amount = stream        .withdrawn_amount        .checked_add(amount)
  1. Line 40 let seeds = &[: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  2. Line 44 &[stream.bump],: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  3. Line 54 let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts).with_signer(signer);: Makes a cross-program invocation into another program.
  4. Line 55 token::transfer(cpi_ctx, amount)?;: An SPL token transfer through the Token Program.
  5. Line 60 .checked_add(amount): Uses checked arithmetic so overflow fails instead of wrapping.
praxzy/cascade · anchor/programs/cascade/src/instructions/close_stream.rs L13–36 @ 015b43e · Apache-2.0 · original
    Ok(())} #[derive(Accounts)]pub struct CloseStream<'info> {    #[account(mut)]    pub employer: Signer<'info>,     #[account(        mut,        close = employer,        seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()],        bump = stream.bump,        has_one = employer    )]    pub stream: Account<'info, PaymentStream>,     #[account(        mut,        close = employer,        seeds = [b"vault", stream.key().as_ref()],        bump,    )]    pub vault: Account<'info, TokenAccount>,
  1. Line 19 pub employer: Signer<'info>,: Requires this account to have signed the transaction.
  2. Line 23 close = employer,: Closes the account at the end of the instruction and refunds its lamports.
  3. Line 24 seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 25 bump = stream.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  5. Line 26 has_one = employer: Checks that a stored pubkey field matches the passed account.
praxzy/cascade · anchor/programs/cascade/src/instructions/top_up_stream.rs L25–47 @ 015b43e · Apache-2.0 · original
    stream.total_deposited = stream        .total_deposited        .checked_add(additional_amount)        .ok_or(ErrorCode::MathOverflow)?;     Ok(())} #[derive(Accounts)]pub struct TopUpStream<'info> {    #[account(mut)]    pub employer: Signer<'info>,     #[account(        mut,        seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()],        bump = stream.bump,        has_one = vault    )]    pub stream: Account<'info, PaymentStream>,     #[account(mut)]    pub vault: Account<'info, TokenAccount>,
  1. Line 27 .checked_add(additional_amount): Uses checked arithmetic so overflow fails instead of wrapping.
  2. Line 36 pub employer: Signer<'info>,: Requires this account to have signed the transaction.
  3. Line 40 seeds = [b"stream", employer.key().as_ref(), stream.employee.as_ref()],: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 41 bump = stream.bump,: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  5. Line 42 has_one = vault: Checks that a stored pubkey field matches the passed account.

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 devnext dev
.npm run startnext start
.npm run buildnext build
.npm run linteslint
anchoranchor buildcompile programs and generate IDLs
anchoranchor testlocal validator + test suite
anchoranchor run testpnpm exec vitest

What you could build from it

Used in lessons