Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/blueshift-gg/quasar/llms.txt

Use this file to discover all available pages before exploring further.

A token escrow lets two parties exchange different SPL tokens trustlessly. The maker deposits token A into a PDA-controlled vault and records how much token B they want in return. Any taker can then satisfy the offer by sending the required amount of token B to the maker and receiving the escrowed token A. If no taker appears, the maker can call refund to reclaim their tokens. This walkthrough covers the complete Quasar implementation: the Escrow state account, the Make, Take, and Refund instructions, and the on-chain event structs that let off-chain clients track lifecycle transitions.

Program Entry Point

declare_id!("22222222222222222222222222222222222222222222");

#[program]
mod quasar_escrow {
    use super::*;

    #[instruction(discriminator = 0)]
    pub fn make(ctx: Ctx<Make>, deposit: u64, receive: u64) -> Result<(), ProgramError> {
        ctx.accounts.make_escrow(receive, &ctx.bumps)?;
        ctx.accounts.emit_event(deposit, receive)?;
        ctx.accounts.deposit_tokens(deposit)
    }

    #[instruction(discriminator = 1)]
    pub fn take(ctx: Ctx<Take>) -> Result<(), ProgramError> {
        ctx.accounts.transfer_tokens()?;
        ctx.accounts.withdraw_tokens_and_close(&ctx.bumps)?;
        ctx.accounts.emit_event()
    }

    #[instruction(discriminator = 2)]
    pub fn refund(ctx: Ctx<Refund>) -> Result<(), ProgramError> {
        ctx.accounts.withdraw_tokens_and_close(&ctx.bumps)?;
        ctx.accounts.emit_event()
    }
}
Each instruction is a thin orchestrator: it delegates real work to methods implemented on the accounts context structs in their respective modules. This keeps the #[program] block readable and lets you unit-test the helper methods directly.

Escrow State

#[account(discriminator = 1, set_inner)]
#[seeds(b"escrow", maker: Address)]
pub struct Escrow {
    pub maker: Address,
    pub mint_a: Address,
    pub mint_b: Address,
    pub maker_ta_b: Address,
    pub receive: u64,
    pub bump: u8,
}
FieldTypePurpose
makerAddressThe wallet that created the escrow and deposited token A
mint_aAddressThe mint of the token deposited by the maker
mint_bAddressThe mint of the token the maker wants in return
maker_ta_bAddressToken account that should receive token B when the escrow is taken
receiveu64Amount of token B the maker requires
bumpu8Canonical PDA bump stored so the escrow can sign CPIs without re-deriving it
set_inner enables the set_inner(...) method, which populates all fields at once from a plain EscrowInner value — useful because the account is allocated empty by init and must be filled in the handler. #[seeds(b"escrow", maker: Address)] generates a Escrow::seeds(maker) function that returns the canonical ProgramDerivedAddress for a given maker pubkey, used in address = Escrow::seeds(maker.address()) constraints elsewhere.

PDA Derivation

On the client side, the escrow address is found the same way:
/// Seeds: [b"escrow", maker]
pub fn find_escrow_address(maker: &Address, program_id: &Address) -> (Address, u8) {
    Address::find_program_address(&[b"escrow", maker.as_ref()], program_id)
}
Each maker can have at most one active escrow at a time because the PDA is derived from a fixed seed plus the maker’s address — no nonce or counter is required.

Instruction 1: Make

The make instruction creates the Escrow PDA, opens two token accounts idempotently (one for the maker’s token B receipts, one vault for token A), and transfers the deposit from the maker’s existing token A account into the vault.

Accounts

#[derive(Accounts)]
pub struct Make {
    #[account(mut)]
    pub maker: Signer,
    #[account(init, payer = maker, address = Escrow::seeds(maker.address()))]
    pub escrow: Account<Escrow>,
    pub mint_a: Account<Mint>,
    pub mint_b: Account<Mint>,
    #[account(mut)]
    pub maker_ta_a: Account<Token>,
    #[account(init(idempotent), payer = maker, token(mint = mint_b, authority = maker, token_program = token_program))]
    pub maker_ta_b: Account<Token>,
    #[account(init(idempotent), payer = maker, token(mint = mint_a, authority = escrow, token_program = token_program))]
    pub vault_ta_a: Account<Token>,
    pub rent: Sysvar<Rent>,
    pub token_program: Program<TokenProgram>,
    pub system_program: Program<SystemProgram>,
}
Key constraints:
  • init, payer = maker, address = Escrow::seeds(maker.address()) — allocates the PDA, charges rent to maker, and validates that the provided address matches the derived seeds. If the address is wrong the instruction fails before any state is written.
  • init(idempotent) — creates the token account if it does not already exist, and succeeds silently if it does. Quasar validates that an existing account has the correct mint and authority, so this is safe even if the maker calls make twice (for example after a partial failure).
  • token(mint = mint_b, authority = maker, ...)maker_ta_b receives token B, owned by the maker.
  • token(mint = mint_a, authority = escrow, ...) — the vault is owned by the escrow PDA so only the program itself can move tokens out of it.

Implementation

impl Make {
    #[inline(always)]
    pub fn make_escrow(&mut self, receive: u64, bumps: &MakeBumps) -> Result<(), ProgramError> {
        self.escrow.set_inner(EscrowInner {
            maker: *self.maker.address(),
            mint_a: *self.mint_a.address(),
            mint_b: *self.mint_b.address(),
            maker_ta_b: *self.maker_ta_b.address(),
            receive,
            bump: bumps.escrow,
        });
        Ok(())
    }

    #[inline(always)]
    pub fn emit_event(&self, deposit: u64, receive: u64) -> Result<(), ProgramError> {
        emit!(MakeEvent {
            escrow: *self.escrow.address(),
            maker: *self.maker.address(),
            mint_a: *self.mint_a.address(),
            mint_b: *self.mint_b.address(),
            deposit,
            receive,
        });
        Ok(())
    }

    #[inline(always)]
    pub fn deposit_tokens(&mut self, amount: u64) -> Result<(), ProgramError> {
        self.token_program
            .transfer(&self.maker_ta_a, &self.vault_ta_a, &self.maker, amount)
            .invoke()
    }
}
bumps.escrow is the canonical bump that find_program_address returned when Quasar validated the address = constraint. Storing it in the escrow state avoids recomputing it during take and refund CPIs.

Make Event

#[event(discriminator = 0)]
pub struct MakeEvent {
    pub escrow: Address,
    pub maker: Address,
    pub mint_a: Address,
    pub mint_b: Address,
    pub deposit: u64,
    pub receive: u64,
}
emit!(MakeEvent { ... }) writes the event to the transaction log in a format that off-chain indexers can parse. The discriminator = 0 byte is prepended automatically.

Instruction 2: Take

Any taker can satisfy the escrow by calling take. The instruction verifies the escrow’s state, transfers token B from the taker to the maker, then transfers token A from the vault to the taker and closes both the vault token account and the escrow PDA.

Accounts

#[derive(Accounts)]
pub struct Take {
    #[account(mut)]
    pub taker: Signer,
    #[account(
        mut,
        has_one(maker),
        has_one(maker_ta_b),
        constraints(escrow.receive > 0),
        close(dest = taker),
        address = Escrow::seeds(maker.address())
    )]
    pub escrow: Account<Escrow>,
    #[account(mut)]
    pub maker: UncheckedAccount,
    pub mint_a: Account<Mint>,
    pub mint_b: Account<Mint>,
    #[account(init(idempotent), payer = taker, token(mint = mint_a, authority = taker, token_program = token_program))]
    pub taker_ta_a: Account<Token>,
    #[account(mut)]
    pub taker_ta_b: Account<Token>,
    #[account(init(idempotent), payer = taker, token(mint = mint_b, authority = maker, token_program = token_program))]
    pub maker_ta_b: Account<Token>,
    #[account(mut)]
    pub vault_ta_a: Account<Token>,
    pub rent: Sysvar<Rent>,
    pub token_program: Program<TokenProgram>,
    pub system_program: Program<SystemProgram>,
}
Notable constraints on escrow:
  • has_one(maker) — the maker account in this instruction must match escrow.maker.
  • has_one(maker_ta_b) — the maker_ta_b token account must match the address recorded in escrow.maker_ta_b.
  • constraints(escrow.receive > 0) — a custom inline constraint that prevents taking a zero-receive escrow.
  • close(dest = taker) — after the handler succeeds, Quasar will zero out the escrow account data and transfer its lamports to taker.

Implementation

impl Take {
    #[inline(always)]
    pub fn transfer_tokens(&mut self) -> Result<(), ProgramError> {
        self.token_program
            .transfer(
                &self.taker_ta_b,
                &self.maker_ta_b,
                &self.taker,
                self.escrow.receive,
            )
            .invoke()
    }

    #[inline(always)]
    pub fn withdraw_tokens_and_close(&self, bumps: &TakeBumps) -> Result<(), ProgramError> {
        let bump = [bumps.escrow];
        let seeds = [
            Seed::from(b"escrow" as &[u8]),
            Seed::from(self.maker.address().as_ref()),
            Seed::from(bump.as_ref()),
        ];

        self.token_program
            .transfer(
                &self.vault_ta_a,
                &self.taker_ta_a,
                &self.escrow,
                self.vault_ta_a.amount(),
            )
            .invoke_signed(&seeds)?;

        self.token_program
            .close_account(&self.vault_ta_a, &self.taker, &self.escrow)
            .invoke_signed(&seeds)
    }

    #[inline(always)]
    pub fn emit_event(&self) -> Result<(), ProgramError> {
        emit!(TakeEvent {
            escrow: *self.escrow.address(),
        });
        Ok(())
    }
}
The PDA signs the CPI by reconstructing the seed slice from the bump stored in escrow.bump. invoke_signed passes these seeds to the runtime, which verifies them against the escrow’s address before granting signing authority.

Instruction 3: Refund

If no taker appears, the maker calls refund to reclaim their token A and recover the lamports locked in the escrow PDA. The instruction mirrors take’s CPI logic but the maker is the beneficiary.

Accounts

#[derive(Accounts)]
pub struct Refund {
    #[account(mut)]
    pub maker: Signer,
    #[account(
        mut,
        has_one(maker),
        close(dest = maker),
        address = Escrow::seeds(maker.address())
    )]
    pub escrow: Account<Escrow>,
    pub mint_a: Account<Mint>,
    #[account(init(idempotent), payer = maker, token(mint = mint_a, authority = maker, token_program = token_program))]
    pub maker_ta_a: Account<Token>,
    #[account(mut)]
    pub vault_ta_a: Account<Token>,
    pub rent: Sysvar<Rent>,
    pub token_program: Program<TokenProgram>,
    pub system_program: Program<SystemProgram>,
}
close(dest = maker) closes the escrow PDA and returns its rent lamports to the maker. has_one(maker) ensures only the original maker can refund — a taker cannot drain the vault by calling refund with a different maker_ta_a.

Implementation

impl Refund {
    #[inline(always)]
    pub fn withdraw_tokens_and_close(&self, bumps: &RefundBumps) -> Result<(), ProgramError> {
        let bump = [bumps.escrow];
        let seeds = [
            Seed::from(b"escrow" as &[u8]),
            Seed::from(self.maker.address().as_ref()),
            Seed::from(bump.as_ref()),
        ];

        self.token_program
            .transfer(
                &self.vault_ta_a,
                &self.maker_ta_a,
                &self.escrow,
                self.vault_ta_a.amount(),
            )
            .invoke_signed(&seeds)?;

        self.token_program
            .close_account(&self.vault_ta_a, &self.maker, &self.escrow)
            .invoke_signed(&seeds)
    }

    #[inline(always)]
    pub fn emit_event(&self) -> Result<(), ProgramError> {
        emit!(RefundEvent {
            escrow: *self.escrow.address(),
        });
        Ok(())
    }
}

Escrow Lifecycle

1

Maker calls make

The maker provides deposit (how much token A to lock) and receive (how much token B they want). Quasar creates the Escrow PDA, opens the vault token account (owned by the escrow PDA), and transfers deposit lamports-worth of token A into the vault. A MakeEvent is emitted.
pub fn make(ctx: Ctx<Make>, deposit: u64, receive: u64) -> Result<(), ProgramError> {
    ctx.accounts.make_escrow(receive, &ctx.bumps)?;
    ctx.accounts.emit_event(deposit, receive)?;
    ctx.accounts.deposit_tokens(deposit)
}
2

Taker calls take (happy path)

Any taker who agrees to the price calls take. Quasar validates the has_one constraints, then the handler sends receive tokens of token B to the maker and withdraws all of token A from the vault to the taker. The vault token account is closed and the escrow PDA’s rent lamports are returned to the taker. A TakeEvent is emitted.
pub fn take(ctx: Ctx<Take>) -> Result<(), ProgramError> {
    ctx.accounts.transfer_tokens()?;
    ctx.accounts.withdraw_tokens_and_close(&ctx.bumps)?;
    ctx.accounts.emit_event()
}
3

Maker calls refund (cancellation path)

If the maker wishes to cancel, they call refund. The handler withdraws token A back to the maker’s token account and closes the vault and escrow. A RefundEvent is emitted.
pub fn refund(ctx: Ctx<Refund>) -> Result<(), ProgramError> {
    ctx.accounts.withdraw_tokens_and_close(&ctx.bumps)?;
    ctx.accounts.emit_event()
}

Events Summary

MakeEvent

Emitted when a maker creates a new escrow. Carries escrow, maker, mint_a, mint_b, deposit, and receive — everything an indexer needs to reconstruct the offer.

TakeEvent

Emitted when a taker fills the escrow. Carries only the escrow address; all other information can be fetched from the corresponding MakeEvent.

RefundEvent

Emitted when the maker cancels and reclaims their tokens. Carries the escrow address so indexers can mark the offer as closed.
The bump stored in escrow.bump is the canonical bump — the one Quasar found and verified during init. Always use this stored value for invoke_signed; never call find_program_address inside the handler because it wastes compute units and can return a different bump on later calls.

Build docs developers (and LLMs) love