# 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_resolvers` → `descro` → `solisting`. --- ## 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: ```rust 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: ```rust 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, 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: ```rust pub fn register_resolver( ctx: Context, 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** ```bash 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** ```bash 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** ```bash 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`** ```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`: ```rust 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** ```bash 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** ```bash 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`** ```rust 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`** ```rust 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`** ```rust /// 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)** ```bash 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** ```bash 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`** ```rust 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, 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`** ```rust 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, 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`** ```rust 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) -> Result<()> { ctx.accounts.listing_account.state = ListingState::Closed; Ok(()) } ``` - [ ] **Step 4: Write `instructions.rs`** (partial — listing only for now) ```rust #![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** ```rust 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, 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, price: u64, quantity: u32, metadata_uri: String, ) -> Result<()> { update_listing::handler(ctx, price, quantity, metadata_uri) } pub fn close_listing(ctx: Context) -> Result<()> { close_listing::handler(ctx) } pub fn create_order(ctx: Context, order_id: u64, escrow_id: u64) -> Result<()> { create_order::handler(ctx, order_id, escrow_id) } pub fn accept_order(ctx: Context) -> Result<()> { accept_order::handler(ctx) } pub fn reject_order(ctx: Context) -> Result<()> { reject_order::handler(ctx) } pub fn cancel_order(ctx: Context) -> 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): ```rust use anchor_lang::prelude::*; pub struct CreateOrder<'info> { pub system_program: Program<'info, System> } pub fn handler(_ctx: Context, _order_id: u64, _escrow_id: u64) -> Result<()> { Ok(()) } ``` `programs/solisting/src/instructions/accept_order.rs` (stub): ```rust use anchor_lang::prelude::*; pub struct AcceptOrder<'info> { pub system_program: Program<'info, System> } pub fn handler(_ctx: Context) -> Result<()> { Ok(()) } ``` `programs/solisting/src/instructions/reject_order.rs` (stub): ```rust use anchor_lang::prelude::*; pub struct RejectOrder<'info> { pub system_program: Program<'info, System> } pub fn handler(_ctx: Context) -> Result<()> { Ok(()) } ``` `programs/solisting/src/instructions/cancel_order.rs` (stub): ```rust use anchor_lang::prelude::*; pub struct CancelOrder<'info> { pub system_program: Program<'info, System> } pub fn handler(_ctx: Context) -> Result<()> { Ok(()) } ``` - [ ] **Step 7: Build** ```bash 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`: ```rust #![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`: ```rust 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)** ```bash 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** ```bash 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`: ```rust 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`** ```rust 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, 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** ```bash 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** ```bash 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`) ```rust // 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`** ```rust 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) -> 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`** ```rust 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) -> 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** ```bash 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** ```bash 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, `SignatureGated` → `resolver.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`) ```rust // 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`** ```rust 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, 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** ```bash cargo build-sbf --manifest-path programs/solisting/Cargo.toml 2>&1 | tail -5 ``` - [ ] **Step 4: Run all order tests** ```bash 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** ```bash 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** ```bash 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