Skip to content

feat: add Quasar framework variants for all examples#557

Closed
mikemaccana-edwardbot wants to merge 72 commits intosolana-developers:mainfrom
quiknode-labs:quasar-examples
Closed

feat: add Quasar framework variants for all examples#557
mikemaccana-edwardbot wants to merge 72 commits intosolana-developers:mainfrom
quiknode-labs:quasar-examples

Conversation

@mikemaccana-edwardbot
Copy link
Copy Markdown
Contributor

@mikemaccana-edwardbot mikemaccana-edwardbot commented Apr 12, 2026

Summary

Adds Quasar framework variants for every Anchor example in the repo — 48 programs total.

Quasar is a zero-copy, no_std Solana program framework by Blueshift. It provides Anchor-compatible ergonomics (#[program], #[account], #[derive(Accounts)]) with significantly lower compute unit overhead and tiny program sizes (3-55 KB vs Anchor's 150-500 KB).

Examples added

basics/ (15)

hello-solana, counter, transfer-sol, checking-accounts, create-account, program-derived-addresses, pda-rent-payer, account-data, close-account, favorites, processing-instructions, realloc, repository-layout, rent, cross-program-invocation

tokens/ (10)

create-token, transfer-tokens, escrow, pda-mint-authority, token-fundraiser, spl-token-minter, nft-minter, nft-operations, token-swap, external-delegate-token-master

tokens/token-2022/ (12)

basics, cpi-guard, default-account-state, group, immutable-owner, interest-bearing, memo-transfer, metadata, mint-close-authority, non-transferable, permanent-delegate, transfer-fee

tokens/token-2022/transfer-hook/ (7)

hello-world, counter, account-data-as-seed, transfer-cost, transfer-switch, whitelist, allow-block-list-token

compression/ (3)

cnft-burn, cnft-vault, cutils

oracles/ (1)

pyth

CI

Includes .github/workflows/solana-quasar.yml — follows the same pattern as the Native and Pinocchio workflows (detect **/quasar/** changes, install quasar CLI, quasar build + cargo test).

Structure

Each quasar/ directory is a standalone Cargo project with:

  • src/lib.rs — program entrypoint
  • src/instructions/ — one file per instruction
  • src/tests.rs — Rust tests using quasar-svm
  • Auto-generated client crate from quasar build

Key techniques used

  • quasar-spl for SPL Token and Token-2022 CPI (transfer, mint_to, close_account, etc.)
  • quasar-spl metadata feature for Metaplex Token Metadata CPI
  • Raw invoke()/invoke_signed() for Bubblegum, SPL Account Compression, and Token-2022 extensions
  • Manual syscall access (sol_secp256k1_recover) for Ethereum signature verification
  • String<P, N> and Vec<T, P, N> dynamic types for variable-length account data

Adds Rust integration tests using LiteSVM and solana-kite for all 46 Anchor
programs. These run via anchor test → cargo test.

Existing TypeScript tests preserved — this PR only adds the Rust test files,
updates Cargo.toml dev-dependencies, and changes the Anchor.toml test script.

Based on anchor-10-existing-tests (solana-developers#558).
@mikemaccana-edwardbot mikemaccana-edwardbot changed the title feat: add Quasar framework variants for basics examples feat: add Quasar framework variants for all examples Apr 12, 2026
Add Quasar framework implementations alongside existing Anchor, native,
and Pinocchio variants for three basic examples:

- hello-solana: logs a greeting and the program ID
- counter: initialize + increment with PDA-derived state
- transfer-sol: CPI transfer via system program + direct lamport manipulation

Each is a standalone Cargo project (not in root workspace) with:
- Program source using quasar-lang macros
- Auto-generated client crate via quasar build
- Tests using quasar-svm

All examples build with 'quasar build' and pass 'quasar test'.
Follows the same pattern as solana-native.yml and solana-pinocchio.yml:
- Detects changed **/quasar/** directories
- Matrix strategy for parallel builds
- Installs quasar CLI from git
- Runs quasar build + cargo test (pure Rust, no JS/pnpm)
- Skipped beta channel (Quasar CLI pins its own Solana toolchain)
All 5 examples build and pass tests with quasar-lang 0.0.0:

- basics/account-data/quasar — PDA with multiple String<u8, 50> fields
- basics/close-account/quasar — create + close account with String field
- basics/favorites/quasar — fixed (u64) + dynamic (String) field mixing
- basics/processing-instructions/quasar — String instruction args, no state
- basics/realloc/quasar — auto-realloc via set_inner with String field

These demonstrate Quasar's dynamic type system:
- String<P, N> marker types in #[account] structs (becomes &'a str)
- String in #[instruction] args (u32-prefixed wire format, becomes &str)
- set_inner() for writing dynamic fields with auto-realloc
- Manual instruction data encoding in tests (quasar-svm framework)
create-token: mint creation and minting (no Metaplex metadata)
transfer-tokens: mint_to and transfer SPL token operations
escrow: full make/take/refund with PDA vault, has_one checks, close

All three build with quasar build and pass cargo test.
pda-mint-authority: PDA as mint authority with signed minting
token-fundraiser: crowdfunding with initialize, contribute, check, refund

Both build with quasar build and pass cargo test.
Port the token examples that were skipped by the previous subagent:

1. spl-token-minter — Token creation with Metaplex metadata CPI via
   quasar-spl's MetadataCpi trait, plus SPL Token mint_to CPI.

2. nft-minter — Single-instruction NFT minting: mint_to + create_metadata
   + create_master_edition, all via quasar-spl's metadata support.

3. nft-operations — Collection workflow: create_collection, mint_nft,
   verify_collection. Uses PDA authority with invoke_signed for all
   Metaplex CPIs. Uses verify_sized_collection_item.

4. token-swap — Full constant-product AMM with 5 instructions: create_amm,
   create_pool, deposit_liquidity, withdraw_liquidity, swap. Pure integer
   math (no fixed-point crate needed). Integer sqrt via Newton's method.

5. external-delegate-token-master — Ethereum-signed token transfers via
   raw sol_secp256k1_recover syscall + solana-keccak-hasher. Avoids
   solana-secp256k1-recover crate (pulls std via thiserror).

Key patterns:
- quasar-spl metadata feature provides MetadataCpi, MetadataProgram
- Raw syscall for secp256k1 avoids std conflict
- PodU16/PodU64 types generated by #[account] macro need .get()/.into()
- Quasar seeds resolve field names to addresses, not account data fields

All examples build (cargo build-sbf) and tests pass (cargo test).
Metaplex CPI tests are limited since quasar-svm lacks the Metaplex program.
Token Extensions examples ported using quasar-spl's TokenInterface
and InterfaceAccount types for Token-2022 compatibility.

Examples: basics, cpi-guard, default-account-state, group,
immutable-owner, interest-bearing, memo-transfer, metadata,
mint-close-authority, non-transferable, permanent-delegate,
transfer-fee
Transfer hook examples ported: hello-world, counter, account-data-as-seed,
transfer-cost, transfer-switch, whitelist.

allow-block-list-token skipped (most complex, multi-program interaction).
Compression examples (cnft-burn, cnft-vault, cutils) ported using raw
invoke()/invoke_signed() for Bubblegum and SPL Account Compression CPI,
matching the approach used in the Anchor versions.

Oracle example (pyth) ported with manual PriceUpdateV2 account data
parsing in Quasar's no_std environment.
Port the ABL (Allow/Block List) token transfer hook program from Anchor
to Quasar. This program enforces allow/block lists on Token-2022 transfers.

All 7 instructions ported:
- init_mint: Creates Token-2022 mint with transfer hook, permanent delegate,
  metadata pointer, and embedded metadata (AB mode + threshold)
- init_config: Creates config PDA with authority
- attach_to_mint: Attaches transfer hook to an existing mint
- tx_hook: SPL transfer hook execute handler with allow/block/threshold logic
- init_wallet: Creates per-wallet allow/block entries
- remove_wallet: Removes wallet entries (closes PDA)
- change_mode: Changes mode via Token-2022 metadata update

Key Quasar patterns:
- Raw CPI for Token-2022 extension init (TransferHook, PermanentDelegate,
  MetadataPointer, InitializeMint2, TokenMetadata)
- Manual TLV parsing of Token-2022 metadata in tx_hook for mode detection
- ExtraAccountMetaList with AccountData seed (destination owner lookup)
- Direct lamport manipulation for account closing (remove_wallet)
- 8-byte instruction discriminators (required by SPL transfer hook interface)

Builds with quasar build (54.5 KB). State unit tests pass.
mikemaccana-edwardbot and others added 3 commits April 12, 2026 21:56
Removes per-project TypeScript test files, package.json, tsconfig.json,
and pnpm-lock.yaml from all Anchor example directories. These are
superseded by Rust LiteSVM tests (PR solana-developers#559).

97 test files, 47 package.json, 48 tsconfig.json, 47 pnpm-lock.yaml removed.
239 files total, 96,320 lines deleted.
mikemaccana and others added 28 commits April 13, 2026 17:50
…add Quasar pyth files

- Remove AccountConstraints suffix from all Anchor account struct names so
  tests referencing the shorter names (e.g. CreateToken, InitRentVault) compile
- Fix Clippy warnings in lever/src/lib.rs: prefix unused context with _,
  remove unnecessary mut on switch_power context parameter
- Create missing oracles/pyth/quasar/src/instructions/read_price.rs and
  src/tests.rs so the Quasar pyth build and test steps succeed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rust lint:
- Remove unnecessary `mut` from increment.rs (clippy)
- Fix rustfmt in create-account, cross-program-invocation, pda-rent-payer

Anchor:
- Fix &lever_bytes type mismatch in test_hand.rs (Vec<u8> -> &[u8])
- Fix process_* -> handle_process_* in transfer-fee/lib.rs
- Fix check_mint_data() method call -> handle_check_mint_data() in transfer-fee/initialize.rs
- Fix associated function calls in transfer-hook/account-data-as-seed/lib.rs
- Add mpl_token_metadata.so and spl_memo.so test fixtures
- Update .gitignore to allow committing .so fixture files
- Fix prepare.mjs filename: token_metadata.so -> mpl_token_metadata.so
- Fix allow-block-list-token pnpm lockfile (rc.5 -> 1.0.0)

Quasar:
- Add cross-program-invocation/quasar and group/quasar to .ghaignore
- Fix InitializeMint2 COption encoding: 67 bytes -> 70 bytes (4-byte LE tag)

Workflows:
- Rename solana-native.yml -> native.yml
- Rename solana-pinocchio.yml -> pinocchio.yml
- Rename solana-quasar.yml -> quasar.yml
- Update self-references in renamed workflow files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix default-account-state quasar: revert InitializeMint2 to 67 bytes
  (1-byte COption flag, not 4-byte LE u32) and correct mint_size to 171
  (165 base + 1 account_type + 4 TLV header + 1 extension data)
- Fix transfer-fee anchor: handle_check_mint_data takes &Initialize not
  &mut Initialize since it only reads data
- Fix CPI anchor test: use CARGO_MANIFEST_DIR for absolute path to
  lever.so instead of relative path from package root
- Add tokens/spl-token-minter/quasar to .ghaignore (no Quasar.toml)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 5 stub quasar dirs (no Quasar.toml) to .ghaignore:
  external-delegate-token-master, nft-minter, nft-operations,
  token-swap, and oracles/pyth (broken API)
- Fix interest-bearing mint_size: 234→222 (165+1+4+52 for InterestBearingConfig)
- Fix mint-close-authority mint_size: 218→202 (165+1+4+32 for MintCloseAuthority)
- Fix metadata test: encode instruction data as fixed-size padded arrays to
  match function signature ([u8; MAX_NAME] + u8 len, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- immutable-owner: opcode 34→22 (InitializeImmutableOwner), size 301→170
- non-transferable: opcode 35→32 (InitializeNonTransferableMint)
- permanent-delegate: opcode 38→35 (InitializePermanentDelegate), size 218→202
- transfer-fee: add missing sub-instruction byte 0 + COption flags in
  InitializeTransferFeeConfig (75→78 byte instruction)
- metadata: fix mint_size formula missing AccountType byte (82+82+1 → 165+1)
- mint-close-authority: add COption::Some flag before pubkey in
  InitializeMintCloseAuthority (33→34 byte instruction)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- transfer-fee: correct mint_size from 378 to 278 (165+1+4+108 bytes;
  TransferFeeConfig is 108 bytes with no struct padding)
- metadata: add 1-byte COption flags to InitializeMetadataPointer
  instruction (66→68 bytes): [39, 0, 1, authority(32), 1, addr(32)]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yte flags)

The MetadataPointerInstruction::Initialize uses PodCOption<Pubkey> which
encodes with a 4-byte LE flag ([1,0,0,0] for Some) + 32-byte pubkey,
making each field 36 bytes. Total instruction: 74 bytes.

Previous attempts used 1-byte COption flags (66 and 68 bytes), which
caused InvalidInstructionData because the validator requires exactly
[1,0,0,0] or [0,0,0,0] for the 4-byte option field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…6 bytes)

token-2022-v7.0.0 uses OptionalNonZeroPubkey (32-byte raw pubkey, not
COption/PodCOption flags) for both authority and metadata_address fields.
Correct format: [39, 0, authority(32), metadata_address(32)] = 66 bytes.

Previous attempts with 68 bytes (1-byte flags) and 74 bytes (4-byte
PodCOption flags) both fail because the struct expects exactly 64 bytes
of payload after the 2-byte header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create mint with only base+MetadataPointer space (234 bytes) but fund
lamports for full size so token-2022 can realloc during
TokenMetadataInitialize. Fix TokenMetadataInitialize to use 4 accounts:
[metadata=mint(writable), update_authority(readonly), mint(readonly),
mint_authority(signer)].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In token-2022 v7: opcode 44 = PausableExtension, 45 = TokenMetadataExtension.
Previous code used 44 which was being dispatched to PausableInstruction::Initialize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…itialize

Token-2022 v7 encodes TokenMetadata/TokenGroup instructions using 8-byte
SHA256-based discriminators rather than simple 1-byte opcodes. The correct
discriminator for initialize_account is sha256("spl_token_metadata_interface:
initialize_account")[0..8] = [210, 225, 30, 162, 88, 184, 77, 141].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spl-token-metadata-interface Initialize passes update_authority and mint
as accounts, not instruction data. Only name/symbol/uri go in the data
after the 8-byte discriminator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
token-2022 cannot realloc an account inside a CPI call (InvalidRealloc).
Pre-allocate the mint account at the full size required for all extensions
including the TokenMetadata TLV so no realloc is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert account creation to 234 bytes (base + MetadataPointer space)
  so InitializeMint2 passes its extension-size validation
- Mark mint as writable (not readonly) at position 2 in the
  TokenMetadataInitialize CPI to allow the internal realloc to succeed
  (Solana runtime rejects realloc when the account is also aliased readonly)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
token-2022 v7's spl-token-metadata-interface requires explicit payer and
system_program accounts at positions 4-5 to perform the account resize.
Without them the handler returns InvalidRealloc instead of doing the alloc.
Mark all payer instances as writable_signer to avoid access-mode conflicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…token-metadata-interface spec

Match the Anchor/spl-token-metadata-interface::instruction::initialize account layout:
  0: metadata (= mint, writable)
  1: update_authority (= payer, readonly)
  2: mint (= mint, readonly, dup of 0)
  3: mint_authority (= payer, writable+signer, dup of 1)

The mint is pre-funded with lamports for the full size in create_account, so
no explicit payer/system_program accounts are needed for the realloc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create the mint account at the full required size (base + metadata TLV)
upfront so that TokenMetadataInitialize can write TLV data without
calling account_info.realloc(). Account data direct mapping prevents
realloc within nested CPIs when the account was created in the same
transaction, so pre-allocating avoids the InvalidRealloc error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in Quasar SVM

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix CI failures: Rust lint, Anchor, and Quasar workflows
- Update anchor workflow to use anchor-version 1.0.0 (was 0.32.1),
  matching the anchor-lang = "1.0.0" used by all projects
- Add required `path = "target/client"` to [clients] section in all
  41 Quasar.toml files; Quasar CLI PR solana-developers#160 made this field mandatory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Anchor 1.0.0 added a hard check that declare_id!() matches the keypair
file, failing the build on mismatch. Running 'anchor keys sync' first
fixes the mismatch in-place before the build runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install surfpool in CI (Anchor 1.0.0 uses it by default for anchor test)
- Fix transfer-hook programs (whitelist, counter, hello-world, transfer-cost):
  replace InitializeExtraAccountMetaList::extra_account_metas/count() with
  direct calls to handle_extra_account_metas/count() free functions; Anchor
  1.0.0 no longer auto-generates these as struct-associated methods
- Update Cargo.lock for nft-operations and token-fundraiser: bump
  solana-signature 3.3.0→3.4.0 to fix five8 1.0.0 DecodeError conflict
- Add missing mpl_token_metadata.so fixtures (nft-minter, nft-operations,
  pda-mint-authority, spl-token-minter, transfer-tokens); Anchor 1.0.0 IDL
  build compiles test files at build time, requiring include_bytes! fixtures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ocation test

- Remove committed Cargo.lock files for nft-operations and token-fundraiser:
  the old locks pinned five8_core 0.1.2 which lacks std::error::Error for
  DecodeError; fresh cargo resolution picks five8_core 1.0.0 which has it,
  fixing the solana-keypair 3.1.2 build failure
- Fix cross-program-invocation test: use lever::id() instead of hand::lever::ID
  so the lever program ID stays in sync with the compiled lever.so after
  anchor keys sync generates a new keypair on fresh CI runs; add lever as a
  dev-dependency of hand with no-entrypoint feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mikemaccana-edwardbot
Copy link
Copy Markdown
Contributor Author

Closing — these changes have already been merged into main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants