better oracle

This commit is contained in:
thesn10
2026-05-25 21:40:09 +02:00
parent 0b00030167
commit 9900ada281

View File

@@ -9,9 +9,10 @@
**Goal:** Build the `solisting` Anchor program — a coordination and discovery layer for product listings and bilateral order consent, orchestrating the full flow from buyer intent → bilateral consent → active `descro` escrow without ever touching the descro vault directly. **Goal:** Build the `solisting` Anchor program — a coordination and discovery layer for product listings and bilateral order consent, orchestrating the full flow from buyer intent → bilateral consent → active `descro` escrow without ever touching the descro vault directly.
**Architecture:** **Architecture:**
- `ListingAccount` holds a single canonical price (`canonical_currency` + `canonical_amount`) as the sole source of truth. Alternative currencies (`alt_currencies`) are always converted from canonical at `create_order` time via an on-chain Pyth oracle — no independent price per currency, no arbitrage possible. The oracle account is only required when `alt_currencies` is non-empty. - `ListingAccount` holds a single canonical price (`canonical_currency` + `canonical_amount`) as the sole source of truth. Alternative currencies (`alt_currencies: Vec<AltCurrencyConfig>`) are always converted from canonical at `create_order` time via on-chain Pyth oracles — no independent price per currency, no arbitrage possible.
- Each `AltCurrencyConfig` carries its own `usd_oracle: Option<Pubkey>` (the Pyth TOKEN/USD feed for that token; `None` = treat as a $1 USD stablecoin). The canonical currency has a parallel `canonical_oracle: Option<Pubkey>`. `Currency::Spl` stores `decimals: u8` so the program never needs to look up the mint.
- `OrderAccount` records the bilateral consent: which currency the buyer chose, the oracle-computed amount, and a pointer to the descro escrow. - `OrderAccount` records the bilateral consent: which currency the buyer chose, the oracle-computed amount, and a pointer to the descro escrow.
- Four seller archetypes: (1) SOL-only, (2) SOL-primary + USDT via oracle, (3) USDT-only, (4) USDT-primary + SOL via oracle. - The oracle module converts between any two currencies through USD as an intermediate (using up to two Pyth feeds): `canonical_usd = amount × canonical_price`, `target_amount = canonical_usd ÷ target_price`. Both sides can independently be a stablecoin (`None` oracle = $1 peg, no account needed).
- `accepted_resolvers` is a global list per listing — resolvers handle disputes, not payments, so one list covers all currencies. - `accepted_resolvers` is a global list per listing — resolvers handle disputes, not payments, so one list covers all currencies.
- `create_order` CPIs `descro.buyer_create_escrow` atomically. `accept_order` CPIs `descro.seller_confirm`. Solisting never holds or transfers vault funds. - `create_order` CPIs `descro.buyer_create_escrow` atomically. `accept_order` CPIs `descro.seller_confirm`. Solisting never holds or transfers vault funds.
- State drift (buyer cancels directly on descro) is handled gracefully: terminal instructions read descro state first and skip the CPI if the escrow is already resolved. - State drift (buyer cancels directly on descro) is handled gracefully: terminal instructions read descro state first and skip the CPI if the escrow is already resolved.
@@ -35,9 +36,9 @@
|---|---|---| |---|---|---|
| Create | `programs/solisting/Cargo.toml` | Crate definition + dependencies (incl. pyth-solana-receiver-sdk) | | Create | `programs/solisting/Cargo.toml` | Crate definition + dependencies (incl. pyth-solana-receiver-sdk) |
| Create | `programs/solisting/src/lib.rs` | Program entrypoints + declare_id! | | Create | `programs/solisting/src/lib.rs` | Program entrypoints + declare_id! |
| Create | `programs/solisting/src/state.rs` | `ListingAccount`, `OrderAccount`, `Currency` enum | | Create | `programs/solisting/src/state.rs` | `ListingAccount`, `OrderAccount`, `Currency` enum, `AltCurrencyConfig` |
| Create | `programs/solisting/src/error.rs` | `SolistingError` enum | | Create | `programs/solisting/src/error.rs` | `SolistingError` enum |
| Create | `programs/solisting/src/oracle.rs` | Pyth price read + SOL↔stablecoin conversion + slippage check | | Create | `programs/solisting/src/oracle.rs` | Generic Pyth feed reader + USD-intermediate cross-currency conversion + slippage check |
| Create | `programs/solisting/src/instructions.rs` | Module re-exports | | Create | `programs/solisting/src/instructions.rs` | Module re-exports |
| Create | `programs/solisting/src/instructions/create_listing.rs` | Init `ListingAccount` | | Create | `programs/solisting/src/instructions/create_listing.rs` | Init `ListingAccount` |
| Create | `programs/solisting/src/instructions/update_listing.rs` | Mutate price/quantity/currencies | | Create | `programs/solisting/src/instructions/update_listing.rs` | Mutate price/quantity/currencies |
@@ -140,11 +141,32 @@ git commit -m "chore(solisting): add crate skeleton and workspace entry"
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
/// Payment currency identifier. Amounts are always in the currency's smallest unit: /// Payment currency identifier. Amounts are always in the currency's smallest unit:
/// Sol → Lamports (u64), Spl → token units defined by the mint's decimals field. /// Sol → Lamports (u64), Spl → token units (smallest unit per `decimals`).
#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug, PartialEq)] #[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug, PartialEq)]
pub enum Currency { pub enum Currency {
Sol, Sol,
Spl { mint: Pubkey }, /// `decimals` mirrors the SPL mint's decimals field (e.g. 6 for USDC/USDT, 5 for BONK).
/// Storing it here avoids a mint account lookup at order time.
Spl { mint: Pubkey, decimals: u8 },
}
impl Currency {
pub fn decimals(&self) -> u8 {
match self {
Currency::Sol => 9,
Currency::Spl { decimals, .. } => *decimals,
}
}
}
/// Per-alt-currency oracle configuration. Each alt currency carries its own Pyth feed
/// because different tokens have different price feeds.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug)]
pub struct AltCurrencyConfig {
pub currency: Currency,
/// Pyth V2 PriceFeedAccount for this token priced in USD (TOKEN/USD).
/// `None` = treat as a $1.00 USD stablecoin — no oracle account required at order time.
pub usd_oracle: Option<Pubkey>,
} }
#[account] #[account]
@@ -155,13 +177,14 @@ pub struct ListingAccount {
pub canonical_currency: Currency, pub canonical_currency: Currency,
/// Price in canonical_currency's smallest unit. /// Price in canonical_currency's smallest unit.
pub canonical_amount: u64, pub canonical_amount: u64,
/// Other currencies the seller accepts. Amounts are always oracle-computed at create_order /// Pyth V2 PriceFeedAccount for the canonical currency priced in USD.
/// time — never stored independently. Empty = oracle never needed. /// `None` = canonical is a $1.00 USD stablecoin.
/// For Sol canonical: set to the SOL/USD Pyth feed.
pub canonical_oracle: Option<Pubkey>,
/// Alt currencies the seller accepts. Amounts are oracle-computed at create_order time.
/// Empty = no oracle ever needed.
#[max_len(3)] #[max_len(3)]
pub alt_currencies: Vec<Currency>, pub alt_currencies: Vec<AltCurrencyConfig>,
/// Pyth Price Feed account. Must be Some when alt_currencies is non-empty.
/// The seller chooses which oracle they trust.
pub price_oracle: Option<Pubkey>,
/// Resolvers the seller accepts for dispute resolution. Empty = any resolver ok. /// Resolvers the seller accepts for dispute resolution. Empty = any resolver ok.
#[max_len(4)] #[max_len(4)]
pub accepted_resolvers: Vec<Pubkey>, pub accepted_resolvers: Vec<Pubkey>,
@@ -214,9 +237,9 @@ pub enum SolistingError {
CurrencyNotAccepted, CurrencyNotAccepted,
#[msg("Resolver is not accepted by this listing")] #[msg("Resolver is not accepted by this listing")]
ResolverNotAccepted, ResolverNotAccepted,
#[msg("alt_currencies is non-empty but price_oracle is None on the listing")] #[msg("Oracle account is required but was not provided (non-stablecoin currency)")]
OracleRequired, OracleRequired,
#[msg("Oracle account key does not match listing.price_oracle")] #[msg("Passed oracle account key does not match the address stored in the listing")]
OracleMismatch, OracleMismatch,
#[msg("Oracle price is unavailable, stale, or has too wide a confidence interval")] #[msg("Oracle price is unavailable, stale, or has too wide a confidence interval")]
OraclePriceUnavailable, OraclePriceUnavailable,
@@ -233,109 +256,113 @@ pub enum SolistingError {
- [ ] **Step 3: Write `oracle.rs`** - [ ] **Step 3: Write `oracle.rs`**
The oracle module reads a Pyth SOL/USD price feed and converts between SOL (Lamports) and a The oracle module converts between any two currencies using USD as an intermediate. Each
stablecoin (6-decimal SPL token, e.g. USDT/USDC). Stablecoins are treated as exactly $1.00 — currency has an optional Pyth feed (TOKEN/USD). `None` = treat as $1.00 stablecoin. No feed ID
no second feed needed for the stable side. is hardcoded — the seller registers the oracle account address in the listing; the program only
verifies that the passed account key matches the stored key (done in `create_order`, not here).
Math (using USD as pivot):
```
canonical_usd = canonical_amount × (cp_raw × 10^cp_expo) / 10^cd
target_amount = canonical_usd × 10^td / (tp_raw × 10^tp_expo)
= canonical_amount × cp_raw / tp_raw × 10^(cp_expo tp_expo + td cd)
let shift = cp_expo tp_expo + td cd (can be positive or negative)
```
Stablecoin sentinel: `(1, 0)` meaning exactly $1.00 per whole token.
Example — SOL → BONK (cd=9, cp_expo=8, td=5, tp_expo=8):
`shift = 8 (8) + 5 9 = 4``target = ca × cp_raw / tp_raw / 10^4`
Example — SOL → USDC (cd=9, cp_expo=8, td=6, tp_raw=1, tp_expo=0 sentinel):
`shift = 8 0 + 6 9 = 11``target = ca × cp_raw / 10^11` ✓ (matches old formula)
```rust ```rust
use anchor_lang::prelude::*; use anchor_lang::prelude::*;
use anchor_lang::AccountDeserialize; use anchor_lang::AccountDeserialize;
use pyth_solana_receiver_sdk::price_update::{PriceUpdateV2, get_feed_id_from_hex}; use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
use crate::error::SolistingError; use crate::error::SolistingError;
use crate::state::Currency; use crate::state::Currency;
/// Maximum age of the oracle price in seconds before it is considered stale. const ORACLE_MAX_AGE_SECS: i64 = 60;
const ORACLE_MAX_AGE_SECS: u64 = 60;
/// Pyth SOL/USD price feed ID (mainnet + devnet). /// Reads any Pyth V2 PriceFeedAccount and returns (price_i64, exponent_i32) where
const SOL_USD_FEED_ID: &str = "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; /// `price * 10^exponent` is the USD value of 1 whole token (price in USD per whole unit).
/// The caller is responsible for verifying the account key matches listing state.
/// Reads the Pyth SOL/USD price feed and returns (price_i64, exponent_i32). fn read_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
/// price_i64 * 10^exponent = USD per SOL.
/// Returns OraclePriceUnavailable if the price is stale or the feed account is invalid.
fn sol_usd_price(oracle_account: &AccountInfo) -> Result<(i64, i32)> {
let data = oracle_account.try_borrow_data()?; let data = oracle_account.try_borrow_data()?;
let price_update = PriceUpdateV2::try_deserialize(&mut data.as_ref()) let price_update = PriceUpdateV2::try_deserialize(&mut data.as_ref())
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?; .map_err(|_| error!(SolistingError::OraclePriceUnavailable))?;
let clock = Clock::get()?; let msg = &price_update.price_message;
let feed_id = get_feed_id_from_hex(SOL_USD_FEED_ID) let now = Clock::get()?.unix_timestamp;
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?; require!(
let price = price_update now - msg.publish_time <= ORACLE_MAX_AGE_SECS,
.get_price_no_older_than(&clock, ORACLE_MAX_AGE_SECS, &feed_id) SolistingError::OraclePriceUnavailable
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?; );
Ok((price.price, price.exponent)) require!(msg.price > 0, SolistingError::OraclePriceUnavailable);
Ok((msg.price, msg.exponent))
} }
/// Converts `canonical_amount` (in `canonical_currency`) to `target_currency` units. /// Returns the USD price as (raw_i64, exponent_i32) for a currency.
/// Stablecoin sentinel (oracle = None) returns (1, 0) = exactly $1.00 per whole token.
fn usd_price(oracle: Option<&AccountInfo>) -> Result<(i64, i32)> {
match oracle {
None => Ok((1, 0)),
Some(account) => read_usd_price(account),
}
}
/// Converts `canonical_amount` (smallest units of `canonical_currency`) to the equivalent
/// amount in `target_currency` smallest units, using USD as an intermediate.
/// ///
/// Supported conversions: /// `canonical_oracle` / `target_oracle`: pass `None` when the currency is a USD stablecoin.
/// Sol → Spl (stablecoin 6 decimals): lamports → token_units /// `canonical_currency` / `target_currency`: used only for their `decimals()` value.
/// Spl (stablecoin 6 decimals) → Sol: token_units → lamports
/// ///
/// Formula Sol → Spl (assuming 1 stablecoin = $1, 6 decimals): /// Slippage: if |computed expected_amount| / expected_amount > max_slippage_bps / 10_000,
/// stablecoin_units = lamports * price * 10^expo / 10^9 * 10^6 /// returns `SlippageExceeded`. Pass `max_slippage_bps = 0` to skip the check.
/// = lamports * price * 10^(expo + 6 - 9)
/// = lamports * price * 10^(expo - 3)
/// With expo=-8: stablecoin_units = lamports * price / 10^11
///
/// Uses u128 intermediate to avoid overflow (max ~1.8e19 in u64 is too small).
///
/// Slippage check: if the oracle-computed amount differs from `expected_amount` by more than
/// `max_slippage_bps` basis points, returns SlippageExceeded.
pub fn convert_with_slippage( pub fn convert_with_slippage(
oracle_account: &AccountInfo, canonical_oracle: Option<&AccountInfo>,
canonical_currency: &Currency, canonical_currency: &Currency,
canonical_amount: u64, canonical_amount: u64,
target_oracle: Option<&AccountInfo>,
target_currency: &Currency, target_currency: &Currency,
expected_amount: u64, expected_amount: u64,
max_slippage_bps: u16, max_slippage_bps: u16,
) -> Result<u64> { ) -> Result<u64> {
let (price_raw, expo) = sol_usd_price(oracle_account)?; let (cp_raw, cp_expo) = usd_price(canonical_oracle)?;
require!(price_raw > 0, SolistingError::OraclePriceUnavailable); let (tp_raw, tp_expo) = usd_price(target_oracle)?;
let computed = match (canonical_currency, target_currency) { let cd = canonical_currency.decimals() as i32;
(Currency::Sol, Currency::Spl { .. }) => { let td = target_currency.decimals() as i32;
// lamports → stablecoin units (6 decimals, $1 peg)
// = lamports * price_raw / 10^(9 - 6 + (-expo)) // shift = cp_expo tp_expo + td cd
// = lamports * price_raw / 10^(3 - expo) [expo is negative, so 3 - expo > 3] let shift: i32 = cp_expo - tp_expo + td - cd;
let shift: u32 = (3i32 - expo) as u32; // expo=-8 → shift=11
let intermediate = (canonical_amount as u128) // target = canonical_amount × cp_raw / tp_raw × 10^shift
.checked_mul(price_raw as u128) // Use u128 to avoid overflow. tp_raw and cp_raw are always positive (checked above).
.ok_or(error!(SolistingError::OraclePriceUnavailable))?; let numerator = (canonical_amount as u128)
let divisor = 10u128.pow(shift); .checked_mul(cp_raw as u128)
u64::try_from(intermediate / divisor) .ok_or(error!(SolistingError::OraclePriceUnavailable))?;
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
} let computed: u64 = if shift >= 0 {
(Currency::Spl { .. }, Currency::Sol) => { let scaled = numerator
// stablecoin units (6 decimals, $1 peg) → lamports .checked_mul(10u128.pow(shift as u32))
// = token_units * 10^9 / (price_raw * 10^expo) .ok_or(error!(SolistingError::OraclePriceUnavailable))?;
// = token_units * 10^9 / price_raw * 10^(-expo) u64::try_from(scaled / tp_raw as u128)
// = token_units * 10^(9 + (-expo)) / price_raw .map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
// = token_units * 10^(9 - expo) / price_raw [expo negative, so 9 - expo > 9] } else {
// Example expo=-8: = token_units * 10^17 / price_raw let divisor = 10u128.pow((-shift) as u32);
// Then divide out the stablecoin decimals (6): u64::try_from(numerator / divisor / tp_raw as u128)
// = token_units * 10^(9 - expo - 6) / price_raw = token_units * 10^(3 - expo) / price_raw .map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
let shift: u32 = (3i32 - expo) as u32; // expo=-8 → shift=11
let numerator = (canonical_amount as u128)
.checked_mul(10u128.pow(shift))
.ok_or(error!(SolistingError::OraclePriceUnavailable))?;
u64::try_from(numerator / price_raw as u128)
.map_err(|_| error!(SolistingError::OraclePriceUnavailable))?
}
_ => return Err(error!(SolistingError::OraclePriceUnavailable)),
}; };
// Slippage: |computed - expected| / expected <= max_slippage_bps / 10_000 if max_slippage_bps > 0 && expected_amount > 0 {
if max_slippage_bps > 0 {
let tolerance = (expected_amount as u128) let tolerance = (expected_amount as u128)
.checked_mul(max_slippage_bps as u128) .checked_mul(max_slippage_bps as u128)
.unwrap_or(u128::MAX) .unwrap_or(u128::MAX)
/ 10_000; / 10_000;
let diff = if computed > expected_amount { let diff = computed.abs_diff(expected_amount) as u128;
computed as u128 - expected_amount as u128
} else {
expected_amount as u128 - computed as u128
};
require!(diff <= tolerance, SolistingError::SlippageExceeded); require!(diff <= tolerance, SolistingError::SlippageExceeded);
} }
@@ -381,8 +408,8 @@ fn seller_can_create_sol_only_listing() {
listing_id, listing_id,
Currency::Sol, Currency::Sol,
price, price,
None, // canonical_oracle: None (no alt currencies)
vec![], // no alt_currencies vec![], // no alt_currencies
None, // no oracle
vec![], // any resolver vec![], // any resolver
10, // quantity 10, // quantity
"ipfs://test".to_string(), "ipfs://test".to_string(),
@@ -393,37 +420,72 @@ fn seller_can_create_sol_only_listing() {
assert_eq!(listing.canonical_currency, Currency::Sol); assert_eq!(listing.canonical_currency, Currency::Sol);
assert_eq!(listing.canonical_amount, price); assert_eq!(listing.canonical_amount, price);
assert_eq!(listing.alt_currencies.len(), 0); assert_eq!(listing.alt_currencies.len(), 0);
assert_eq!(listing.price_oracle, None); assert_eq!(listing.canonical_oracle, None);
assert_eq!(listing.quantity, 10); assert_eq!(listing.quantity, 10);
assert_eq!(listing.quantity_reserved, 0); assert_eq!(listing.quantity_reserved, 0);
assert!(listing.is_active); assert!(listing.is_active);
} }
#[test] #[test]
fn create_listing_with_alt_currency_requires_oracle() { fn seller_can_create_listing_with_usdc_alt_stablecoin() {
// alt_currencies non-empty but price_oracle = None → should fail // canonical=Sol, alt=USDC with no oracle (stablecoin $1 peg)
let (mut svm, seller, _) = setup(); let (mut svm, seller, _) = setup();
let usdt_mint = Pubkey::new_unique(); let usdc_mint = Pubkey::new_unique();
let sol_usd_feed = Pubkey::new_unique(); // mock SOL/USD feed key
let ix = ix_create_listing( let ix = ix_create_listing(
&seller.pubkey(), &seller.pubkey(),
42, 10,
Currency::Sol, Currency::Sol,
1_000_000_000, 1_000_000_000,
vec![Currency::Spl { mint: usdt_mint }], Some(sol_usd_feed), // canonical_oracle = SOL/USD feed
None, // oracle missing — must fail vec![AltCurrencyConfig {
currency: Currency::Spl { mint: usdc_mint, decimals: 6 },
usd_oracle: None, // USDC is a stablecoin — no feed needed
}],
vec![], vec![],
5, 5,
"ipfs://x".to_string(), "ipfs://x".to_string(),
); );
let result = try_send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
assert!(result.is_err());
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() {
// canonical=Sol, alt=BONK — needs both SOL/USD and BONK/USD feeds
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] #[test]
fn seller_can_update_listing() { fn seller_can_update_listing() {
let (mut svm, seller, _) = setup(); let (mut svm, seller, _) = setup();
let listing_id: u64 = 2; let listing_id: u64 = 2;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, vec![], None, vec![], 10, "".to_string()); 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]); send(&mut svm, &[ix], &[&seller]);
let ix_update = ix_update_listing( let ix_update = ix_update_listing(
@@ -431,9 +493,9 @@ fn seller_can_update_listing() {
listing_id, listing_id,
Currency::Sol, Currency::Sol,
2_000_000_000, 2_000_000_000,
vec![],
None, None,
vec![], vec![],
vec![],
20, 20,
"ipfs://new".to_string(), "ipfs://new".to_string(),
); );
@@ -448,7 +510,7 @@ fn seller_can_update_listing() {
fn seller_can_close_listing() { fn seller_can_close_listing() {
let (mut svm, seller, _) = setup(); let (mut svm, seller, _) = setup();
let listing_id: u64 = 3; let listing_id: u64 = 3;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, vec![], None, vec![], 5, "".to_string()); 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]); send(&mut svm, &[ix], &[&seller]);
let ix_close = ix_close_listing(&seller.pubkey(), listing_id); let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
@@ -497,23 +559,18 @@ pub fn handler(
listing_id: u64, listing_id: u64,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, canonical_amount: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
require!(
alt_currencies.is_empty() || price_oracle.is_some(),
SolistingError::OracleRequired
);
let listing = &mut ctx.accounts.listing_account; let listing = &mut ctx.accounts.listing_account;
listing.seller = ctx.accounts.seller.key(); listing.seller = ctx.accounts.seller.key();
listing.canonical_currency = canonical_currency; listing.canonical_currency = canonical_currency;
listing.canonical_amount = canonical_amount; listing.canonical_amount = canonical_amount;
listing.canonical_oracle = canonical_oracle;
listing.alt_currencies = alt_currencies; listing.alt_currencies = alt_currencies;
listing.price_oracle = price_oracle;
listing.accepted_resolvers = accepted_resolvers; listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity; listing.quantity = quantity;
listing.quantity_reserved = 0; listing.quantity_reserved = 0;
@@ -550,22 +607,17 @@ pub fn handler(
ctx: Context<UpdateListing>, ctx: Context<UpdateListing>,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, canonical_amount: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
require!(
alt_currencies.is_empty() || price_oracle.is_some(),
SolistingError::OracleRequired
);
let listing = &mut ctx.accounts.listing_account; let listing = &mut ctx.accounts.listing_account;
listing.canonical_currency = canonical_currency; listing.canonical_currency = canonical_currency;
listing.canonical_amount = canonical_amount; listing.canonical_amount = canonical_amount;
listing.canonical_oracle = canonical_oracle;
listing.alt_currencies = alt_currencies; listing.alt_currencies = alt_currencies;
listing.price_oracle = price_oracle;
listing.accepted_resolvers = accepted_resolvers; listing.accepted_resolvers = accepted_resolvers;
listing.quantity = quantity; listing.quantity = quantity;
listing.metadata_uri = metadata_uri; listing.metadata_uri = metadata_uri;
@@ -651,8 +703,8 @@ pub fn ix_create_listing(
listing_id: u64, listing_id: u64,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, canonical_amount: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
@@ -665,8 +717,8 @@ pub fn ix_update_listing(
listing_id: u64, listing_id: u64,
canonical_currency: Currency, canonical_currency: Currency,
canonical_amount: u64, canonical_amount: u64,
alt_currencies: Vec<Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
@@ -720,15 +772,15 @@ pub mod solisting {
listing_id: u64, listing_id: u64,
canonical_currency: state::Currency, canonical_currency: state::Currency,
canonical_amount: u64, canonical_amount: u64,
alt_currencies: Vec<state::Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
create_listing::handler( create_listing::handler(
ctx, listing_id, canonical_currency, canonical_amount, ctx, listing_id, canonical_currency, canonical_amount,
alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
) )
} }
@@ -736,15 +788,15 @@ pub mod solisting {
ctx: Context<UpdateListing>, ctx: Context<UpdateListing>,
canonical_currency: state::Currency, canonical_currency: state::Currency,
canonical_amount: u64, canonical_amount: u64,
alt_currencies: Vec<state::Currency>, canonical_oracle: Option<Pubkey>,
price_oracle: Option<Pubkey>, alt_currencies: Vec<state::AltCurrencyConfig>,
accepted_resolvers: Vec<Pubkey>, accepted_resolvers: Vec<Pubkey>,
quantity: u32, quantity: u32,
metadata_uri: String, metadata_uri: String,
) -> Result<()> { ) -> Result<()> {
update_listing::handler( update_listing::handler(
ctx, canonical_currency, canonical_amount, ctx, canonical_currency, canonical_amount,
alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri,
) )
} }
@@ -797,7 +849,7 @@ fn buyer_can_create_order_canonical_sol() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 1u64; let listing_id = 1u64;
let price = 100_000_000u64; let price = 100_000_000u64;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, vec![], None, vec![], 5, "".to_string()); let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string());
send(&mut svm, &[ix], &[&seller]); send(&mut svm, &[ix], &[&seller]);
let order_id = 1u64; let order_id = 1u64;
@@ -824,7 +876,7 @@ fn buyer_can_create_order_canonical_sol() {
fn create_order_fails_if_listing_inactive() { fn create_order_fails_if_listing_inactive() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 2u64; let listing_id = 2u64;
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 1_000_000_000, vec![], None, vec![], 1, "".to_string()); 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]); send(&mut svm, &[ix], &[&seller]);
let ix_close = ix_close_listing(&seller.pubkey(), listing_id); let ix_close = ix_close_listing(&seller.pubkey(), listing_id);
send(&mut svm, &[ix_close], &[&seller]); send(&mut svm, &[ix_close], &[&seller]);
@@ -840,13 +892,13 @@ fn create_order_fails_if_out_of_stock() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 3u64; let listing_id = 3u64;
// quantity = 1, reserve it with first order // quantity = 1, reserve it with first order
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 1, "".to_string()); 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], &[&seller]);
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None)], &[&buyer]); send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None)], &[&buyer]);
let buyer2 = Keypair::new(); let buyer2 = Keypair::new();
svm.airdrop(&buyer2.pubkey(), 10_000_000_000).unwrap(); 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)], &[&buyer2]); 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()); assert!(result.is_err());
} }
@@ -855,12 +907,12 @@ fn create_order_fails_if_currency_not_accepted() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 4u64; let listing_id = 4u64;
// Sol-only listing — buyer tries to pay with fake SPL // Sol-only listing — buyer tries to pay with fake SPL
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string()); 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]); send(&mut svm, &[ix], &[&seller]);
let fake_mint = Pubkey::new_unique(); let fake_mint = Pubkey::new_unique();
let result = try_send(&mut svm, &[ix_create_order( let result = try_send(&mut svm, &[ix_create_order(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Spl { mint: fake_mint }, 0, None, &buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Spl { mint: fake_mint, decimals: 6 }, 0, None, None,
)], &[&buyer]); )], &[&buyer]);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -870,12 +922,12 @@ fn create_order_fails_if_resolver_not_accepted() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 5u64; let listing_id = 5u64;
let allowed_resolver = Pubkey::new_unique(); let allowed_resolver = Pubkey::new_unique();
let ix = ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![allowed_resolver], 5, "".to_string()); 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]); send(&mut svm, &[ix], &[&seller]);
// Pass a different resolver — must fail // Pass a different resolver — must fail
let result = try_send(&mut svm, &[ix_create_order_with_resolver( let result = try_send(&mut svm, &[ix_create_order_with_resolver(
&buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, Pubkey::new_unique(), &buyer.pubkey(), &seller.pubkey(), listing_id, 1, Currency::Sol, 0, None, None, Pubkey::new_unique(),
)], &[&buyer]); )], &[&buyer]);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -943,9 +995,14 @@ pub struct CreateOrder<'info> {
)] )]
pub descro_vault: UncheckedAccount<'info>, pub descro_vault: UncheckedAccount<'info>,
/// CHECK: Pyth price feed account. Required when payment_currency != canonical_currency. /// CHECK: Pyth USD feed for the canonical currency (listing.canonical_oracle).
/// Pass SystemProgram ID as a no-op placeholder when oracle is not needed. /// Pass SystemProgram as placeholder when canonical_oracle is None (stablecoin).
pub oracle: UncheckedAccount<'info>, 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 (stablecoin).
/// Also pass SystemProgram when paying in the canonical currency (oracle unused).
pub target_oracle: UncheckedAccount<'info>,
/// CHECK: Descro program — verified in handler /// CHECK: Descro program — verified in handler
pub descro_program: UncheckedAccount<'info>, pub descro_program: UncheckedAccount<'info>,
@@ -979,24 +1036,37 @@ pub fn handler(
// Determine amount in payment_currency // Determine amount in payment_currency
let amount = if payment_currency == listing.canonical_currency { let amount = if payment_currency == listing.canonical_currency {
// No oracle needed — pay canonical amount directly // Paying in canonical currency — no oracle needed
listing.canonical_amount listing.canonical_amount
} else { } else {
// Must be an accepted alt_currency // Must be a configured alt_currency
require!( let alt_cfg = listing.alt_currencies
listing.alt_currencies.contains(&payment_currency), .iter()
SolistingError::CurrencyNotAccepted .find(|c| c.currency == payment_currency)
); .ok_or(error!(SolistingError::CurrencyNotAccepted))?;
// Oracle must be configured on listing
let oracle_key = listing.price_oracle.ok_or(error!(SolistingError::OracleRequired))?; // Validate passed oracle accounts against listing state
require!( if let Some(expected_canonical) = listing.canonical_oracle {
ctx.accounts.oracle.key() == oracle_key, require!(
SolistingError::OracleMismatch 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( oracle::convert_with_slippage(
ctx.accounts.oracle.as_ref(), c_oracle,
&listing.canonical_currency, &listing.canonical_currency,
listing.canonical_amount, listing.canonical_amount,
t_oracle,
&payment_currency, &payment_currency,
expected_amount, expected_amount,
max_slippage_bps, max_slippage_bps,
@@ -1064,7 +1134,8 @@ pub fn read_order(svm: &LiteSVM, listing: Pubkey, buyer: Pubkey, order_id: u64)
solisting::state::OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap() solisting::state::OrderAccount::try_deserialize(&mut account.data.as_slice()).unwrap()
} }
// ix_create_order: derive escrow_id from order_pda bytes[0..8], pass SystemProgram as oracle placeholder /// `canonical_oracle` / `target_oracle`: pass None to use SystemProgram as placeholder
/// (i.e. when paying in canonical currency or when the oracle is a stablecoin None).
pub fn ix_create_order( pub fn ix_create_order(
buyer: &Pubkey, buyer: &Pubkey,
seller: &Pubkey, seller: &Pubkey,
@@ -1072,13 +1143,14 @@ pub fn ix_create_order(
order_id: u64, order_id: u64,
payment_currency: Currency, payment_currency: Currency,
max_slippage_bps: u16, max_slippage_bps: u16,
oracle: Option<Pubkey>, canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>,
) -> solana_message::compiled_instruction::CompiledInstruction { ) -> solana_message::compiled_instruction::CompiledInstruction {
let listing = listing_pda(seller, listing_id); let listing = listing_pda(seller, listing_id);
let order = order_pda(listing, *buyer, order_id); let order = order_pda(listing, *buyer, order_id);
let escrow_id = u64::from_le_bytes(order.to_bytes()[0..8].try_into().unwrap()); let escrow_id = u64::from_le_bytes(order.to_bytes()[0..8].try_into().unwrap());
let resolver = Pubkey::new_unique(); // default resolver for simple tests let resolver = Pubkey::new_unique(); // default resolver for simple tests
ix_create_order_with_resolver(buyer, seller, listing_id, order_id, payment_currency, max_slippage_bps, oracle, resolver) 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( pub fn ix_create_order_with_resolver(
@@ -1088,7 +1160,8 @@ pub fn ix_create_order_with_resolver(
order_id: u64, order_id: u64,
payment_currency: Currency, payment_currency: Currency,
max_slippage_bps: u16, max_slippage_bps: u16,
oracle: Option<Pubkey>, canonical_oracle: Option<Pubkey>,
target_oracle: Option<Pubkey>,
resolver: Pubkey, resolver: Pubkey,
) -> solana_message::compiled_instruction::CompiledInstruction { ) -> solana_message::compiled_instruction::CompiledInstruction {
let listing = listing_pda(seller, listing_id); let listing = listing_pda(seller, listing_id);
@@ -1102,9 +1175,10 @@ pub fn ix_create_order_with_resolver(
&[b"vault", escrow_account.as_ref()], &[b"vault", escrow_account.as_ref()],
&descro::id(), &descro::id(),
); );
let oracle_key = oracle.unwrap_or(solana_sdk::system_program::id()); let canonical_oracle_key = canonical_oracle.unwrap_or(solana_sdk::system_program::id());
let target_oracle_key = target_oracle.unwrap_or(solana_sdk::system_program::id());
let expected_amount = 0u64; // tests that don't use oracle set this to 0 let expected_amount = 0u64; // tests that don't use oracle set this to 0
todo!("build CompiledInstruction: discriminator + borsh encode (order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps), accounts list") todo!("build CompiledInstruction: discriminator + borsh encode (order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps), accounts: [..., canonical_oracle_key, target_oracle_key, ...]")
} }
``` ```
@@ -1140,9 +1214,9 @@ fn seller_accept_creates_active_descro_escrow() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 10u64; let listing_id = 10u64;
let price = 100_000_000u64; let price = 100_000_000u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, vec![], None, vec![], 5, "".to_string())], &[&seller]); send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, None, vec![], vec![], 5, "".to_string())], &[&seller]);
let order_id = 1u64; let order_id = 1u64;
send(&mut svm, &[ix_create_order(&buyer.pubkey(), &seller.pubkey(), listing_id, order_id, Currency::Sol, 0, None)], &[&buyer]); 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 listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), order_id); let order = read_order(&svm, listing_key, buyer.pubkey(), order_id);
@@ -1269,8 +1343,8 @@ fn seller_can_reject_order() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 20u64; let listing_id = 20u64;
let price = 100_000_000u64; let price = 100_000_000u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, price, vec![], None, vec![], 5, "".to_string())], &[&seller]); 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)], &[&buyer]); 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 buyer_before = svm.get_account(&buyer.pubkey()).map(|a| a.lamports).unwrap_or(0);
let listing_key = listing_pda(&seller.pubkey(), listing_id); let listing_key = listing_pda(&seller.pubkey(), listing_id);
@@ -1288,8 +1362,8 @@ fn seller_can_reject_order() {
fn buyer_can_cancel_order() { fn buyer_can_cancel_order() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 21u64; let listing_id = 21u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); 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)], &[&buyer]); 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 listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1303,8 +1377,8 @@ fn reject_handles_already_cancelled_escrow() {
// Buyer cancels directly on descro, then seller calls reject_order — must not panic // Buyer cancels directly on descro, then seller calls reject_order — must not panic
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 22u64; let listing_id = 22u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); 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)], &[&buyer]); 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 listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1525,8 +1599,8 @@ Anyone can call this to close an `OrderAccount` whose descro escrow is in a term
fn anyone_can_close_stale_order_after_terminal_escrow() { fn anyone_can_close_stale_order_after_terminal_escrow() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 30u64; let listing_id = 30u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); 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)], &[&buyer]); 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 listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1548,8 +1622,8 @@ fn anyone_can_close_stale_order_after_terminal_escrow() {
fn close_stale_order_fails_if_escrow_still_active() { fn close_stale_order_fails_if_escrow_still_active() {
let (mut svm, seller, buyer) = setup(); let (mut svm, seller, buyer) = setup();
let listing_id = 31u64; let listing_id = 31u64;
send(&mut svm, &[ix_create_listing(&seller.pubkey(), listing_id, Currency::Sol, 100_000_000, vec![], None, vec![], 5, "".to_string())], &[&seller]); 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)], &[&buyer]); 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 listing_key = listing_pda(&seller.pubkey(), listing_id);
let order = read_order(&svm, listing_key, buyer.pubkey(), 1); let order = read_order(&svm, listing_key, buyer.pubkey(), 1);
@@ -1671,12 +1745,12 @@ declare_id!("So1istingProgramID11111111111111111111111111"); // replace with: so
pub mod solisting { pub mod solisting {
use super::*; use super::*;
pub fn create_listing(ctx: Context<CreateListing>, listing_id: u64, canonical_currency: state::Currency, canonical_amount: u64, alt_currencies: Vec<state::Currency>, price_oracle: Option<Pubkey>, accepted_resolvers: Vec<Pubkey>, quantity: u32, metadata_uri: String) -> Result<()> { pub fn create_listing(ctx: Context<CreateListing>, listing_id: u64, canonical_currency: state::Currency, canonical_amount: 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, canonical_amount, alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri) create_listing::handler(ctx, listing_id, canonical_currency, canonical_amount, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri)
} }
pub fn update_listing(ctx: Context<UpdateListing>, canonical_currency: state::Currency, canonical_amount: u64, alt_currencies: Vec<state::Currency>, price_oracle: Option<Pubkey>, accepted_resolvers: Vec<Pubkey>, quantity: u32, metadata_uri: String) -> Result<()> { pub fn update_listing(ctx: Context<UpdateListing>, canonical_currency: state::Currency, canonical_amount: 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, canonical_amount, alt_currencies, price_oracle, accepted_resolvers, quantity, metadata_uri) update_listing::handler(ctx, canonical_currency, canonical_amount, canonical_oracle, alt_currencies, accepted_resolvers, quantity, metadata_uri)
} }
pub fn close_listing(ctx: Context<CloseListing>) -> Result<()> { pub fn close_listing(ctx: Context<CloseListing>) -> Result<()> {
@@ -1684,6 +1758,7 @@ pub mod solisting {
} }
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<()> { 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<()> {
// canonical_oracle and target_oracle come from the account struct, not args
create_order::handler(ctx, order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps) create_order::handler(ctx, order_id, escrow_id, resolver, payment_currency, expected_amount, max_slippage_bps)
} }