Anchor workspace for Solana primitives
programs/merke-airdrop: on-chain Anchor program entrypoints/accounts.programs/vesting: on-chain vesting program (cliff + linear release + revoke).programs/converter: on-chain SPL token converter with owner-managed exchange rate.programs/vault: on-chain vault for underlying SPL token and derivative token.crates/macro: shared ownership and pausable helpers.crates/airdrop-merkle: shared Merkle airdrop logic and account types.crates/vesting: shared vesting logic.crates/converter: shared token converter logic.crates/vault: shared vault logic and account types.utils/merkle-tree-generator: Bun TypeScript tool forrootandproofcommands.
Each shared crate with program-specific state generates its program ID from env vars in its own build.rs.
Supported env vars:
PRIMITIVO_MERKLE_AIRDROP_IDPRIMITIVO_VESTING_IDPRIMITIVO_CONVERTER_IDPRIMITIVO_VAULT_ID
You can override IDs per build:
PRIMITIVO_MERKLE_AIRDROP_ID=<YOUR_AIRDROP_PROGRAM_ID> \
PRIMITIVO_VESTING_ID=<YOUR_VESTING_PROGRAM_ID> \
anchor buildLocal defaults are set in .cargo/config.toml for this repo.
crates/macro/src/ownership.rs provides reusable ownership state:
ownerpending_ownerpending_expires_at
It is now embedded in on-chain state:
Distributor.ownershipinmerke_airdropVestingConfig.ownershipinvesting
Current usage in both programs enforces authority via ownership.owner.
Helper methods:
require_owner(signer)propose_transfer(signer, new_owner, now_ts, accept_window_secs)accept_transfer(signer, now_ts)cancel_transfer(signer)
Ownership macros for programs:
generate_ownership_transfer_accounts!generate_ownership_transfer_handlers!
Pausable macros for programs:
generate_pausable_accounts!generate_pausable_handlers!require_not_paused!(ctx, state_account)
Detailed usage:
- Ensure your state account has an
ownership: Ownershipfield. - Generate account context structs near the bottom of your program file:
primitivo_macro::generate_ownership_transfer_accounts!(
state_ty = Distributor,
state_account = distributor,
propose_ctx = ProposeOwnershipTransfer,
accept_ctx = AcceptOwnershipTransfer,
cancel_ctx = CancelOwnershipTransfer
);- Generate instruction handlers inside
#[program] mod ...:
primitivo_macro::generate_ownership_transfer_handlers!(
propose_fn = propose_ownership_transfer,
accept_fn = accept_ownership_transfer,
cancel_fn = cancel_ownership_transfer,
propose_ctx = ProposeOwnershipTransfer,
accept_ctx = AcceptOwnershipTransfer,
cancel_ctx = CancelOwnershipTransfer,
state_account = distributor
);Macro arguments:
state_ty: Anchor account struct type containingownership.state_account: account field name in generated contexts.propose_ctx/accept_ctx/cancel_ctx: generatedAccountsstruct names.propose_fn/accept_fn/cancel_fn: generated instruction function names.
Generated behavior:
propose: current owner setspending_owner+ expiry window.accept: pending owner accepts before expiry and becomesowner.cancel: current owner clears pending transfer.
Program instructions using this helper:
merke_airdrop:propose_ownership_transfer,accept_ownership_transfer,cancel_ownership_transfervesting:propose_ownership_transfer,accept_ownership_transfer,cancel_ownership_transfer
anchor build
anchor testvesting supports:
- configurable SPL mint per vesting config
- per-user vesting schedule
- cliff timestamp
- linear release between
start_tsandend_ts - authority revoke of unvested amount
Main instructions:
initialize_vesting_config(id)create_schedule(total_amount, start_ts, cliff_ts, end_ts)claim()revoke()pause()/unpause()
converter supports:
- configurable
from_mint/to_mint - owner-only exchange rate updates (
rate_numerator/rate_denominator) - token swap with slippage guard via
minimum_received
Main instructions:
initialize_converter(id, rate_numerator, rate_denominator)update_rate(rate_numerator, rate_denominator)(owner only)swap(amount_in, minimum_received)
vault supports:
- one underlying SPL token configured at init
- derivative token mint controlled by vault program
- deposit underlying -> mint derivative at current rate
- redeem derivative -> return underlying at current rate
- tracks
underlying_assetsin config state
Rate model:
deposit:derivative_out = amount_in * supply / underlying_assets(or1:1when empty)redeem:underlying_out = derivative_in * underlying_assets / supply
Main instructions:
initialize_vault(id, derivative_decimals)deposit(underlying_amount)redeem(derivative_amount)
Airdrop file format is one line per user:
<address> <amount>Example:
BNGRxgaJ9TBB2RScFDhZCxvHoXqtpJnG6BMTZjfeZETj 100
55XMjVxUhLqErahWDJ1gikHhxAufWivYkTvs4DcRh7ry 200
AXruuuQaXDZQd7t4pJfFTp1zDpYiDg6QMCBfk9UTHQoN 300Commands:
cd utils/merkle-tree-generator
bun install
bun run root --input ./addresses.txt
bun run proof --input ./addresses.txt --address <WALLET>proof output includes index, amount, and proof for on-chain claim.