Full lesson
featherlabs
New-gen onchain stateless asset protocol for ownership, delegation, and rentability of digital assets at scale.
What it does
From the project README:
Feather Assets is a unified, flexible, and stateless asset protocol for ownership, delegation, and rentability on the Solana blockchain.
- Original
- featherlabsxyz/featherlabs
- SuperStack fork
- Blockchains/featherlabs @
5f12cef - Stars
- 9
- Last push (original)
- 2024-10-09
- Languages
- TypeScript, Rust, JavaScript, CSS
- Detected stack
- AnchorMetaplexNext.jsweb3.js
- Categories
- NFTs & Digital Assets
- Licence
- Apache-2.0
- Homepage
- https://featherlabs.xyz
Architecture
64 source files. By extension: .rs ×25, .ts ×22, .tsx ×16, .js ×1.
| Top-level directory | Source files |
|---|---|
programs | 25 |
apps | 19 |
clients | 18 |
migrations | 1 |
tests | 1 |
On-chain programs: programs/feather_assets
Dapp: deploying
This fork is queued for its own live dapp on Sol.College, with a Devnet/Mainnet switch. It appears here, at /dapps/featherlabs/, once it is deployed and checked on devnet. Until then, 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.
programs/feather_assets/src/processor/group/create_group.rs L61–84 @ 5f12cef · Apache-2.0 · original .push(group_data.output_compressed_account(&crate::ID, remaining_accounts)?); msg!( "Group Metadata Compressed Account: {:?}", group_data_address ); } None => { group.has_metadata = false; } }; output_compressed_accounts.insert( 0, group .output_compressed_account(&crate::ID, remaining_accounts)? .unwrap(), ); let bump = Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], invoking_program).1; let signer_seeds = [CPI_AUTHORITY_PDA_SEED, &[bump]]; let instruction = InstructionDataInvokeCpi { proof: Some(lrp.proof), new_address_params, relay_fee: None, input_compressed_accounts_with_merkle_context: Vec::new(), output_compressed_accounts,- Line 62
msg!(: Logs to the program log, which is visible in explorers and the test output. - Line 77
let bump = Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], invoking_program).1;: Derives a PDA from seeds and the program ID and returns the address with its canonical bump. - Line 77
let bump = Pubkey::find_program_address(&[CPI_AUTHORITY_PDA_SEED], invoking_program).1;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 78
let signer_seeds = [CPI_AUTHORITY_PDA_SEED, &[bump]];: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
programs/feather_assets/src/processor/asset/create_member_asset.rs L97–120 @ 5f12cef · Apache-2.0 · original // acc.privilege_attributes = Vec::new(); let compressed = acc.output_compressed_account(&crate::ID, remaining_accounts)?; output_compressed_accounts.push(compressed); msg!("Asset Metadata Compressed Account: {:?}", address); } None => asset.has_metadata = false, } match args.royalties_initializable { true => asset.royalty_state = RoyaltyState::Unintialized, false => asset.royalty_state = RoyaltyState::Disabled, } output_compressed_accounts.insert( 1, asset .output_compressed_account(&crate::ID, remaining_accounts)? .unwrap(), ); let bump = Pubkey::find_program_address( &[CPI_AUTHORITY_PDA_SEED], ctx.accounts.get_invoking_program().key, ) .1; let signer_seeds = [CPI_AUTHORITY_PDA_SEED, &[bump]]; let instruction = InstructionDataInvokeCpi {- Line 100
msg!("Asset Metadata Compressed Account: {:?}", address);: Logs to the program log, which is visible in explorers and the test output. - Line 114
let bump = Pubkey::find_program_address(: Derives a PDA from seeds and the program ID and returns the address with its canonical bump. - Line 114
let bump = Pubkey::find_program_address(: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve. - Line 119
let signer_seeds = [CPI_AUTHORITY_PDA_SEED, &[bump]];: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
programs/feather_assets/src/processor/asset/add_royalties_asset.rs L49–72 @ 5f12cef · Apache-2.0 · original &asset_royalty.new_address_params().unwrap().seed, &address_merkle_context, )); msg!("Asset Royalty Compressed Account: {:?}", address); ctx.verify(lrp.proof)?; Ok(())}#[light_accounts]#[instruction(asset_derivation_key: Pubkey, asset_address: Pubkey)]pub struct AddRoyaltiesToAsset<'info> { #[account(mut)] #[fee_payer] pub authority: Signer<'info>, #[self_program] pub self_program: Program<'info, crate::program::FeatherAssets>, /// CHECK: Checked in light-system-program. #[authority] pub cpi_signer: AccountInfo<'info>, #[light_account(mut, seeds = [asset_derivation_key.to_bytes().as_ref()] constraint = asset.owner == authority.key() @ FeatherErrorCode::InvalidAssetSigner )] pub asset: LightAccount<AssetV1>, #[light_account( init,- Line 52
msg!("Asset Royalty Compressed Account: {:?}", address);: Logs to the program log, which is visible in explorers and the test output. - Line 61
pub authority: Signer<'info>,: Requires this account to have signed the transaction. - Line 67
#[light_account(mut, seeds = [asset_derivation_key.to_bytes().as_ref()]: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match. - Line 72
init,: Creates and pays for a new account. Space and payer must be specified.
programs/feather_assets/src/processor/group/transfer_group_authority.rs L25–48 @ 5f12cef · Apache-2.0 · original &address_merkle_context, )); msg!("Group Compressed Account: {:?}", group_address); ctx.verify(lrp.proof)?; Ok(())}#[light_accounts]#[instruction(derivation_key: Pubkey)]pub struct TransferGroupAuthority<'info> { #[account(mut)] #[fee_payer] pub authority: Signer<'info>, /// CHECK: Its safe pub new_authority: AccountInfo<'info>, #[self_program] pub self_program: Program<'info, crate::program::FeatherAssets>, /// CHECK: Checked in light-system-program. #[authority] pub cpi_signer: AccountInfo<'info>, #[light_account( mut, seeds = [derivation_key.to_bytes().as_ref()] constraint = authority.key() == group.owner @ FeatherErrorCode::InvalidGroupSigner )]- Line 27
msg!("Group Compressed Account: {:?}", group_address);: Logs to the program log, which is visible in explorers and the test output. - Line 36
pub authority: Signer<'info>,: Requires this account to have signed the transaction. - Line 46
seeds = [derivation_key.to_bytes().as_ref()]: 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.
| Directory | Command | Runs |
|---|---|---|
. | npm run lint | prettier */*.js "*/**/*{.js,.ts}" --check |
clients/js | npm run test | echo "Error: no test specified" && exit 1 |
apps/web | npm run dev | next dev |
apps/web | npm run start | next start |
apps/web | npm run build | next build |
apps/web | npm run lint | next lint |
. | anchor build | compile programs and generate IDLs |
. | anchor test | local validator + test suite |
. | anchor run test | pnpm ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts |
. | cargo build | Rust workspace build |
. | cargo test | Rust tests |
What you could build from it
- Fork the SuperStack copy and extend it within the NFTs & Digital Assets gaps listed on the idea page.
- Learn the pattern first: NFTs with Metaplex
- Learn the pattern first: Compressed NFTs and state compression
- Learn the pattern first: Token-2022 (Token Extensions)