impl solisting

This commit is contained in:
thesn10
2026-06-14 15:57:01 +02:00
parent ee1e6bdb06
commit cfc75a05b3
16 changed files with 1728 additions and 59 deletions

View File

@@ -22,15 +22,14 @@ custom-panic = []
anchor-lang = "1.0.2"
descro = { path = "../../../descro/programs/descro", features = ["cpi"] }
descro_ext_resolvers = { path = "../../../descro/programs/descro_ext_resolvers" }
# pyth-solana-receiver-sdk is not used directly — oracle.rs reads PriceUpdateV2 account data
# manually (borsh layout matching the SDK) to avoid SDK compilation issues with this Rust toolchain.
pyth-solana-receiver-sdk = "1.2.0"
[dev-dependencies]
litesvm = "0.12.0"
solana-message = "3.0.1"
solana-transaction = "3.0.2"
solana-signer = "3.0.0"
solana-keypair = "3.0.1"
solana-message = "4.2.1"
solana-transaction = "4.1.3"
solana-signer = "3.0.1"
solana-keypair = "3.1.2"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }

View File

@@ -1,3 +1,21 @@
pub mod initialize;
#![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 initialize;
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 initialize::*;
pub use reject_order::*;
pub use update_listing::*;

View File

@@ -0,0 +1,72 @@
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: 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,
close = seller,
)]
pub order_account: Account<'info, OrderAccount>,
/// CHECK: Descro EscrowAccount — 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
pub resolver_entry: UncheckedAccount<'info>,
/// CHECK: Descro program
pub descro_program: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
pub fn handler(ctx: Context<AcceptOrder>) -> Result<()> {
require!(
ctx.accounts.descro_program.key() == descro::id(),
SolistingError::InvalidDescroProgram
);
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(),
},
),
)?;
let listing = &mut ctx.accounts.listing_account;
listing.quantity_reserved = listing.quantity_reserved.saturating_sub(1);
listing.quantity = listing.quantity.saturating_sub(1);
Ok(())
}

View File

@@ -0,0 +1,81 @@
use anchor_lang::prelude::*;
use anchor_lang::AccountDeserialize;
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 before CPI
#[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<CancelOrder>) -> 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()?;
if let Ok(escrow) = descro::EscrowAccount::try_deserialize(&mut data.as_ref()) {
if escrow.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(())
}

View File

@@ -0,0 +1,22 @@
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<CloseListing>) -> Result<()> {
Ok(())
}

View File

@@ -0,0 +1,42 @@
use anchor_lang::prelude::*;
use anchor_lang::AccountDeserialize;
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 or closed
pub escrow_account: UncheckedAccount<'info>,
}
pub fn handler(ctx: Context<CloseStaleOrder>) -> Result<()> {
require!(
ctx.accounts.escrow_account.key() == ctx.accounts.order_account.escrow_account,
SolistingError::EscrowStateUnexpected
);
// Closed (zero data) escrow = already resolved = ok to clean up
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(())
}

View File

@@ -0,0 +1,47 @@
use anchor_lang::prelude::*;
use crate::state::{AltCurrencyConfig, Currency, 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<CreateListing>,
listing_id: u64,
canonical_currency: Currency,
price: u64,
canonical_oracle: Option<Pubkey>,
alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>,
quantity: u32,
metadata_uri: String,
) -> Result<()> {
let listing = &mut ctx.accounts.listing_account;
listing.seller = ctx.accounts.seller.key();
listing.canonical_currency = canonical_currency;
listing.price = price;
listing.canonical_oracle = canonical_oracle;
listing.alt_currencies = alt_currencies;
listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity;
listing.quantity_reserved = 0;
listing.metadata_uri = metadata_uri;
listing.listing_id = listing_id;
listing.is_active = true;
listing.bump = ctx.bumps.listing_account;
Ok(())
}

View File

@@ -0,0 +1,165 @@
use anchor_lang::prelude::*;
use crate::state::{Currency, ListingAccount, OrderAccount};
use crate::error::SolistingError;
use crate::oracle;
#[derive(Accounts)]
#[instruction(order_id: u64, escrow_id: u64, resolver: Pubkey, payment_currency: Currency)]
pub struct CreateOrder<'info> {
#[account(mut)]
pub buyer: Signer<'info>,
/// CHECK: Seller — verified against listing.seller via constraint
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: Pyth USD feed for the canonical currency (listing.canonical_oracle).
/// Pass SystemProgram as placeholder when canonical_oracle is None (stablecoin).
pub canonical_oracle: UncheckedAccount<'info>,
/// CHECK: Pyth USD feed for the chosen alt currency (alt_cfg.usd_oracle).
/// Pass SystemProgram as placeholder when usd_oracle is None or paying canonical.
pub target_oracle: UncheckedAccount<'info>,
/// CHECK: Descro program — verified in handler
pub descro_program: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
pub fn handler(
ctx: Context<CreateOrder>,
order_id: u64,
escrow_id: u64,
resolver: Pubkey,
payment_currency: Currency,
expected_amount: u64,
max_slippage_bps: u16,
) -> Result<()> {
require!(
ctx.accounts.descro_program.key() == descro::id(),
SolistingError::InvalidDescroProgram
);
let listing = &ctx.accounts.listing_account;
if !listing.accepted_resolvers.is_empty() {
require!(
listing.accepted_resolvers.contains(&resolver),
SolistingError::ResolverNotAccepted
);
}
let amount = if payment_currency == listing.canonical_currency {
listing.price
} else {
let alt_cfg = listing
.alt_currencies
.iter()
.find(|c| c.currency == payment_currency)
.ok_or(error!(SolistingError::CurrencyNotAccepted))?;
if let Some(expected_canonical) = listing.canonical_oracle {
require!(
ctx.accounts.canonical_oracle.key() == expected_canonical,
SolistingError::OracleMismatch
);
}
if let Some(expected_target) = alt_cfg.usd_oracle {
require!(
ctx.accounts.target_oracle.key() == expected_target,
SolistingError::OracleMismatch
);
}
let c_oracle = listing
.canonical_oracle
.map(|_| ctx.accounts.canonical_oracle.as_ref());
let t_oracle = alt_cfg
.usd_oracle
.map(|_| ctx.accounts.target_oracle.as_ref());
oracle::convert_with_slippage(
c_oracle,
&listing.canonical_currency,
listing.price,
t_oracle,
&payment_currency,
expected_amount,
max_slippage_bps,
)?
};
require!(
payment_currency == Currency::Sol,
SolistingError::SplNotImplemented
);
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,
)?;
ctx.accounts.listing_account.quantity_reserved += 1;
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.payment_currency = payment_currency;
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(())
}

View File

@@ -0,0 +1,88 @@
use anchor_lang::prelude::*;
use anchor_lang::AccountDeserialize;
use crate::state::{ListingAccount, OrderAccount};
use crate::error::SolistingError;
#[derive(Accounts)]
pub struct RejectOrder<'info> {
#[account(mut)]
pub seller: Signer<'info>,
/// CHECK: Buyer receives 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 before CPI
#[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<RejectOrder>) -> Result<()> {
require!(
ctx.accounts.descro_program.key() == descro::id(),
SolistingError::InvalidDescroProgram
);
// Defensive: skip CPI if escrow already resolved (buyer cancelled directly on descro)
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()) {
if escrow.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(),
},
),
)?;
}
}
}
ctx.accounts.listing_account.quantity_reserved =
ctx.accounts.listing_account.quantity_reserved.saturating_sub(1);
Ok(())
}

View File

@@ -0,0 +1,38 @@
use anchor_lang::prelude::*;
use crate::state::{AltCurrencyConfig, Currency, 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<UpdateListing>,
canonical_currency: Currency,
price: u64,
canonical_oracle: Option<Pubkey>,
alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>,
quantity: u32,
metadata_uri: String,
) -> Result<()> {
let listing = &mut ctx.accounts.listing_account;
listing.canonical_currency = canonical_currency;
listing.price = price;
listing.canonical_oracle = canonical_oracle;
listing.alt_currencies = alt_currencies;
listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity;
listing.metadata_uri = metadata_uri;
Ok(())
}

View File

@@ -5,8 +5,7 @@ pub mod oracle;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use error::*;
pub use instructions::*;
pub use state::*;
@@ -19,4 +18,69 @@ pub mod solisting {
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
initialize::handler(ctx)
}
pub fn create_listing(
ctx: Context<CreateListing>,
listing_id: u64,
canonical_currency: state::Currency,
price: u64,
canonical_oracle: Option<Pubkey>,
alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>,
quantity: u32,
metadata_uri: String,
) -> Result<()> {
create_listing::handler(
ctx, listing_id, canonical_currency, price,
canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
)
}
pub fn update_listing(
ctx: Context<UpdateListing>,
canonical_currency: state::Currency,
price: u64,
canonical_oracle: Option<Pubkey>,
alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>,
quantity: u32,
metadata_uri: String,
) -> Result<()> {
update_listing::handler(
ctx, canonical_currency, price,
canonical_oracle, alt_currencies, accepted_resolvers, 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,
resolver: Pubkey,
payment_currency: state::Currency,
expected_amount: u64,
max_slippage_bps: u16,
) -> Result<()> {
create_order::handler(ctx, order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps)
}
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)
}
pub fn close_stale_order(ctx: Context<CloseStaleOrder>) -> Result<()> {
close_stale_order::handler(ctx)
}
}

View File

@@ -1,47 +1,17 @@
use anchor_lang::prelude::*;
use anchor_lang::AnchorDeserialize;
use anchor_lang::AccountDeserialize;
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
use crate::error::SolistingError;
use crate::state::Currency;
const ORACLE_MAX_AGE_SECS: i64 = 60;
/// Mirrors pyth_solana_receiver_sdk::price_update::VerificationLevel (borsh layout).
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
enum VerificationLevel {
Partial { num_signatures: u8 },
Full,
}
/// Mirrors pythnet_sdk::messages::PriceFeedMessage (borsh layout).
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
struct PriceFeedMessage {
pub feed_id: [u8; 32],
pub price: i64,
pub conf: u64,
pub exponent: i32,
pub publish_time: i64,
pub prev_publish_time: i64,
pub ema_price: i64,
pub ema_conf: u64,
}
/// Mirrors pyth_solana_receiver_sdk::price_update::PriceUpdateV2 (borsh layout after discriminator).
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
struct PriceUpdateV2Local {
pub write_authority: Pubkey,
pub verification_level: VerificationLevel,
pub price_message: PriceFeedMessage,
pub posted_slot: u64,
}
/// Reads a Pyth V2 PriceFeedAccount and returns (price_i64, exponent_i32).
/// `price * 10^exponent` is the USD value of 1 whole token.
/// The caller must verify the account key matches listing state before calling this.
fn read_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
let data = oracle_account.try_borrow_data()?;
require!(data.len() > 8, SolistingError::OraclePriceUnavailable);
// Skip the 8-byte Anchor discriminator, then borsh-decode the rest.
let price_update = PriceUpdateV2Local::try_from_slice(&data[8..])
let price_update = PriceUpdateV2::try_deserialize(&mut data.as_ref())
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?;
let msg = &price_update.price_message;
let now = Clock::get()?.unix_timestamp;

View File

@@ -0,0 +1,371 @@
#![allow(dead_code, unused_imports)]
use anchor_lang::{
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
};
use litesvm::LiteSVM;
use solana_keypair::Keypair;
use solana_message::{Message, VersionedMessage};
use solana_signer::Signer;
use solana_transaction::versioned::VersionedTransaction;
pub use anchor_lang::prelude::Pubkey;
pub use solisting::state::{AltCurrencyConfig, Currency, ListingAccount, OrderAccount};
pub fn setup() -> (LiteSVM, Keypair, Keypair) {
let mut svm = LiteSVM::new();
let solisting_bytes = include_bytes!("../../../../target/deploy/solisting.so");
svm.add_program(solisting::id(), solisting_bytes).unwrap();
let descro_bytes = include_bytes!("../../../../../descro/target/deploy/descro.so");
svm.add_program(descro::id(), descro_bytes).unwrap();
let seller = Keypair::new();
let buyer = Keypair::new();
svm.airdrop(&seller.pubkey(), 10_000_000_000).unwrap();
svm.airdrop(&buyer.pubkey(), 10_000_000_000).unwrap();
(svm, seller, buyer)
}
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 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 vault_pda(escrow: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[b"vault", escrow.as_ref()], &descro::id()).0
}
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 account 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 account not found");
OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
}
pub fn send(svm: &mut LiteSVM, ixs: &[Instruction], signers: &[&Keypair]) {
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(ixs, Some(&signers[0].pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers).unwrap();
svm.send_transaction(tx).expect("transaction failed");
}
pub fn try_send(
svm: &mut LiteSVM,
ixs: &[Instruction],
signers: &[&Keypair],
) -> Result<(), Box<dyn std::error::Error>> {
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(ixs, Some(&signers[0].pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
svm.send_transaction(tx)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
// --- Listing instruction builders ---
pub fn ix_create_listing(
seller: &Pubkey,
listing_id: u64,
canonical_currency: Currency,
price: u64,
canonical_oracle: Option<Pubkey>,
alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>,
quantity: u32,
metadata_uri: String,
) -> Instruction {
let listing_account = listing_pda(seller, listing_id);
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::CreateListing {
listing_id,
canonical_currency,
price,
canonical_oracle,
alt_currencies,
accepted_resolvers,
quantity,
metadata_uri,
}
.data(),
solisting::accounts::CreateListing {
seller: *seller,
listing_account,
system_program: system_program::ID,
}
.to_account_metas(None),
)
}
pub fn ix_update_listing(
seller: &Pubkey,
listing_id: u64,
canonical_currency: Currency,
price: u64,
canonical_oracle: Option<Pubkey>,
alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>,
quantity: u32,
metadata_uri: String,
) -> Instruction {
let listing_account = listing_pda(seller, listing_id);
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::UpdateListing {
canonical_currency,
price,
canonical_oracle,
alt_currencies,
accepted_resolvers,
quantity,
metadata_uri,
}
.data(),
solisting::accounts::UpdateListing {
seller: *seller,
listing_account,
}
.to_account_metas(None),
)
}
pub fn ix_close_listing(seller: &Pubkey, listing_id: u64) -> Instruction {
let listing_account = listing_pda(seller, listing_id);
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::CloseListing {}.data(),
solisting::accounts::CloseListing {
seller: *seller,
listing_account,
}
.to_account_metas(None),
)
}
// --- Order instruction builders ---
/// Build a create_order instruction paying in the canonical currency (no oracle accounts needed).
/// For alt-currency orders, use ix_create_order_with_resolver with explicit oracle keys.
pub fn ix_create_order(
buyer: &Pubkey,
seller: &Pubkey,
listing_id: u64,
order_id: u64,
payment_currency: Currency,
max_slippage_bps: u16,
canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>,
) -> Instruction {
let resolver = Pubkey::new_unique();
ix_create_order_with_resolver(
buyer,
seller,
listing_id,
order_id,
payment_currency,
max_slippage_bps,
canonical_oracle,
target_oracle,
resolver,
)
}
pub fn ix_create_order_with_resolver(
buyer: &Pubkey,
seller: &Pubkey,
listing_id: u64,
order_id: u64,
payment_currency: Currency,
max_slippage_bps: u16,
canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>,
resolver: Pubkey,
) -> Instruction {
let listing = listing_pda(seller, listing_id);
let order = order_pda(listing, *buyer, order_id);
// escrow_id is derived from the first 8 bytes of the order PDA to guarantee uniqueness
let escrow_id = u64::from_le_bytes(order.to_bytes()[0..8].try_into().unwrap());
let escrow_account = escrow_pda(seller, escrow_id);
let descro_vault = vault_pda(&escrow_account);
let canonical_oracle_key = canonical_oracle.unwrap_or(system_program::ID);
let target_oracle_key = target_oracle.unwrap_or(system_program::ID);
// expected_amount = 0 disables slippage check (tests compute it externally or don't need it)
let expected_amount: u64 = 0;
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::CreateOrder {
order_id,
escrow_id,
resolver,
payment_currency,
expected_amount,
max_slippage_bps,
}
.data(),
solisting::accounts::CreateOrder {
buyer: *buyer,
seller: *seller,
listing_account: listing,
order_account: order,
escrow_account,
descro_vault,
canonical_oracle: canonical_oracle_key,
target_oracle: target_oracle_key,
descro_program: descro::id(),
system_program: system_program::ID,
}
.to_account_metas(None),
)
}
pub fn ix_accept_order(
seller: &Pubkey,
listing_id: u64,
buyer: &Pubkey,
order_id: u64,
escrow_id: u64,
resolver: Pubkey,
) -> Instruction {
let listing_account = listing_pda(seller, listing_id);
let order_account = order_pda(listing_account, *buyer, order_id);
let escrow_account = escrow_pda(seller, escrow_id);
// Pass a fresh key as resolver_entry — it won't exist, so descro skips the policy check
let resolver_entry = Pubkey::new_unique();
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::AcceptOrder {}.data(),
solisting::accounts::AcceptOrder {
seller: *seller,
resolver,
listing_account,
order_account,
escrow_account,
resolver_entry,
descro_program: descro::id(),
system_program: system_program::ID,
}
.to_account_metas(None),
)
}
pub fn ix_reject_order(
seller: &Pubkey,
listing_id: u64,
buyer: &Pubkey,
order_id: u64,
escrow_id: u64,
) -> Instruction {
let listing_account = listing_pda(seller, listing_id);
let order_account = order_pda(listing_account, *buyer, order_id);
let escrow_account = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow_account);
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::RejectOrder {}.data(),
solisting::accounts::RejectOrder {
seller: *seller,
buyer: *buyer,
listing_account,
order_account,
escrow_account,
vault,
descro_program: descro::id(),
system_program: system_program::ID,
}
.to_account_metas(None),
)
}
pub fn ix_cancel_order(
buyer: &Pubkey,
listing_id: u64,
seller: &Pubkey,
order_id: u64,
escrow_id: u64,
) -> Instruction {
let listing_account = listing_pda(seller, listing_id);
let order_account = order_pda(listing_account, *buyer, order_id);
let escrow_account = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow_account);
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::CancelOrder {}.data(),
solisting::accounts::CancelOrder {
buyer: *buyer,
listing_account,
order_account,
escrow_account,
vault,
descro_program: descro::id(),
system_program: system_program::ID,
}
.to_account_metas(None),
)
}
pub fn ix_close_stale_order(
caller: &Pubkey,
listing: Pubkey,
buyer: Pubkey,
order_id: u64,
escrow_account: Pubkey,
) -> Instruction {
let order_account = order_pda(listing, buyer, order_id);
Instruction::new_with_bytes(
solisting::id(),
&solisting::instruction::CloseStaleOrder {}.data(),
solisting::accounts::CloseStaleOrder {
caller: *caller,
order_account,
escrow_account,
}
.to_account_metas(None),
)
}
/// Build a descro cancel instruction directly (bypasses solisting — used to test defensive paths)
pub fn ix_descro_cancel(buyer: &Pubkey, seller: &Pubkey, escrow_id: u64) -> Instruction {
let escrow_account = escrow_pda(seller, escrow_id);
let vault = vault_pda(&escrow_account);
Instruction::new_with_bytes(
descro::id(),
&descro::instruction::Cancel {}.data(),
descro::accounts::Cancel {
canceller: *buyer,
buyer: *buyer,
escrow_account,
vault,
system_program: system_program::ID,
}
.to_account_metas(None),
)
}

View File

@@ -0,0 +1,129 @@
mod common;
use common::*;
#[test]
fn seller_can_create_sol_only_listing() {
let (mut svm, seller, _) = setup();
let listing_id: u64 = 1;
let price = 100_000_000u64; // 0.1 SOL in lamports
let ix = ix_create_listing(
&seller.pubkey(),
listing_id,
Currency::Sol,
price,
None,
vec![],
vec![],
10,
"ipfs://test".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.canonical_currency, Currency::Sol);
assert_eq!(listing.price, price);
assert_eq!(listing.alt_currencies.len(), 0);
assert_eq!(listing.canonical_oracle, None);
assert_eq!(listing.quantity, 10);
assert_eq!(listing.quantity_reserved, 0);
assert!(listing.is_active);
}
#[test]
fn seller_can_create_listing_with_usdc_alt_stablecoin() {
let (mut svm, seller, _) = setup();
let usdc_mint = Pubkey::new_unique();
let sol_usd_feed = Pubkey::new_unique();
let ix = ix_create_listing(
&seller.pubkey(),
10,
Currency::Sol,
1_000_000_000,
Some(sol_usd_feed),
vec![AltCurrencyConfig {
currency: Currency::Spl { mint: usdc_mint, decimals: 6 },
usd_oracle: None,
}],
vec![],
5,
"ipfs://x".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let listing = read_listing(&svm, &seller.pubkey(), 10);
assert_eq!(listing.canonical_oracle, Some(sol_usd_feed));
assert_eq!(listing.alt_currencies[0].usd_oracle, None);
}
#[test]
fn seller_can_create_listing_with_bonk_alt_oracle() {
let (mut svm, seller, _) = setup();
let bonk_mint = Pubkey::new_unique();
let sol_usd_feed = Pubkey::new_unique();
let bonk_usd_feed = Pubkey::new_unique();
let ix = ix_create_listing(
&seller.pubkey(),
11,
Currency::Sol,
1_000_000_000,
Some(sol_usd_feed),
vec![AltCurrencyConfig {
currency: Currency::Spl { mint: bonk_mint, decimals: 5 },
usd_oracle: Some(bonk_usd_feed),
}],
vec![],
5,
"".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let listing = read_listing(&svm, &seller.pubkey(), 11);
assert_eq!(listing.canonical_oracle, Some(sol_usd_feed));
assert_eq!(listing.alt_currencies[0].usd_oracle, Some(bonk_usd_feed));
}
#[test]
fn seller_can_update_listing() {
let (mut svm, seller, _) = setup();
let listing_id: u64 = 2;
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
None, vec![], vec![], 10, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let ix_update = ix_update_listing(
&seller.pubkey(),
listing_id,
Currency::Sol,
2_000_000_000,
None,
vec![],
vec![],
20,
"ipfs://new".to_string(),
);
send(&mut svm, &[ix_update], &[&seller]);
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.price, 2_000_000_000);
assert_eq!(listing.quantity, 20);
}
#[test]
fn seller_can_close_listing() {
let (mut svm, seller, _) = setup();
let listing_id: u64 = 3;
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
None, vec![], vec![], 5, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
send(&mut svm, &[ix_close], &[&seller]);
let pda = listing_pda(&seller.pubkey(), listing_id);
assert!(svm.get_account(&pda).is_none());
}

View File

@@ -0,0 +1,307 @@
mod common;
use common::*;
use solana_keypair::Keypair;
use solana_signer::Signer;
#[test]
fn buyer_can_create_order_canonical_sol() {
let (mut svm, seller, buyer) = setup();
let listing_id = 1u64;
let price = 100_000_000u64;
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, price,
None, vec![], vec![], 5, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let order_id = 1u64;
let ix_order = ix_create_order(
&buyer.pubkey(),
&seller.pubkey(),
listing_id,
order_id,
Currency::Sol,
0,
None,
None,
);
send(&mut svm, &[ix_order], &[&buyer]);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), order_id);
assert_eq!(order.payment_currency, Currency::Sol);
assert_eq!(order.amount, price);
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.quantity_reserved, 1);
}
#[test]
fn create_order_fails_if_listing_inactive() {
let (mut svm, seller, buyer) = setup();
let listing_id = 2u64;
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000,
None, vec![], vec![], 1, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
send(&mut svm, &[ix_close], &[&seller]);
let result = try_send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
assert!(result.is_err());
}
#[test]
fn create_order_fails_if_out_of_stock() {
let (mut svm, seller, buyer) = setup();
let listing_id = 3u64;
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
None, vec![], vec![], 1, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
let buyer2 = Keypair::new();
svm.airdrop(&buyer2.pubkey(), 10_000_000_000).unwrap();
let result = try_send(
&mut svm,
&[ix_create_order(&buyer2.pubkey(), &seller.pubkey(), listing_id, 2, Currency::Sol, 0, None, None)],
&[&buyer2],
);
assert!(result.is_err());
}
#[test]
fn create_order_fails_if_currency_not_accepted() {
let (mut svm, seller, buyer) = setup();
let listing_id = 4u64;
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
None, vec![], vec![], 5, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let fake_mint = Pubkey::new_unique();
let result = try_send(
&mut svm,
&[ix_create_order(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1,
Currency::Spl { mint: fake_mint, decimals: 6 }, 0, None, None,
)],
&[&buyer],
);
assert!(result.is_err());
}
#[test]
fn create_order_fails_if_resolver_not_accepted() {
let (mut svm, seller, buyer) = setup();
let listing_id = 5u64;
let allowed_resolver = Pubkey::new_unique();
let ix = ix_create_listing(
&seller.pubkey(), listing_id, Currency::Sol, 100_000_000,
None, vec![], vec![allowed_resolver], 5, "".to_string(),
);
send(&mut svm, &[ix], &[&seller]);
let result = try_send(
&mut svm,
&[ix_create_order_with_resolver(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1,
Currency::Sol, 0, None, None, Pubkey::new_unique(),
)],
&[&buyer],
);
assert!(result.is_err());
}
#[test]
fn seller_accept_creates_active_descro_escrow() {
let (mut svm, seller, buyer) = setup();
let listing_id = 10u64;
let price = 100_000_000u64;
send(
&mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())],
&[&seller],
);
let order_id = 1u64;
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, order_id, Currency::Sol, 0, None, None)],
&[&buyer],
);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), order_id);
send(
&mut svm,
&[ix_accept_order(&seller.pubkey(), listing_id, &buyer.pubkey(), order_id, order.escrow_id, order.resolver)],
&[&seller],
);
let order_key = order_pda(listing_key, buyer.pubkey(), order_id);
assert!(svm.get_account(&order_key).is_none());
let listing = read_listing(&svm, &seller.pubkey(), listing_id);
assert_eq!(listing.quantity_reserved, 0);
assert_eq!(listing.quantity, 4);
}
#[test]
fn seller_can_reject_order() {
let (mut svm, seller, buyer) = setup();
let listing_id = 20u64;
let price = 100_000_000u64;
send(
&mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())],
&[&seller],
);
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
let buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
send(
&mut svm,
&[ix_reject_order(&seller.pubkey(), listing_id, &buyer.pubkey(), 1, order.escrow_id)],
&[&seller],
);
let buyer_after = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
assert!(buyer_after >= buyer_before + price - 10_000);
assert_eq!(read_listing(&svm, &seller.pubkey(), listing_id).quantity_reserved, 0);
assert!(svm.get_account(&order_pda(listing_key, buyer.pubkey(), 1)).is_none());
}
#[test]
fn buyer_can_cancel_order() {
let (mut svm, seller, buyer) = setup();
let listing_id = 21u64;
send(
&mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
&[&seller],
);
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
send(
&mut svm,
&[ix_cancel_order(&buyer.pubkey(), listing_id, &seller.pubkey(), 1, order.escrow_id)],
&[&buyer],
);
assert_eq!(read_listing(&svm, &seller.pubkey(), listing_id).quantity_reserved, 0);
}
#[test]
fn reject_handles_already_cancelled_escrow() {
let (mut svm, seller, buyer) = setup();
let listing_id = 22u64;
send(
&mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
&[&seller],
);
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
// Buyer cancels directly on descro (bypassing solisting)
send(&mut svm, &[ix_descro_cancel(&buyer.pubkey(), &seller.pubkey(), order.escrow_id)], &[&buyer]);
// Seller calls solisting reject — should succeed without CPI error
send(
&mut svm,
&[ix_reject_order(&seller.pubkey(), listing_id, &buyer.pubkey(), 1, order.escrow_id)],
&[&seller],
);
}
#[test]
fn anyone_can_close_stale_order_after_terminal_escrow() {
let (mut svm, seller, buyer) = setup();
let listing_id = 30u64;
send(
&mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
&[&seller],
);
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
// Buyer cancels on descro directly → escrow is Cancelled
send(&mut svm, &[ix_descro_cancel(&buyer.pubkey(), &seller.pubkey(), order.escrow_id)], &[&buyer]);
let janitor = Keypair::new();
svm.airdrop(&janitor.pubkey(), 1_000_000).unwrap();
let janitor_before = svm.get_account(&janitor.pubkey()).unwrap().lamports;
send(
&mut svm,
&[ix_close_stale_order(&janitor.pubkey(), listing_key, buyer.pubkey(), 1, order.escrow_account)],
&[&janitor],
);
assert!(svm.get_account(&order_pda(listing_key, buyer.pubkey(), 1)).is_none());
let janitor_after = svm.get_account(&janitor.pubkey()).unwrap().lamports;
assert!(janitor_after > janitor_before);
}
#[test]
fn close_stale_order_fails_if_escrow_still_active() {
let (mut svm, seller, buyer) = setup();
let listing_id = 31u64;
send(
&mut svm,
&[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, None, vec![], vec![], 5, "".to_string())],
&[&seller],
);
send(
&mut svm,
&[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)],
&[&buyer],
);
let listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
let janitor = Keypair::new();
svm.airdrop(&janitor.pubkey(), 1_000_000).unwrap();
let result = try_send(
&mut svm,
&[ix_close_stale_order(&janitor.pubkey(), listing_key, buyer.pubkey(), 1, order.escrow_account)],
&[&janitor],
);
assert!(result.is_err());
}