# 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.** Two prerequisites must be completed first: > 1. `2026-05-19-descro-buyer-flow.md` — adds `buyer_create_escrow`, `seller_confirm`, and symmetric `cancel` to descro > 2. `AcceptancePolicy` prerequisite from the original solisting plan (adding `AcceptancePolicy` enum to `descro_ext_resolvers`) — already documented in the old plan's prerequisite section **Goal:** Build the `solisting` Anchor program — a coordination and discovery layer for product listings and bilateral order consent. It orchestrates the full flow from buyer intent → bilateral consent → active `descro` escrow, without ever touching the descro vault directly. **Architecture:** - `solisting` holds two account types: `ListingAccount` (seller's offer with payment options) and `OrderAccount` (pending bilateral consent record). - Payment method is negotiated per-order: the seller declares supported options in the listing (`DescroPaymentOption` with accepted resolvers), the buyer picks one when creating an order. - For Descro orders: `create_order` CPIs `descro.buyer_create_escrow` atomically — funds go directly into descro's vault. `accept_order` CPIs `descro.seller_confirm`. Solisting never holds or transfers escrow SOL. - The `OrderAccount` has no state enum. Its existence means the order is pending. Closure means it is resolved. - State drift (buyer cancelling directly on descro) is handled gracefully: all terminal solisting instructions read the descro escrow state first and skip the CPI if descro already resolved it. **Key design decisions:** - No `OrderVault` — funds live in descro's vault from the moment `create_order` is called - `AcceptancePolicy` is enforced by descro in `seller_confirm`, not by solisting - Listing `quantity_reserved` tracks pending orders; decremented at `create_order`, restored at `reject_order`/`cancel_order` - `escrow_id` is derived from the first 8 bytes of the `order_account` PDA to guarantee uniqueness without coordinator state - `close_stale_order` can be called by anyone to clean up an OrderAccount whose descro escrow is already in a terminal state **Tech Stack:** Rust, Anchor 1.0.x, LiteSVM 0.10.0. Depends on `descro` (CPI features) and `descro_ext_resolvers` (for `AcceptancePolicy` type). Build order: `descro_ext_resolvers` → `descro` → `solisting`. --- ## 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 + PaymentOption | | Create | `programs/solisting/src/error.rs` | SolistingError enum | | Create | `programs/solisting/src/instructions.rs` | Module re-exports | | Create | `programs/solisting/src/instructions/create_listing.rs` | | | Create | `programs/solisting/src/instructions/update_listing.rs` | | | Create | `programs/solisting/src/instructions/close_listing.rs` | | | Create | `programs/solisting/src/instructions/create_order.rs` | Core: validates payment option, CPIs buyer_create_escrow | | Create | `programs/solisting/src/instructions/accept_order.rs` | CPIs seller_confirm | | Create | `programs/solisting/src/instructions/reject_order.rs` | CPIs descro.cancel (defensive) | | Create | `programs/solisting/src/instructions/cancel_order.rs` | CPIs descro.cancel (defensive) | | Create | `programs/solisting/src/instructions/close_stale_order.rs` | Cleans up orphaned OrderAccounts | | Create | `programs/solisting/tests/common/mod.rs` | LiteSVM setup + helpers | | 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 - [ ] **Step 1: Add `solisting` to workspace 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 directory structure** ```bash mkdir -p programs/solisting/src/instructions programs/solisting/tests/common touch programs/solisting/src/lib.rs touch programs/solisting/src/state.rs touch programs/solisting/src/error.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 touch programs/solisting/src/instructions/close_stale_order.rs ``` - [ ] **Step 4: 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 and Errors **Files:** `state.rs`, `error.rs` - [ ] **Step 1: Write `state.rs`** ```rust use anchor_lang::prelude::*; #[account] #[derive(InitSpace)] pub struct ListingAccount { pub seller: Pubkey, pub price: u64, /// Total units available (including reserved). pub quantity: u32, /// Units held by pending (AwaitingSellerAccept) orders. quantity - quantity_reserved = available. pub quantity_reserved: u32, /// Descro payment option; None means descro is not accepted for this listing. pub descro_option: Option, #[max_len(256)] pub metadata_uri: String, pub listing_id: u64, pub is_active: bool, pub bump: u8, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, InitSpace, Debug)] pub struct DescroPaymentOption { /// Resolvers the seller accepts. Empty = any resolver is acceptable. #[max_len(4)] pub accepted_resolvers: Vec, } #[account] #[derive(InitSpace)] pub struct OrderAccount { pub listing: Pubkey, pub buyer: Pubkey, pub seller: Pubkey, /// The resolver agreed upon for this order (validated against listing's accepted_resolvers). pub resolver: Pubkey, pub amount: u64, /// The descro EscrowAccount PDA for this order. pub escrow_account: Pubkey, /// Derived from order_account PDA bytes[0..8]; used as descro escrow_id seed. pub escrow_id: u64, pub order_id: u64, pub created_at: i64, pub bump: u8, } ``` > **`escrow_id` derivation note:** At `create_order` time, the `order_account` PDA has been derived but not yet initialized. Solisting passes the `escrow_id` as an instruction argument. The frontend/SDK derives it as `u64::from_le_bytes(order_pda.to_bytes()[0..8])`. This ties the escrow_id to the order PDA, ensuring uniqueness as long as `order_id` is unique per buyer+listing. - [ ] **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("Listing does not support Descro payments")] DescroNotSupported, #[msg("Resolver is not accepted by this listing")] ResolverNotAccepted, #[msg("Descro escrow is not in expected state — order may have already been resolved")] EscrowStateUnexpected, #[msg("Descro program address mismatch")] InvalidDescroProgram, } ``` --- ## Task 3: Listing Instructions **Files:** `create_listing.rs`, `update_listing.rs`, `close_listing.rs` - [ ] **Step 1: Write `create_listing.rs`** ```rust use anchor_lang::prelude::*; use crate::state::{DescroPaymentOption, ListingAccount}; #[derive(Accounts)] #[instruction(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, listing_id: u64, price: u64, quantity: u32, descro_option: Option, metadata_uri: String, ) -> Result<()> { let listing = &mut ctx.accounts.listing_account; listing.seller = ctx.accounts.seller.key(); listing.price = price; listing.quantity = quantity; listing.quantity_reserved = 0; listing.descro_option = descro_option; listing.metadata_uri = metadata_uri; listing.listing_id = listing_id; listing.is_active = true; listing.bump = ctx.bumps.listing_account; Ok(()) } ``` - [ ] **Step 2: Write `update_listing.rs`** ```rust use anchor_lang::prelude::*; use crate::state::{DescroPaymentOption, ListingAccount}; 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.is_active @ SolistingError::ListingNotActive, )] pub listing_account: Account<'info, ListingAccount>, } pub fn handler( ctx: Context, price: u64, quantity: u32, descro_option: Option, metadata_uri: String, ) -> Result<()> { let listing = &mut ctx.accounts.listing_account; listing.price = price; listing.quantity = quantity; listing.descro_option = descro_option; listing.metadata_uri = metadata_uri; Ok(()) } ``` - [ ] **Step 3: Write `close_listing.rs`** ```rust use anchor_lang::prelude::*; use crate::state::ListingAccount; 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<()> { Ok(()) } ``` --- ## Task 4: `create_order` — Validate + CPI buyer_create_escrow This is the buyer's single transaction: it validates the order, reserves quantity on the listing, CPIs `descro.buyer_create_escrow` to atomically create and fund the escrow, and records the order. **File:** `programs/solisting/src/instructions/create_order.rs` - [ ] **Step 1: Write failing test** (in `tests/test_orders.rs`) ```rust #[test] fn buyer_can_create_order_descro() { // setup listing with DescroPaymentOption (any resolver) // buyer calls create_order with resolver keypair // Assert: OrderAccount exists with correct fields // Assert: descro EscrowAccount exists, state == AwaitingSellerConfirm // Assert: descro vault has listing.price lamports // Assert: listing.quantity_reserved == 1 } #[test] fn create_order_fails_if_listing_inactive() { } #[test] fn create_order_fails_if_out_of_stock() { } #[test] fn create_order_fails_if_resolver_not_accepted() { // listing has specific accepted_resolvers = [resolver_a] // buyer tries with resolver_b → must fail } #[test] fn create_order_fails_if_descro_not_supported() { // listing has descro_option = None // buyer tries to create descro order → must fail } ``` - [ ] **Step 2: Implement `create_order.rs`** ```rust use anchor_lang::prelude::*; use crate::state::{ListingAccount, OrderAccount}; use crate::error::SolistingError; #[derive(Accounts)] #[instruction(order_id: u64, escrow_id: u64, resolver: Pubkey)] pub struct CreateOrder<'info> { #[account(mut)] pub buyer: Signer<'info>, /// CHECK: Seller — verified against listing 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.is_active @ SolistingError::ListingNotActive, constraint = listing_account.quantity > listing_account.quantity_reserved @ 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: Descro EscrowAccount PDA — created by the 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 — funded by the CPI #[account( mut, seeds = [b"vault", escrow_account.key().as_ref()], bump, seeds::program = descro::id(), )] pub descro_vault: UncheckedAccount<'info>, /// CHECK: Descro program — verified in handler pub descro_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } pub fn handler( ctx: Context, order_id: u64, escrow_id: u64, resolver: Pubkey, ) -> Result<()> { require!( ctx.accounts.descro_program.key() == descro::id(), SolistingError::InvalidDescroProgram ); let listing = &ctx.accounts.listing_account; // Validate descro payment option is supported let descro_opt = listing.descro_option.as_ref() .ok_or(SolistingError::DescroNotSupported)?; // Validate resolver is accepted (empty list = any resolver ok) if !descro_opt.accepted_resolvers.is_empty() { require!( descro_opt.accepted_resolvers.contains(&resolver), SolistingError::ResolverNotAccepted ); } let amount = listing.price; // CPI: descro.buyer_create_escrow — creates EscrowAccount + funds vault atomically descro::cpi::buyer_create_escrow( CpiContext::new( ctx.accounts.descro_program.to_account_info(), descro::cpi::accounts::BuyerCreateEscrow { buyer: ctx.accounts.buyer.to_account_info(), seller: ctx.accounts.seller.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(resolver), escrow_id, )?; // Reserve quantity ctx.accounts.listing_account.quantity_reserved += 1; // Record order let order = &mut ctx.accounts.order_account; order.listing = ctx.accounts.listing_account.key(); order.buyer = ctx.accounts.buyer.key(); order.seller = ctx.accounts.seller.key(); order.resolver = resolver; order.amount = amount; order.escrow_account = ctx.accounts.escrow_account.key(); order.escrow_id = escrow_id; order.order_id = order_id; order.created_at = Clock::get()?.unix_timestamp; order.bump = ctx.bumps.order_account; Ok(()) } ``` --- ## Task 5: `accept_order` — CPI seller_confirm The seller accepts: CPIs `descro.seller_confirm` to move the escrow to `Active`, then closes the `OrderAccount`. **File:** `programs/solisting/src/instructions/accept_order.rs` - [ ] **Step 1: Write failing test** ```rust #[test] fn seller_accept_creates_active_descro_escrow() { // create_order → AwaitingSellerConfirm // seller calls accept_order // Assert: descro escrow state == Active // Assert: OrderAccount is closed // Assert: listing.quantity_reserved == 0 // Assert: listing.quantity == original - 1 } ``` - [ ] **Step 2: Implement `accept_order.rs`** ```rust use anchor_lang::prelude::*; use crate::state::{ListingAccount, OrderAccount}; use crate::error::SolistingError; #[derive(Accounts)] pub struct AcceptOrder<'info> { #[account(mut)] pub seller: Signer<'info>, /// CHECK: Resolver — may need to co-sign if AcceptancePolicy is SignatureGated (descro enforces this) 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, )] 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 = seller.key() == order_account.seller @ SolistingError::Unauthorized, close = seller, )] pub order_account: Account<'info, OrderAccount>, /// CHECK: Descro EscrowAccount — verified via seeds, state checked by descro.seller_confirm #[account( mut, seeds = [b"escrow", seller.key().as_ref(), &order_account.escrow_id.to_le_bytes()], bump, seeds::program = descro::id(), constraint = escrow_account.key() == order_account.escrow_account @ SolistingError::EscrowStateUnexpected, )] pub escrow_account: UncheckedAccount<'info>, /// CHECK: Optional resolver registry entry — passed through to descro.seller_confirm for AcceptancePolicy check pub resolver_entry: UncheckedAccount<'info>, /// CHECK: Descro program pub descro_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context) -> Result<()> { require!( ctx.accounts.descro_program.key() == descro::id(), SolistingError::InvalidDescroProgram ); // CPI: descro.seller_confirm — descro checks AcceptancePolicy internally descro::cpi::seller_confirm( CpiContext::new( ctx.accounts.descro_program.to_account_info(), descro::cpi::accounts::SellerConfirm { seller: ctx.accounts.seller.to_account_info(), resolver: ctx.accounts.resolver.to_account_info(), escrow_account: ctx.accounts.escrow_account.to_account_info(), resolver_entry: ctx.accounts.resolver_entry.to_account_info(), }, ), )?; // Commit quantity: reserved → sold let listing = &mut ctx.accounts.listing_account; listing.quantity_reserved = listing.quantity_reserved.saturating_sub(1); listing.quantity = listing.quantity.saturating_sub(1); Ok(()) } ``` --- ## Task 6: `reject_order` and `cancel_order` — Defensive CPI to descro.cancel Both instructions read the descro escrow state before attempting the cancel CPI. If descro already resolved the escrow (buyer cancelled directly), the CPI is skipped and the `OrderAccount` is still closed. This prevents stuck accounts and failed transactions. **Files:** `reject_order.rs`, `cancel_order.rs` - [ ] **Step 1: Write failing tests** ```rust #[test] fn seller_can_reject_order() { // create_order → AwaitingSellerConfirm // seller calls reject_order // Assert: OrderAccount closed // Assert: buyer received SOL back (descro vault drained) // Assert: listing.quantity_reserved == 0 } #[test] fn buyer_can_cancel_order_anytime() { // create_order → AwaitingSellerConfirm // buyer calls cancel_order immediately (no timeout) // Assert: same as reject — funds returned, order closed } #[test] fn reject_handles_already_cancelled_escrow() { // buyer calls descro.cancel directly (bypassing solisting) // seller calls solisting.reject_order // Assert: no panic, OrderAccount is still closed cleanly } #[test] fn cancel_handles_already_cancelled_escrow() { // buyer calls descro.cancel directly // buyer calls solisting.cancel_order // Assert: OrderAccount closed, no CPI error } ``` - [ ] **Step 2: Implement `reject_order.rs`** ```rust use anchor_lang::prelude::*; use crate::state::{ListingAccount, OrderAccount}; use crate::error::SolistingError; #[derive(Accounts)] pub struct RejectOrder<'info> { #[account(mut)] pub seller: Signer<'info>, /// CHECK: Buyer receives the vault refund from descro.cancel #[account(mut)] pub buyer: UncheckedAccount<'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, )] 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 = seller.key() == order_account.seller @ SolistingError::Unauthorized, constraint = buyer.key() == order_account.buyer @ SolistingError::Unauthorized, close = seller, )] pub order_account: Account<'info, OrderAccount>, /// CHECK: Descro EscrowAccount — read to check current state #[account( mut, seeds = [b"escrow", seller.key().as_ref(), &order_account.escrow_id.to_le_bytes()], bump, seeds::program = descro::id(), )] pub escrow_account: UncheckedAccount<'info>, /// CHECK: Descro vault — drained to buyer by descro.cancel #[account( mut, seeds = [b"vault", escrow_account.key().as_ref()], bump, seeds::program = descro::id(), )] pub vault: UncheckedAccount<'info>, /// CHECK: Descro program pub descro_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context) -> Result<()> { require!( ctx.accounts.descro_program.key() == descro::id(), SolistingError::InvalidDescroProgram ); // Defensive: only CPI if escrow is still in AwaitingSellerConfirm // If buyer already cancelled directly, skip CPI — just close the order if !ctx.accounts.escrow_account.data_is_empty() { let data = ctx.accounts.escrow_account.try_borrow_data()?; let escrow = descro::EscrowAccount::try_deserialize(&mut data.as_ref()); if let Ok(e) = escrow { if e.state == descro::EscrowState::AwaitingSellerConfirm { drop(data); descro::cpi::cancel( CpiContext::new( ctx.accounts.descro_program.to_account_info(), descro::cpi::accounts::Cancel { canceller: 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.vault.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), }, ), )?; } } } // Restore reserved quantity ctx.accounts.listing_account.quantity_reserved = ctx.accounts.listing_account.quantity_reserved.saturating_sub(1); Ok(()) } ``` - [ ] **Step 3: Implement `cancel_order.rs`** Identical to `reject_order.rs` except: - `buyer: Signer<'info>` instead of `seller: Signer<'info>` - Constraint checks buyer == order.buyer - CPI `canceller` = buyer - `close = buyer` - No timeout — buyer can cancel anytime pre-Active ```rust use anchor_lang::prelude::*; use crate::state::{ListingAccount, OrderAccount}; use crate::error::SolistingError; #[derive(Accounts)] pub struct CancelOrder<'info> { #[account(mut)] pub buyer: Signer<'info>, #[account( mut, seeds = [b"listing", listing_account.seller.as_ref(), &listing_account.listing_id.to_le_bytes()], bump = listing_account.bump, )] 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, close = buyer, )] pub order_account: Account<'info, OrderAccount>, /// CHECK: Descro EscrowAccount — read defensively #[account( mut, seeds = [b"escrow", order_account.seller.as_ref(), &order_account.escrow_id.to_le_bytes()], bump, seeds::program = descro::id(), )] pub escrow_account: UncheckedAccount<'info>, /// CHECK: Descro vault #[account( mut, seeds = [b"vault", escrow_account.key().as_ref()], bump, seeds::program = descro::id(), )] pub vault: UncheckedAccount<'info>, /// CHECK: Descro program pub descro_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } pub fn handler(ctx: Context) -> Result<()> { require!( ctx.accounts.descro_program.key() == descro::id(), SolistingError::InvalidDescroProgram ); if !ctx.accounts.escrow_account.data_is_empty() { let data = ctx.accounts.escrow_account.try_borrow_data()?; let escrow = descro::EscrowAccount::try_deserialize(&mut data.as_ref()); if let Ok(e) = escrow { if e.state == descro::EscrowState::AwaitingSellerConfirm { drop(data); descro::cpi::cancel( CpiContext::new( ctx.accounts.descro_program.to_account_info(), descro::cpi::accounts::Cancel { canceller: ctx.accounts.buyer.to_account_info(), buyer: ctx.accounts.buyer.to_account_info(), escrow_account: ctx.accounts.escrow_account.to_account_info(), vault: ctx.accounts.vault.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), }, ), )?; } } } ctx.accounts.listing_account.quantity_reserved = ctx.accounts.listing_account.quantity_reserved.saturating_sub(1); Ok(()) } ``` --- ## Task 7: `close_stale_order` — Cleanup Orphaned Orders Anyone can call this to close an `OrderAccount` whose descro escrow is in a terminal state (`Cancelled` or `Complete`). Rent goes to the caller as incentive. **File:** `programs/solisting/src/instructions/close_stale_order.rs` ```rust use anchor_lang::prelude::*; use crate::state::OrderAccount; use crate::error::SolistingError; #[derive(Accounts)] pub struct CloseStaleOrder<'info> { #[account(mut)] pub caller: 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, close = caller, )] pub order_account: Account<'info, OrderAccount>, /// CHECK: Descro EscrowAccount — must be in terminal state pub escrow_account: UncheckedAccount<'info>, } pub fn handler(ctx: Context) -> Result<()> { require!( ctx.accounts.escrow_account.key() == ctx.accounts.order_account.escrow_account, SolistingError::EscrowStateUnexpected ); // Only allow close if escrow is terminal or gone (already closed) if !ctx.accounts.escrow_account.data_is_empty() { let data = ctx.accounts.escrow_account.try_borrow_data()?; if let Ok(escrow) = descro::EscrowAccount::try_deserialize(&mut data.as_ref()) { require!( escrow.state == descro::EscrowState::Cancelled || escrow.state == descro::EscrowState::Complete, SolistingError::EscrowStateUnexpected ); } } Ok(()) } ``` --- ## Task 8: Wire Up `lib.rs` and `instructions.rs`, Build, Test - [ ] **Step 1: Write `instructions.rs`** ```rust #![allow(ambiguous_glob_reexports)] pub mod accept_order; pub mod cancel_order; pub mod close_listing; pub mod close_stale_order; 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 close_stale_order::*; pub use create_listing::*; pub use create_order::*; pub use reject_order::*; pub use update_listing::*; ``` - [ ] **Step 2: Write `lib.rs`** ```rust 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 after anchor keys list #[program] pub mod solisting { use super::*; pub fn create_listing( ctx: Context, listing_id: u64, price: u64, quantity: u32, descro_option: Option, metadata_uri: String, ) -> Result<()> { create_listing::handler(ctx, listing_id, price, quantity, descro_option, metadata_uri) } pub fn update_listing( ctx: Context, price: u64, quantity: u32, descro_option: Option, metadata_uri: String, ) -> Result<()> { update_listing::handler(ctx, price, quantity, descro_option, 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, resolver: Pubkey, ) -> Result<()> { create_order::handler(ctx, order_id, escrow_id, resolver) } 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) } pub fn close_stale_order(ctx: Context) -> Result<()> { close_stale_order::handler(ctx) } } ``` - [ ] **Step 3: Build 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 ``` - [ ] **Step 4: Run full test suite** ```bash 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 5: Final commit** ```bash git add programs/solisting/ Cargo.toml Cargo.lock git commit -m "feat(solisting): implement listing + order flow with buyer-initiated descro escrow" ``` --- ## Done Full buyer-to-escrow flow across three programs with clean separation: - `solisting` = coordination and discovery only; never holds escrow SOL - `descro` = sole custodian of vault; enforces AcceptancePolicy - `descro_ext_resolvers` = resolver registry; read by descro at seller_confirm time Remaining items for future phases: - USDC/SPL token payment option in `ListingAccount` - `ProgramGated` resolver CPI in `descro.seller_confirm` - Event emission (`emit!()`) for off-chain indexers - Guard against closing listing while pending orders exist (`quantity_reserved > 0`)