Files
descro/docs/superpowers/plans/2026-05-19-solisting.md
2026-05-19 17:41:54 +02:00

53 KiB

Solisting Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

⚠️ Reference plan — do not implement yet. The descro plan (2026-05-19-descro-flow.md) must be fully implemented first. solisting CPIs into descro and depends on its create_escrow_prefunded instruction being available.

Goal: Build the solisting Anchor program that handles product listings and buyer-seller order consent, orchestrating the full flow from buyer intent → bilateral consent → active descro escrow.

Architecture: solisting is a standalone Anchor program with no reverse dependency on descro. It holds two account types: ListingAccount (seller's offer) and OrderAccount + OrderVault (buyer's pending purchase). On accept_order, solisting performs three CPI/transfers atomically: (1) checks resolver acceptance policy, (2) transfers SOL from OrderVault to the deterministic descro vault PDA, (3) CPIs into descro.create_escrow_prefunded to create an Active escrow. Descro does not know solisting exists.

Tech Stack: Rust, Anchor 1.0.x, LiteSVM 0.10.0. Depends on descro (CPI features) and descro_ext_resolvers (for AcceptancePolicy enum + ResolverEntry type). Build order: descro_ext_resolversdescrosolisting.


Prerequisites

Before starting Task 1, complete this prerequisite in descro_ext_resolvers.

solisting's accept_order must read the resolver's AcceptancePolicy from the registry. This enum doesn't exist yet and must be added to descro_ext_resolvers first.

Prerequisite: Add AcceptancePolicy to descro_ext_resolvers

Files:

  • Modify: programs/descro_ext_resolvers/src/state.rs

  • Modify: programs/descro_ext_resolvers/src/instructions/register.rs

  • Modify: programs/descro_ext_resolvers/src/lib.rs (register updated instruction signature)

  • P1: Add AcceptancePolicy enum and field to ResolverEntry

Replace programs/descro_ext_resolvers/src/state.rs with:

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct ResolverEntry {
    pub authority: Pubkey,
    pub resolver_type: ResolverType,
    pub acceptance_policy: AcceptancePolicy,
    #[max_len(64)]
    pub name: String,
    #[max_len(256)]
    pub description: String,
    pub fee_bps: u16,
    pub fee_recipient: Pubkey,
    #[max_len(256)]
    pub metadata_uri: String,
    pub total_resolved: u64,
    pub ruled_for_buyer: u64,
    pub ruled_for_seller: u64,
    pub registered_at: i64,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum ResolverType {
    CentralAuthority,
    JuryDAO,
    MAD,
    Algorithmic,
    Multisig,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum AcceptancePolicy {
    /// Anyone can use this resolver — no signature needed at escrow creation (JuryDAO, MAD)
    Open,
    /// Resolver must co-sign accept_order; their backend controls access (CentralAuthority)
    SignatureGated,
    /// Resolver is a program that validates via CPI to its own accept_escrow instruction
    ProgramGated,
}

/// Passed to update_stats; mirrors the Escrow program's Winner enum.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum Ruling {
    Buyer,
    Seller,
}
  • P2: Update register_resolver to accept acceptance_policy

Replace programs/descro_ext_resolvers/src/instructions/register.rs with:

use anchor_lang::prelude::*;
use crate::state::{AcceptancePolicy, ResolverEntry, ResolverType};
use crate::error::RegistryError;

#[derive(Accounts)]
pub struct RegisterResolver<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        init,
        payer = authority,
        space = 8 + ResolverEntry::INIT_SPACE,
        seeds = [b"resolver", authority.key().as_ref()],
        bump
    )]
    pub resolver_entry: Account<'info, ResolverEntry>,

    pub system_program: Program<'info, System>,
}

pub fn handler(
    ctx: Context<RegisterResolver>,
    resolver_type: ResolverType,
    acceptance_policy: AcceptancePolicy,
    name: String,
    description: String,
    fee_bps: u16,
    fee_recipient: Pubkey,
    metadata_uri: String,
) -> Result<()> {
    require!(!name.is_empty(), RegistryError::EmptyName);
    require!(fee_bps <= 10_000, RegistryError::InvalidFeeBps);

    let entry = &mut ctx.accounts.resolver_entry;
    entry.authority = ctx.accounts.authority.key();
    entry.resolver_type = resolver_type;
    entry.acceptance_policy = acceptance_policy;
    entry.name = name;
    entry.description = description;
    entry.fee_bps = fee_bps;
    entry.fee_recipient = fee_recipient;
    entry.metadata_uri = metadata_uri;
    entry.total_resolved = 0;
    entry.ruled_for_buyer = 0;
    entry.ruled_for_seller = 0;
    entry.registered_at = Clock::get()?.unix_timestamp;
    Ok(())
}
  • P3: Update lib.rs entrypoint signature for register_resolver

In programs/descro_ext_resolvers/src/lib.rs, update the register_resolver entrypoint:

pub fn register_resolver(
    ctx: Context<RegisterResolver>,
    resolver_type: ResolverType,
    acceptance_policy: AcceptancePolicy,
    name: String,
    description: String,
    fee_bps: u16,
    fee_recipient: Pubkey,
    metadata_uri: String,
) -> Result<()> {
    register::handler(ctx, resolver_type, acceptance_policy, name, description, fee_bps, fee_recipient, metadata_uri)
}
  • P4: Build both programs to confirm no regressions
cargo build-sbf --manifest-path programs/descro_ext_resolvers/Cargo.toml 2>&1 | tail -3
cargo build-sbf --manifest-path programs/descro/Cargo.toml 2>&1 | tail -3

Fix any test breakage caused by the changed RegisterResolver instruction signature (tests pass ResolverType positionally — add AcceptancePolicy as second argument).

  • P5: Run full test suites
cargo test --manifest-path programs/descro_ext_resolvers/Cargo.toml 2>&1 | tail -10
cargo test --manifest-path programs/descro/Cargo.toml 2>&1 | tail -10
  • P6: Commit prerequisite
git add programs/descro_ext_resolvers/src/state.rs \
        programs/descro_ext_resolvers/src/instructions/register.rs \
        programs/descro_ext_resolvers/src/lib.rs
git commit -m "feat(registry): add AcceptancePolicy enum to ResolverEntry"

File Map

Action Path Responsibility
Create programs/solisting/Cargo.toml Crate definition + dependencies
Create programs/solisting/src/lib.rs Program entrypoints + declare_id!
Create programs/solisting/src/state.rs ListingAccount + OrderAccount
Create programs/solisting/src/error.rs SolistingError enum
Create programs/solisting/src/constants.rs ORDER_TIMEOUT_SECS
Create programs/solisting/src/instructions.rs Module re-exports
Create programs/solisting/src/instructions/create_listing.rs Instruction
Create programs/solisting/src/instructions/update_listing.rs Instruction
Create programs/solisting/src/instructions/close_listing.rs Instruction
Create programs/solisting/src/instructions/create_order.rs Instruction
Create programs/solisting/src/instructions/accept_order.rs Core orchestration instruction
Create programs/solisting/src/instructions/reject_order.rs Instruction
Create programs/solisting/src/instructions/cancel_order.rs Instruction
Create programs/solisting/tests/common/mod.rs LiteSVM setup + PDA helpers + ix builders
Create programs/solisting/tests/test_listings.rs Listing instruction tests
Create programs/solisting/tests/test_orders.rs Order flow tests
Modify Cargo.toml (workspace root) Add solisting to workspace members

Task 1: Workspace + Crate Setup

Files:

  • Modify: Cargo.toml (workspace root)

  • Create: programs/solisting/Cargo.toml

  • Create: programs/solisting/src/lib.rs (stub)

  • Step 1: Add solisting to workspace

In the root Cargo.toml, add "programs/solisting" to the [workspace] members array.

  • Step 2: Create programs/solisting/Cargo.toml
[package]
name = "solisting"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "solisting"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
anchor-debug = []
custom-heap = []
custom-panic = []

[dependencies]
anchor-lang = "1.0.2"
descro = { path = "../descro", features = ["cpi"] }
descro_ext_resolvers = { path = "../descro_ext_resolvers" }

[dev-dependencies]
litesvm = "0.10.0"
solana-message = "3.0.1"
solana-transaction = "3.0.2"
solana-signer = "3.0.0"
solana-keypair = "3.0.1"

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
  • Step 3: Create stub lib.rs

Create programs/solisting/src/lib.rs:

pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;

use anchor_lang::prelude::*;

pub use error::*;
pub use instructions::*;
pub use state::*;

declare_id!("So1istingProgramID11111111111111111111111111"); // replace with actual after `anchor keys list`

Note: Run solana-keygen grind --starts-with Sol:1 to generate a vanity address, or just use anchor keys list after anchor build to get the auto-generated ID. Update both declare_id! and Anchor.toml with this ID.

  • Step 4: Create all empty module files
mkdir -p programs/solisting/src/instructions programs/solisting/tests/common
touch programs/solisting/src/constants.rs
touch programs/solisting/src/error.rs
touch programs/solisting/src/state.rs
touch programs/solisting/src/instructions.rs
touch programs/solisting/src/instructions/create_listing.rs
touch programs/solisting/src/instructions/update_listing.rs
touch programs/solisting/src/instructions/close_listing.rs
touch programs/solisting/src/instructions/create_order.rs
touch programs/solisting/src/instructions/accept_order.rs
touch programs/solisting/src/instructions/reject_order.rs
touch programs/solisting/src/instructions/cancel_order.rs
  • Step 5: Commit skeleton
git add programs/solisting/ Cargo.toml Cargo.lock
git commit -m "chore(solisting): add crate skeleton and workspace entry"

Task 2: State, Errors, Constants

Files:

  • programs/solisting/src/state.rs

  • programs/solisting/src/error.rs

  • programs/solisting/src/constants.rs

  • Step 1: Write state.rs

use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct ListingAccount {
    pub seller: Pubkey,
    pub price: u64,
    pub quantity: u32,
    pub resolver: Pubkey,
    #[max_len(256)]
    pub metadata_uri: String,
    pub listing_id: u64,
    pub state: ListingState,
    pub bump: u8,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum ListingState {
    Active,
    Closed,
}

#[account]
#[derive(InitSpace)]
pub struct OrderAccount {
    pub listing: Pubkey,
    pub buyer: Pubkey,
    pub seller: Pubkey,
    pub resolver: Pubkey,
    pub amount: u64,
    pub escrow_id: u64,
    pub state: OrderState,
    pub order_id: u64,
    pub created_at: i64,
    pub bump: u8,
    pub vault_bump: u8,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)]
pub enum OrderState {
    AwaitingSellerAccept,
    Accepted,
    Rejected,
}
  • Step 2: Write error.rs
use anchor_lang::prelude::*;

#[error_code]
pub enum SolistingError {
    #[msg("Listing is not active")]
    ListingNotActive,
    #[msg("No quantity available")]
    OutOfStock,
    #[msg("Signer is not authorized")]
    Unauthorized,
    #[msg("Order is not in AwaitingSellerAccept state")]
    InvalidOrderState,
    #[msg("Order cancellation timeout not reached")]
    CancelTimeoutNotReached,
    #[msg("Resolver requires a signature to accept this escrow")]
    ResolverSignatureRequired,
}
  • Step 3: Write constants.rs
/// Seconds a buyer must wait before cancelling an unresponded order.
pub const ORDER_TIMEOUT_SECS: i64 = 3 * 24 * 60 * 60; // 3 days
  • Step 4: Build (will fail until instructions are filled in — that's fine)
cargo build-sbf --manifest-path programs/solisting/Cargo.toml 2>&1 | grep "error\[" | head -10

Expected: errors about empty instruction files. Proceed.

  • Step 5: Commit
git add programs/solisting/src/state.rs programs/solisting/src/error.rs programs/solisting/src/constants.rs
git commit -m "feat(solisting): add state, errors, and constants"

Task 3: Listing Instructions

Files:

  • programs/solisting/src/instructions/create_listing.rs

  • programs/solisting/src/instructions/update_listing.rs

  • programs/solisting/src/instructions/close_listing.rs

  • programs/solisting/src/instructions.rs

  • Step 1: Write create_listing.rs

use anchor_lang::prelude::*;
use crate::state::{ListingAccount, ListingState};

#[derive(Accounts)]
#[instruction(price: u64, quantity: u32, resolver: Pubkey, metadata_uri: String, listing_id: u64)]
pub struct CreateListing<'info> {
    #[account(mut)]
    pub seller: Signer<'info>,

    #[account(
        init,
        payer = seller,
        space = 8 + ListingAccount::INIT_SPACE,
        seeds = [b"listing", seller.key().as_ref(), &listing_id.to_le_bytes()],
        bump
    )]
    pub listing_account: Account<'info, ListingAccount>,

    pub system_program: Program<'info, System>,
}

pub fn handler(
    ctx: Context<CreateListing>,
    price: u64,
    quantity: u32,
    resolver: Pubkey,
    metadata_uri: String,
    listing_id: u64,
) -> Result<()> {
    let listing = &mut ctx.accounts.listing_account;
    listing.seller = ctx.accounts.seller.key();
    listing.price = price;
    listing.quantity = quantity;
    listing.resolver = resolver;
    listing.metadata_uri = metadata_uri;
    listing.listing_id = listing_id;
    listing.state = ListingState::Active;
    listing.bump = ctx.bumps.listing_account;
    Ok(())
}
  • Step 2: Write update_listing.rs
use anchor_lang::prelude::*;
use crate::state::{ListingAccount, ListingState};
use crate::error::SolistingError;

#[derive(Accounts)]
pub struct UpdateListing<'info> {
    pub seller: Signer<'info>,

    #[account(
        mut,
        seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
        bump = listing_account.bump,
        constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
        constraint = listing_account.state == ListingState::Active @ SolistingError::ListingNotActive,
    )]
    pub listing_account: Account<'info, ListingAccount>,
}

pub fn handler(
    ctx: Context<UpdateListing>,
    price: u64,
    quantity: u32,
    metadata_uri: String,
) -> Result<()> {
    let listing = &mut ctx.accounts.listing_account;
    listing.price = price;
    listing.quantity = quantity;
    listing.metadata_uri = metadata_uri;
    Ok(())
}
  • Step 3: Write close_listing.rs
use anchor_lang::prelude::*;
use crate::state::{ListingAccount, ListingState};
use crate::error::SolistingError;

#[derive(Accounts)]
pub struct CloseListing<'info> {
    #[account(mut)]
    pub seller: Signer<'info>,

    #[account(
        mut,
        seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
        bump = listing_account.bump,
        constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
        close = seller,
    )]
    pub listing_account: Account<'info, ListingAccount>,
}

pub fn handler(ctx: Context<CloseListing>) -> Result<()> {
    ctx.accounts.listing_account.state = ListingState::Closed;
    Ok(())
}
  • Step 4: Write instructions.rs (partial — listing only for now)
#![allow(ambiguous_glob_reexports)]

pub mod accept_order;
pub mod cancel_order;
pub mod close_listing;
pub mod create_listing;
pub mod create_order;
pub mod reject_order;
pub mod update_listing;

pub use accept_order::*;
pub use cancel_order::*;
pub use close_listing::*;
pub use create_listing::*;
pub use create_order::*;
pub use reject_order::*;
pub use update_listing::*;
  • Step 5: Write stub lib.rs with listing entrypoints
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;

use anchor_lang::prelude::*;

pub use error::*;
pub use instructions::*;
pub use state::*;

declare_id!("So1istingProgramID11111111111111111111111111");

#[program]
pub mod solisting {
    use super::*;

    pub fn create_listing(
        ctx: Context<CreateListing>,
        price: u64,
        quantity: u32,
        resolver: Pubkey,
        metadata_uri: String,
        listing_id: u64,
    ) -> Result<()> {
        create_listing::handler(ctx, price, quantity, resolver, metadata_uri, listing_id)
    }

    pub fn update_listing(
        ctx: Context<UpdateListing>,
        price: u64,
        quantity: u32,
        metadata_uri: String,
    ) -> Result<()> {
        update_listing::handler(ctx, price, quantity, metadata_uri)
    }

    pub fn close_listing(ctx: Context<CloseListing>) -> Result<()> {
        close_listing::handler(ctx)
    }

    pub fn create_order(ctx: Context<CreateOrder>, order_id: u64, escrow_id: u64) -> Result<()> {
        create_order::handler(ctx, order_id, escrow_id)
    }

    pub fn accept_order(ctx: Context<AcceptOrder>) -> Result<()> {
        accept_order::handler(ctx)
    }

    pub fn reject_order(ctx: Context<RejectOrder>) -> Result<()> {
        reject_order::handler(ctx)
    }

    pub fn cancel_order(ctx: Context<CancelOrder>) -> Result<()> {
        cancel_order::handler(ctx)
    }
}
  • Step 6: Stub out the remaining instruction files so the program compiles:

programs/solisting/src/instructions/create_order.rs (stub):

use anchor_lang::prelude::*;
pub struct CreateOrder<'info> { pub system_program: Program<'info, System> }
pub fn handler(_ctx: Context<CreateOrder>, _order_id: u64, _escrow_id: u64) -> Result<()> { Ok(()) }

programs/solisting/src/instructions/accept_order.rs (stub):

use anchor_lang::prelude::*;
pub struct AcceptOrder<'info> { pub system_program: Program<'info, System> }
pub fn handler(_ctx: Context<AcceptOrder>) -> Result<()> { Ok(()) }

programs/solisting/src/instructions/reject_order.rs (stub):

use anchor_lang::prelude::*;
pub struct RejectOrder<'info> { pub system_program: Program<'info, System> }
pub fn handler(_ctx: Context<RejectOrder>) -> Result<()> { Ok(()) }

programs/solisting/src/instructions/cancel_order.rs (stub):

use anchor_lang::prelude::*;
pub struct CancelOrder<'info> { pub system_program: Program<'info, System> }
pub fn handler(_ctx: Context<CancelOrder>) -> Result<()> { Ok(()) }
  • Step 7: Build
cargo build-sbf --manifest-path programs/solisting/Cargo.toml 2>&1 | tail -5

Expected: Finished.

  • Step 8: Write listing tests

Create programs/solisting/tests/common/mod.rs:

#![allow(dead_code, unused_imports)]

pub use anchor_lang::prelude::Pubkey;
pub use solisting::{ListingState, OrderState};
use {
    anchor_lang::{
        solana_program::{instruction::Instruction, system_program},
        AccountDeserialize, InstructionData, ToAccountMetas,
    },
    solisting::{ListingAccount, OrderAccount},
    litesvm::LiteSVM,
    solana_keypair::Keypair,
    solana_message::{Message, VersionedMessage},
    solana_signer::Signer,
    solana_transaction::versioned::VersionedTransaction,
};

pub const PRICE: u64 = 1_000_000_000;
pub const LISTING_ID: u64 = 1;
pub const ORDER_ID: u64 = 1;
pub const ESCROW_ID: u64 = 42;

pub fn setup() -> (LiteSVM, Keypair, Keypair, Keypair) {
    let program_id = solisting::id();
    let mut svm = LiteSVM::new();

    let solisting_bytes = include_bytes!("../../../../target/deploy/solisting.so");
    svm.add_program(program_id, solisting_bytes).unwrap();

    let descro_bytes = include_bytes!("../../../../target/deploy/descro.so");
    svm.add_program(descro::id(), descro_bytes).unwrap();

    let registry_bytes = include_bytes!("../../../../target/deploy/descro_ext_resolvers.so");
    svm.add_program(descro_ext_resolvers::id(), registry_bytes).unwrap();

    let seller = Keypair::new();
    let buyer = Keypair::new();
    let resolver = Keypair::new();

    svm.airdrop(&seller.pubkey(), 10_000_000_000).unwrap();
    svm.airdrop(&buyer.pubkey(), 10_000_000_000).unwrap();
    svm.airdrop(&resolver.pubkey(), 5_000_000_000).unwrap();

    (svm, seller, buyer, resolver)
}

pub fn listing_pda(seller: &Pubkey, listing_id: u64) -> Pubkey {
    Pubkey::find_program_address(
        &[b"listing", seller.as_ref(), &listing_id.to_le_bytes()],
        &solisting::id(),
    ).0
}

pub fn order_pda(listing: &Pubkey, buyer: &Pubkey, order_id: u64) -> Pubkey {
    Pubkey::find_program_address(
        &[b"order", listing.as_ref(), buyer.as_ref(), &order_id.to_le_bytes()],
        &solisting::id(),
    ).0
}

pub fn order_vault_pda(order: &Pubkey) -> Pubkey {
    Pubkey::find_program_address(&[b"order_vault", order.as_ref()], &solisting::id()).0
}

pub fn escrow_pda(seller: &Pubkey, escrow_id: u64) -> Pubkey {
    Pubkey::find_program_address(
        &[b"escrow", seller.as_ref(), &escrow_id.to_le_bytes()],
        &descro::id(),
    ).0
}

pub fn send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) {
    let blockhash = svm.latest_blockhash();
    let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
    let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer]).unwrap();
    svm.send_transaction(tx).expect("transaction failed");
}

pub fn try_send(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair) -> bool {
    let blockhash = svm.latest_blockhash();
    let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
    let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[payer]).unwrap();
    svm.send_transaction(tx).is_ok()
}

pub fn send_multi(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair, extra: &[&Keypair]) {
    let blockhash = svm.latest_blockhash();
    let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
    let mut signers: Vec<&Keypair> = vec![payer];
    signers.extend_from_slice(extra);
    let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &signers).unwrap();
    svm.send_transaction(tx).expect("multi-signer transaction failed");
}

pub fn try_send_multi(svm: &mut LiteSVM, ix: Instruction, payer: &Keypair, extra: &[&Keypair]) -> bool {
    let blockhash = svm.latest_blockhash();
    let msg = Message::new_with_blockhash(&[ix], Some(&payer.pubkey()), &blockhash);
    let mut signers: Vec<&Keypair> = vec![payer];
    signers.extend_from_slice(extra);
    let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &signers).unwrap();
    svm.send_transaction(tx).is_ok()
}

pub fn ix_create_listing(
    seller: &Pubkey,
    price: u64,
    quantity: u32,
    resolver: &Pubkey,
    metadata_uri: &str,
    listing_id: u64,
) -> Instruction {
    let listing = listing_pda(seller, listing_id);
    Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::CreateListing {
            price,
            quantity,
            resolver: *resolver,
            metadata_uri: metadata_uri.to_string(),
            listing_id,
        }.data(),
        solisting::accounts::CreateListing {
            seller: *seller,
            listing_account: listing,
            system_program: system_program::ID,
        }.to_account_metas(None),
    )
}

pub fn read_listing(svm: &LiteSVM, seller: &Pubkey, listing_id: u64) -> ListingAccount {
    let pda = listing_pda(seller, listing_id);
    let account = svm.get_account(&pda).expect("listing not found");
    ListingAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
}

pub fn read_order(svm: &LiteSVM, listing: &Pubkey, buyer: &Pubkey, order_id: u64) -> OrderAccount {
    let pda = order_pda(listing, buyer, order_id);
    let account = svm.get_account(&pda).expect("order not found");
    OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
}

Create programs/solisting/tests/test_listings.rs:

mod common;
use common::*;
use solana_signer::Signer;

#[test]
fn seller_can_create_listing() {
    let (mut svm, seller, _buyer, resolver) = setup();

    send(
        &mut svm,
        ix_create_listing(&seller.pubkey(), PRICE, 5, &resolver.pubkey(), "ipfs://test", LISTING_ID),
        &seller,
    );

    let listing = read_listing(&svm, &seller.pubkey(), LISTING_ID);
    assert_eq!(listing.seller, seller.pubkey());
    assert_eq!(listing.price, PRICE);
    assert_eq!(listing.quantity, 5);
    assert_eq!(listing.state, ListingState::Active);
}

#[test]
fn seller_can_update_listing() {
    let (mut svm, seller, _buyer, resolver) = setup();
    send(
        &mut svm,
        ix_create_listing(&seller.pubkey(), PRICE, 5, &resolver.pubkey(), "ipfs://test", LISTING_ID),
        &seller,
    );

    let listing_pda_addr = listing_pda(&seller.pubkey(), LISTING_ID);
    let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::UpdateListing {
            price: PRICE * 2,
            quantity: 3,
            metadata_uri: "ipfs://updated".to_string(),
        }.data(),
        solisting::accounts::UpdateListing {
            seller: seller.pubkey(),
            listing_account: listing_pda_addr,
        }.to_account_metas(None),
    );
    send(&mut svm, ix, &seller);

    let listing = read_listing(&svm, &seller.pubkey(), LISTING_ID);
    assert_eq!(listing.price, PRICE * 2);
    assert_eq!(listing.quantity, 3);
}

#[test]
fn stranger_cannot_update_listing() {
    let (mut svm, seller, buyer, resolver) = setup();
    send(
        &mut svm,
        ix_create_listing(&seller.pubkey(), PRICE, 5, &resolver.pubkey(), "ipfs://test", LISTING_ID),
        &seller,
    );

    let listing_pda_addr = listing_pda(&seller.pubkey(), LISTING_ID);
    let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::UpdateListing {
            price: 1,
            quantity: 99,
            metadata_uri: "".to_string(),
        }.data(),
        solisting::accounts::UpdateListing {
            seller: buyer.pubkey(), // wrong
            listing_account: listing_pda_addr,
        }.to_account_metas(None),
    );
    assert!(!try_send(&mut svm, ix, &buyer));
}
  • Step 9: Run listing tests (will fail — stubs are empty)
cargo build-sbf --manifest-path programs/solisting/Cargo.toml && \
cargo test --manifest-path programs/solisting/Cargo.toml --test test_listings 2>&1 | tail -15

Tests fail because stubs have empty Accounts structs. This is expected — proceed to Task 4.

  • Step 10: Commit
git add programs/solisting/
git commit -m "feat(solisting): add listing instructions and stub order instructions"

Task 4: create_order — Buyer deposits into OrderVault

Files:

  • programs/solisting/src/instructions/create_order.rs

  • Step 1: Write the failing test (add to test_listings.rs or create test_orders.rs)

Create programs/solisting/tests/test_orders.rs:

mod common;
use common::*;
use solana_signer::Signer;

fn setup_listing(svm: &mut litesvm::LiteSVM, seller: &solana_keypair::Keypair, resolver: &solana_keypair::Keypair) {
    send(
        svm,
        ix_create_listing(&seller.pubkey(), PRICE, 5, &resolver.pubkey(), "ipfs://item", LISTING_ID),
        seller,
    );
}

fn ix_create_order(
    buyer: &anchor_lang::prelude::Pubkey,
    seller: &anchor_lang::prelude::Pubkey,
    listing: &anchor_lang::prelude::Pubkey,
) -> anchor_lang::solana_program::instruction::Instruction {
    let order = order_pda(listing, buyer, ORDER_ID);
    let vault = order_vault_pda(&order);
    anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::CreateOrder {
            order_id: ORDER_ID,
            escrow_id: ESCROW_ID,
        }.data(),
        solisting::accounts::CreateOrder {
            buyer: *buyer,
            seller: *seller,
            listing_account: *listing,
            order_account: order,
            order_vault: vault,
            system_program: anchor_lang::solana_program::system_program::ID,
        }.to_account_metas(None),
    )
}

#[test]
fn buyer_can_create_order() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    let listing = listing_pda(&seller.pubkey(), LISTING_ID);
    send(&mut svm, ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing), &buyer);

    let order = read_order(&svm, &listing, &buyer.pubkey(), ORDER_ID);
    assert_eq!(order.buyer, buyer.pubkey());
    assert_eq!(order.seller, seller.pubkey());
    assert_eq!(order.amount, PRICE);
    assert_eq!(order.state, OrderState::AwaitingSellerAccept);

    // Vault has the funds
    let vault = order_vault_pda(&order_pda(&listing, &buyer.pubkey(), ORDER_ID));
    let vault_lamports = svm.get_account(&vault).map(|a| a.lamports).unwrap_or(0);
    assert_eq!(vault_lamports, PRICE);
}

#[test]
fn create_order_fails_if_listing_inactive() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    // Close the listing first
    let listing_pda_addr = listing_pda(&seller.pubkey(), LISTING_ID);
    let ix_close = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::CloseListing {}.data(),
        solisting::accounts::CloseListing {
            seller: seller.pubkey(),
            listing_account: listing_pda_addr,
        }.to_account_metas(None),
    );
    send(&mut svm, ix_close, &seller);

    assert!(!try_send(
        &mut svm,
        ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing_pda_addr),
        &buyer,
    ));
}
  • Step 2: Implement create_order.rs
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, Transfer};
use crate::state::{ListingAccount, ListingState, OrderAccount, OrderState};
use crate::error::SolistingError;

#[derive(Accounts)]
#[instruction(order_id: u64, escrow_id: u64)]
pub struct CreateOrder<'info> {
    #[account(mut)]
    pub buyer: Signer<'info>,

    /// CHECK: seller pubkey read from listing; stored in order
    pub seller: UncheckedAccount<'info>,

    #[account(
        mut,
        seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
        bump = listing_account.bump,
        constraint = listing_account.state == ListingState::Active @ SolistingError::ListingNotActive,
        constraint = listing_account.quantity > 0 @ SolistingError::OutOfStock,
        constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
    )]
    pub listing_account: Account<'info, ListingAccount>,

    #[account(
        init,
        payer = buyer,
        space = 8 + OrderAccount::INIT_SPACE,
        seeds = [b"order", listing_account.key().as_ref(), buyer.key().as_ref(), &order_id.to_le_bytes()],
        bump
    )]
    pub order_account: Account<'info, OrderAccount>,

    /// CHECK: System-owned vault PDA; receives buyer's SOL
    #[account(
        mut,
        seeds = [b"order_vault", order_account.key().as_ref()],
        bump
    )]
    pub order_vault: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(ctx: Context<CreateOrder>, order_id: u64, escrow_id: u64) -> Result<()> {
    let listing = &ctx.accounts.listing_account;
    let amount = listing.price;

    system_program::transfer(
        CpiContext::new(
            system_program::ID,
            Transfer {
                from: ctx.accounts.buyer.to_account_info(),
                to: ctx.accounts.order_vault.to_account_info(),
            },
        ),
        amount,
    )?;

    let order = &mut ctx.accounts.order_account;
    order.listing = ctx.accounts.listing_account.key();
    order.buyer = ctx.accounts.buyer.key();
    order.seller = listing.seller;
    order.resolver = listing.resolver;
    order.amount = amount;
    order.escrow_id = escrow_id;
    order.state = OrderState::AwaitingSellerAccept;
    order.order_id = order_id;
    order.created_at = Clock::get()?.unix_timestamp;
    order.bump = ctx.bumps.order_account;
    order.vault_bump = ctx.bumps.order_vault;
    Ok(())
}
  • Step 3: Build and run tests
cargo build-sbf --manifest-path programs/solisting/Cargo.toml && \
cargo test --manifest-path programs/solisting/Cargo.toml --test test_orders -- buyer_can_create_order create_order_fails_if_listing_inactive 2>&1 | tail -15

Expected: both tests pass.

  • Step 4: Commit
git add programs/solisting/src/instructions/create_order.rs
git commit -m "feat(solisting): implement create_order — buyer deposits into OrderVault"

Task 5: reject_order and cancel_order

Files:

  • programs/solisting/src/instructions/reject_order.rs

  • programs/solisting/src/instructions/cancel_order.rs

  • Step 1: Write failing tests (add to test_orders.rs)

// Add to test_orders.rs (inside the file, after existing tests)

fn ix_reject_order(
    seller: &anchor_lang::prelude::Pubkey,
    listing: &anchor_lang::prelude::Pubkey,
    buyer: &anchor_lang::prelude::Pubkey,
) -> anchor_lang::solana_program::instruction::Instruction {
    let order = order_pda(listing, buyer, ORDER_ID);
    let vault = order_vault_pda(&order);
    anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::RejectOrder {}.data(),
        solisting::accounts::RejectOrder {
            seller: *seller,
            buyer: *buyer,
            order_account: order,
            order_vault: vault,
            system_program: anchor_lang::solana_program::system_program::ID,
        }.to_account_metas(None),
    )
}

#[test]
fn seller_can_reject_order() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    let listing = listing_pda(&seller.pubkey(), LISTING_ID);
    send(&mut svm, ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing), &buyer);

    let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
    send(&mut svm, ix_reject_order(&seller.pubkey(), &listing, &buyer.pubkey()), &seller);

    let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
    assert!(buyer_after > buyer_before); // SOL returned
    assert!(svm.get_account(&order_pda(&listing, &buyer.pubkey(), ORDER_ID)).is_none());
}

#[test]
fn buyer_can_cancel_after_timeout() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    let listing = listing_pda(&seller.pubkey(), LISTING_ID);
    send(&mut svm, ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing), &buyer);

    // Warp past ORDER_TIMEOUT_SECS (3 days)
    use anchor_lang::solana_program::clock::Clock;
    svm.set_sysvar(&Clock {
        slot: 1_000_000,
        epoch_start_timestamp: 0,
        epoch: 0,
        leader_schedule_epoch: 0,
        unix_timestamp: 3 * 24 * 60 * 60 + 1,
    });

    let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
    let order = order_pda(&listing, &buyer.pubkey(), ORDER_ID);
    let vault = order_vault_pda(&order);
    let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::CancelOrder {}.data(),
        solisting::accounts::CancelOrder {
            buyer: buyer.pubkey(),
            order_account: order,
            order_vault: vault,
            system_program: anchor_lang::solana_program::system_program::ID,
        }.to_account_metas(None),
    );
    send(&mut svm, ix, &buyer);

    let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
    assert!(buyer_after > buyer_before);
}

#[test]
fn buyer_cannot_cancel_before_timeout() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    let listing = listing_pda(&seller.pubkey(), LISTING_ID);
    send(&mut svm, ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing), &buyer);

    let order = order_pda(&listing, &buyer.pubkey(), ORDER_ID);
    let vault = order_vault_pda(&order);
    let ix = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::CancelOrder {}.data(),
        solisting::accounts::CancelOrder {
            buyer: buyer.pubkey(),
            order_account: order,
            order_vault: vault,
            system_program: anchor_lang::solana_program::system_program::ID,
        }.to_account_metas(None),
    );
    assert!(!try_send(&mut svm, ix, &buyer));
}
  • Step 2: Implement reject_order.rs
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, Transfer};
use crate::state::{OrderAccount, OrderState};
use crate::error::SolistingError;

#[derive(Accounts)]
pub struct RejectOrder<'info> {
    pub seller: Signer<'info>,

    /// CHECK: Buyer receives their SOL back
    #[account(mut)]
    pub buyer: UncheckedAccount<'info>,

    #[account(
        mut,
        seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
        bump = order_account.bump,
        constraint = seller.key() == order_account.seller @ SolistingError::Unauthorized,
        constraint = buyer.key() == order_account.buyer @ SolistingError::Unauthorized,
        constraint = order_account.state == OrderState::AwaitingSellerAccept @ SolistingError::InvalidOrderState,
        close = seller,
    )]
    pub order_account: Account<'info, OrderAccount>,

    /// CHECK: OrderVault PDA — SOL transferred back to buyer then this closes implicitly
    #[account(
        mut,
        seeds = [b"order_vault", order_account.key().as_ref()],
        bump = order_account.vault_bump,
    )]
    pub order_vault: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(ctx: Context<RejectOrder>) -> Result<()> {
    let order = &ctx.accounts.order_account;
    let vault_balance = ctx.accounts.order_vault.to_account_info().lamports();
    let order_key = order.key();
    let vault_bump = order.vault_bump;

    system_program::transfer(
        CpiContext::new_with_signer(
            system_program::ID,
            Transfer {
                from: ctx.accounts.order_vault.to_account_info(),
                to: ctx.accounts.buyer.to_account_info(),
            },
            &[&[b"order_vault", order_key.as_ref(), &[vault_bump]]],
        ),
        vault_balance,
    )?;

    Ok(())
}
  • Step 3: Implement cancel_order.rs
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, Transfer};
use crate::state::{OrderAccount, OrderState};
use crate::error::SolistingError;
use crate::constants::ORDER_TIMEOUT_SECS;

#[derive(Accounts)]
pub struct CancelOrder<'info> {
    #[account(mut)]
    pub buyer: Signer<'info>,

    #[account(
        mut,
        seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
        bump = order_account.bump,
        constraint = buyer.key() == order_account.buyer @ SolistingError::Unauthorized,
        constraint = order_account.state == OrderState::AwaitingSellerAccept @ SolistingError::InvalidOrderState,
        close = buyer,
    )]
    pub order_account: Account<'info, OrderAccount>,

    /// CHECK: OrderVault PDA — SOL returned to buyer
    #[account(
        mut,
        seeds = [b"order_vault", order_account.key().as_ref()],
        bump = order_account.vault_bump,
    )]
    pub order_vault: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(ctx: Context<CancelOrder>) -> Result<()> {
    let now = Clock::get()?.unix_timestamp;
    require!(
        now >= ctx.accounts.order_account.created_at + ORDER_TIMEOUT_SECS,
        SolistingError::CancelTimeoutNotReached
    );

    let order = &ctx.accounts.order_account;
    let vault_balance = ctx.accounts.order_vault.to_account_info().lamports();
    let order_key = order.key();
    let vault_bump = order.vault_bump;

    system_program::transfer(
        CpiContext::new_with_signer(
            system_program::ID,
            Transfer {
                from: ctx.accounts.order_vault.to_account_info(),
                to: ctx.accounts.buyer.to_account_info(),
            },
            &[&[b"order_vault", order_key.as_ref(), &[vault_bump]]],
        ),
        vault_balance,
    )?;

    Ok(())
}
  • Step 4: Build and run tests
cargo build-sbf --manifest-path programs/solisting/Cargo.toml && \
cargo test --manifest-path programs/solisting/Cargo.toml --test test_orders 2>&1 | tail -15

Expected: all tests pass.

  • Step 5: Commit
git add programs/solisting/src/instructions/reject_order.rs \
        programs/solisting/src/instructions/cancel_order.rs \
        programs/solisting/tests/test_orders.rs
git commit -m "feat(solisting): implement reject_order and cancel_order"

Task 6: accept_order — Core Orchestration

This is the most complex instruction. It:

  1. Checks resolver acceptance policy (Open → no-op, SignatureGatedresolver.is_signer, ProgramGated → CPI)
  2. Transfers SOL from OrderVault to the deterministic descro vault PDA address
  3. CPIs into descro.create_escrow_prefunded to create an Active escrow
  4. Decrements listing.quantity
  5. Closes OrderAccount (rent → seller)

Files:

  • programs/solisting/src/instructions/accept_order.rs

  • Step 1: Write failing tests (add to test_orders.rs)

// Helper — build accept_order ix (Open resolver, no registry entry needed)
fn ix_accept_order_open(
    seller: &anchor_lang::prelude::Pubkey,
    buyer: &anchor_lang::prelude::Pubkey,
    resolver: &anchor_lang::prelude::Pubkey,
    listing: &anchor_lang::prelude::Pubkey,
) -> anchor_lang::solana_program::instruction::Instruction {
    use anchor_lang::solana_program::system_program;
    let order = order_pda(listing, buyer, ORDER_ID);
    let order_vault = order_vault_pda(&order);
    let escrow_pda_addr = escrow_pda(seller, ESCROW_ID);
    let descro_vault = anchor_lang::prelude::Pubkey::find_program_address(
        &[b"vault", escrow_pda_addr.as_ref()],
        &descro::id(),
    ).0;
    let resolver_entry = anchor_lang::prelude::Pubkey::find_program_address(
        &[b"resolver", resolver.as_ref()],
        &descro_ext_resolvers::id(),
    ).0;

    anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        solisting::id(),
        &solisting::instruction::AcceptOrder { escrow_id: ESCROW_ID }.data(),
        solisting::accounts::AcceptOrder {
            seller: *seller,
            buyer: *buyer,
            resolver: *resolver,
            listing_account: *listing,
            order_account: order,
            order_vault,
            escrow_account: escrow_pda_addr,
            descro_vault,
            resolver_entry,
            descro_program: descro::id(),
            system_program: system_program::ID,
        }.to_account_metas(None),
    )
}

#[test]
fn seller_accept_creates_active_descro_escrow() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    let listing = listing_pda(&seller.pubkey(), LISTING_ID);
    send(&mut svm, ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing), &buyer);

    send(
        &mut svm,
        ix_accept_order_open(&seller.pubkey(), &buyer.pubkey(), &resolver.pubkey(), &listing),
        &seller,
    );

    // Descro escrow exists and is Active
    let escrow_addr = escrow_pda(&seller.pubkey(), ESCROW_ID);
    let account = svm.get_account(&escrow_addr).expect("escrow not found");
    let escrow = descro::EscrowAccount::try_deserialize(&mut account.data.as_slice()).unwrap();
    assert_eq!(escrow.state, descro::EscrowState::Active);
    assert_eq!(escrow.buyer, buyer.pubkey());
    assert_eq!(escrow.seller, seller.pubkey());

    // OrderAccount is closed
    assert!(svm.get_account(&order_pda(&listing, &buyer.pubkey(), ORDER_ID)).is_none());

    // Listing quantity decremented
    let listing_acc = read_listing(&svm, &seller.pubkey(), LISTING_ID);
    assert_eq!(listing_acc.quantity, 4);
}

#[test]
fn signature_gated_resolver_without_signature_fails() {
    let (mut svm, seller, buyer, resolver) = setup();
    setup_listing(&mut svm, &seller, &resolver);

    // Register the resolver as SignatureGated in the registry
    let resolver_entry_pda = anchor_lang::prelude::Pubkey::find_program_address(
        &[b"resolver", resolver.pubkey().as_ref()],
        &descro_ext_resolvers::id(),
    ).0;
    let ix_register = anchor_lang::solana_program::instruction::Instruction::new_with_bytes(
        descro_ext_resolvers::id(),
        &descro_ext_resolvers::instruction::RegisterResolver {
            resolver_type: descro_ext_resolvers::ResolverType::CentralAuthority,
            acceptance_policy: descro_ext_resolvers::state::AcceptancePolicy::SignatureGated,
            name: "Gated Resolver".to_string(),
            description: "Test".to_string(),
            fee_bps: 100,
            fee_recipient: resolver.pubkey(),
            metadata_uri: "ipfs://test".to_string(),
        }.data(),
        descro_ext_resolvers::accounts::RegisterResolver {
            authority: resolver.pubkey(),
            resolver_entry: resolver_entry_pda,
            system_program: anchor_lang::solana_program::system_program::ID,
        }.to_account_metas(None),
    );
    send(&mut svm, ix_register, &resolver);

    let listing = listing_pda(&seller.pubkey(), LISTING_ID);
    send(&mut svm, ix_create_order(&buyer.pubkey(), &seller.pubkey(), &listing), &buyer);

    // accept_order called with ONLY seller signing — resolver (SignatureGated) does not sign → must fail
    assert!(!try_send(
        &mut svm,
        ix_accept_order_open(&seller.pubkey(), &buyer.pubkey(), &resolver.pubkey(), &listing),
        &seller,
    ));
}
  • Step 2: Implement accept_order.rs
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, Transfer};
use crate::state::{ListingAccount, ListingState, OrderAccount, OrderState};
use crate::error::SolistingError;
use descro_ext_resolvers::state::AcceptancePolicy;

#[derive(Accounts)]
#[instruction(escrow_id: u64)]
pub struct AcceptOrder<'info> {
    #[account(mut)]
    pub seller: Signer<'info>,

    /// CHECK: Buyer pubkey verified against order_account
    pub buyer: UncheckedAccount<'info>,

    /// CHECK: Resolver — may or may not need to sign depending on AcceptancePolicy
    pub resolver: AccountInfo<'info>,

    #[account(
        mut,
        seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()],
        bump = listing_account.bump,
        constraint = seller.key() == listing_account.seller @ SolistingError::Unauthorized,
        constraint = listing_account.state == ListingState::Active @ SolistingError::ListingNotActive,
    )]
    pub listing_account: Account<'info, ListingAccount>,

    #[account(
        mut,
        seeds = [b"order", order_account.listing.as_ref(), order_account.buyer.as_ref(), &order_account.order_id.to_le_bytes()],
        bump = order_account.bump,
        constraint = buyer.key() == order_account.buyer @ SolistingError::Unauthorized,
        constraint = order_account.state == OrderState::AwaitingSellerAccept @ SolistingError::InvalidOrderState,
        close = seller,
    )]
    pub order_account: Account<'info, OrderAccount>,

    /// CHECK: OrderVault PDA — drained into descro vault
    #[account(
        mut,
        seeds = [b"order_vault", order_account.key().as_ref()],
        bump = order_account.vault_bump,
    )]
    pub order_vault: UncheckedAccount<'info>,

    /// CHECK: Descro EscrowAccount PDA — created by create_escrow_prefunded CPI
    #[account(
        mut,
        seeds = [b"escrow", seller.key().as_ref(), &escrow_id.to_le_bytes()],
        bump,
        seeds::program = descro::id(),
    )]
    pub escrow_account: UncheckedAccount<'info>,

    /// CHECK: Descro vault PDA — receives SOL from order_vault before CPI
    #[account(
        mut,
        seeds = [b"vault", escrow_account.key().as_ref()],
        bump,
        seeds::program = descro::id(),
    )]
    pub descro_vault: UncheckedAccount<'info>,

    /// CHECK: Optional resolver registry entry — used for policy check
    pub resolver_entry: UncheckedAccount<'info>,

    /// CHECK: Descro program — verified against descro::id() in handler
    pub descro_program: UncheckedAccount<'info>,

    pub system_program: Program<'info, System>,
}

pub fn handler(ctx: Context<AcceptOrder>, escrow_id: u64) -> Result<()> {
    require!(
        ctx.accounts.descro_program.key() == descro::id(),
        SolistingError::Unauthorized
    );

    // Check resolver acceptance policy
    if !ctx.accounts.resolver_entry.data_is_empty() {
        let entry_data = ctx.accounts.resolver_entry.try_borrow_data()?;
        let entry = descro_ext_resolvers::state::ResolverEntry::try_deserialize(
            &mut entry_data.as_ref(),
        )?;
        match entry.acceptance_policy {
            AcceptancePolicy::Open => {}
            AcceptancePolicy::SignatureGated => {
                require!(
                    ctx.accounts.resolver.is_signer,
                    SolistingError::ResolverSignatureRequired
                );
            }
            AcceptancePolicy::ProgramGated => {
                // For ProgramGated, a CPI to the resolver program's accept_escrow
                // would go here. Resolver program address is the resolver pubkey itself.
                // Left as future extension — ProgramGated resolvers are out of MVP scope.
            }
        }
    } else {
        // No registry entry → treat as SignatureGated (must have signed)
        require!(
            ctx.accounts.resolver.is_signer,
            SolistingError::ResolverSignatureRequired
        );
    }

    let order = &ctx.accounts.order_account;
    let amount = order.amount;
    let order_key = order.key();
    let vault_bump = order.vault_bump;

    // Transfer SOL from OrderVault → descro vault PDA
    // The descro vault doesn't exist yet but can receive lamports via system transfer
    system_program::transfer(
        CpiContext::new_with_signer(
            system_program::ID,
            Transfer {
                from: ctx.accounts.order_vault.to_account_info(),
                to: ctx.accounts.descro_vault.to_account_info(),
            },
            &[&[b"order_vault", order_key.as_ref(), &[vault_bump]]],
        ),
        amount,
    )?;

    // CPI: descro.create_escrow_prefunded
    descro::cpi::create_escrow_prefunded(
        CpiContext::new(
            ctx.accounts.descro_program.to_account_info(),
            descro::cpi::accounts::CreateEscrowPrefunded {
                seller: ctx.accounts.seller.to_account_info(),
                buyer: ctx.accounts.buyer.to_account_info(),
                escrow_account: ctx.accounts.escrow_account.to_account_info(),
                vault: ctx.accounts.descro_vault.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
            },
        ),
        amount,
        Some(ctx.accounts.resolver.key()),
        escrow_id,
    )?;

    // Decrement listing quantity
    ctx.accounts.listing_account.quantity -= 1;

    Ok(())
}
  • Step 3: Build
cargo build-sbf --manifest-path programs/solisting/Cargo.toml 2>&1 | tail -5
  • Step 4: Run all order tests
cargo test --manifest-path programs/solisting/Cargo.toml --test test_orders 2>&1 | tail -20

Expected: all tests pass.

  • Step 5: Run full suite for all three programs
cargo build-sbf --manifest-path programs/descro_ext_resolvers/Cargo.toml && \
cargo build-sbf --manifest-path programs/descro/Cargo.toml && \
cargo build-sbf --manifest-path programs/solisting/Cargo.toml && \
cargo test --manifest-path programs/descro_ext_resolvers/Cargo.toml 2>&1 | tail -5 && \
cargo test --manifest-path programs/descro/Cargo.toml 2>&1 | tail -5 && \
cargo test --manifest-path programs/solisting/Cargo.toml 2>&1 | tail -5

Expected: all green.

  • Step 6: Commit
git add programs/solisting/src/instructions/accept_order.rs \
        programs/solisting/tests/test_orders.rs
git commit -m "feat(solisting): implement accept_order — orchestrates SOL transfer + descro CPI"

Done

Full buyer-to-escrow flow is now working across three programs. Remaining items for future phases:

  • ProgramGated resolver CPI in accept_order
  • USDC vault support (SPL Token)
  • Listing quantity validation when closing (no open orders guard)
  • Event emission (emit!()) for off-chain indexers