From 9def662dc17d0c1d996eed87605ad3cd62ace764 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Mon, 31 Aug 2026 14:23:37 +0300 Subject: [PATCH 1/8] feat(scenarios): add Tessera state preparation Tessera is a proprietary market maker with no IDL, so its market accounts are written through the raw byte layout the BisonFi work introduced. Eight templates cover price, depth, curve, halt, staleness and freshness. The market catalog lists all 26 live markets with their mints, decimals and freshness limit, so the UI constrains the choice and a model can resolve one through search_constant_options. One builder exists, for the only thing a template cannot express: turning a human price into the pair of reciprocal atomic ratios, which needs both mints' decimals. --- Cargo.lock | 3 + crates/core/Cargo.toml | 2 + crates/core/src/scenarios/protocols/mod.rs | 1 + .../src/scenarios/protocols/tessera/README.md | 134 ++ .../src/scenarios/protocols/tessera/mod.rs | 1 + .../protocols/tessera/v1/fair_value.rs | 413 +++++ .../src/scenarios/protocols/tessera/v1/mod.rs | 6 + .../protocols/tessera/v1/overrides.yaml | 874 +++++++++++ crates/core/src/scenarios/registry.rs | 14 +- crates/core/src/tests/live.rs | 60 + crates/core/src/tests/mod.rs | 4 + crates/core/src/tests/tessera/mod.rs | 1369 +++++++++++++++++ crates/mcp/Cargo.toml | 2 + crates/mcp/src/surfpool/mod.rs | 166 +- 14 files changed, 3014 insertions(+), 35 deletions(-) create mode 100644 crates/core/src/scenarios/protocols/tessera/README.md create mode 100644 crates/core/src/scenarios/protocols/tessera/mod.rs create mode 100644 crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs create mode 100644 crates/core/src/scenarios/protocols/tessera/v1/mod.rs create mode 100644 crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml create mode 100644 crates/core/src/tests/live.rs create mode 100644 crates/core/src/tests/tessera/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 141bce739..eda85fc42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12154,6 +12154,7 @@ dependencies = [ "solana-packet", "solana-program-option 3.1.0", "solana-program-pack 3.1.0", + "solana-program-runtime", "solana-pubkey 3.0.0", "solana-pubsub-client", "solana-rpc-client", @@ -12218,6 +12219,8 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "solana-account 4.3.1", + "solana-commitment-config", "solana-keypair", "solana-pubkey 3.0.0", "solana-signer", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 53f510da0..7952c8145 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -115,6 +115,8 @@ axum = { version = "0.8", default-features = false, features = ["tokio", "http1" [dev-dependencies] ed25519-dalek = "1.0.1" +# Only the Tessera live suite uses it, to host the CPI wrapper that drives the deployed program. +solana-program-runtime = "4.1.2" libsecp256k1 = "0.7.2" p256 = { version = "0.13", default-features = false, features = ["ecdsa"] } test-case = { workspace = true } diff --git a/crates/core/src/scenarios/protocols/mod.rs b/crates/core/src/scenarios/protocols/mod.rs index 99f0b0967..b6af45e27 100644 --- a/crates/core/src/scenarios/protocols/mod.rs +++ b/crates/core/src/scenarios/protocols/mod.rs @@ -1 +1,2 @@ pub mod pump; +pub mod tessera; diff --git a/crates/core/src/scenarios/protocols/tessera/README.md b/crates/core/src/scenarios/protocols/tessera/README.md new file mode 100644 index 000000000..33ba18c65 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/README.md @@ -0,0 +1,134 @@ +# Tessera + +Tessera is a proprietary market maker that publishes no IDL. Surfpool writes its market accounts +through the raw byte layout in `v1/overrides.yaml`. It prepares state; it does not construct or +submit a swap. + +## Deployment + +- Program: `TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH` +- ProgramData: `BzSXM6KLDpHQQChzr7Fdgbzwp8r8zRYWFFrHK2uZmDYV` +- Upgrade authority: `7bJ9xu9UGVZPtYzH1fMwdaKdvfhqeSJtoFc2eGrXBPhK` +- Deploy slot: `438800691` +- ELF SHA-256: `433f2a857ffe2045310a478b4aca0fd824308d01f283719275baec60e2aecb3b` + +Every offset and behavior below was proven against exactly this deployment; the live suite pins +it and fails when any of these values move. A redeploy voids the layout evidence — re-verify +before trusting the templates again. + +## The guard, and what it does not cover + +A market is 1264 bytes with the eight-byte layout tag `05 00 00 00 00 00 00 00` at offset 96. All +26 live markets carry that tag, and no other Tessera account is 1264 bytes, so size alone already +separates markets from the program's other accounts today. The tag is the version half of the +guard: it is what rejects a future market layout that reuses the size. + +The shared raw-layout schema has no owner predicate, so a foreign account of the same size carrying +the same eight bytes would pass a raw template. `validate_tessera_market_layout` adds the ownership +check, and every scenario made through the fair-value builder goes through it. Composing the raw +template against an arbitrary address does not. That is a property of the shared schema, not of +this integration, and the raw scenario API is unvalidated by contract. + +## Templates + +| Template | Prepared state | +|---|---| +| `tessera-fair-value` | both directional atomic-ratio fields | +| `tessera-depth` | all twenty directional capacities on both ladders | +| `tessera-curve` | all twenty directional output factors on both ladders | +| `tessera-halt` | both required first-level enabled flags | +| `tessera-stale-quote` | offset 120, aged by the lead you pass (default -20) | +| `tessera-freshness` | offset 120, the current materialization slot | + +The direct field at offset 128 is quote atomic units per base atomic unit multiplied by `10^15`. +The reciprocal at offset 144 uses the same scale, so their product is approximately `10^30` after +integer-floor rounding. Changing only one of them moves one quote direction and leaves the other +where it was, which is why they are one invariant. + +The sell ladder occupies bytes 160 through 639 and the buy ladder 640 through 1119. Each holds +twenty 24-byte records: directional capacity at `+0`, marginal-price factor at `+8`, enabled flag +at `+16`. Capacity and factor changes affect only their active quote direction. For a fill +contained in the first level, both directions match +`floor(input_atoms * directional_price * first_level_factor / 10^21)` exactly. + +Offset 88 stores the age at which the program rejects a quote. Age 19 succeeds and age 20 fails +with custom error 65535 on a market configured at 20. + +Both slot templates take the lead from the caller: the value supplied for `last_update_slot` is +added to the materialization slot, and only `null` falls back to the template's own lead. One stale +template therefore covers every market, including one configured at a limit nobody has seen yet. +Each market's limit travels with its address in the catalog, so a caller reads it there and passes +its negation. Passing a number where you meant the default is the one trap: `0` on the stale +template writes a perfectly fresh quote. + +## The market catalog + +`v1/overrides.yaml` carries a `market` constant listing every live market with its mints, their +decimals, and its freshness limit. The UI constrains the choice to it and `search_constant_options` +resolves it for models. It is a snapshot, captured 2026-08-31 (26 markets: +fourteen with freshness limit 20, eleven at 25, one at 55): Tessera lists markets continuously, +a new one reaches the catalog on the next refresh, and until then the raw scenario API still +accepts its address directly. + +Refresh it by reading the live set and rewriting the `options` block: + +```bash +curl -s -X POST "$RPC_URL" -H 'Content-Type: application/json' -d '{ + "jsonrpc":"2.0","id":1,"method":"getProgramAccounts", + "params":["TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH", + {"encoding":"base64","commitment":"confirmed", + "filters":[{"dataSize":1264}], + "dataSlice":{"offset":24,"length":72}}]}' +``` + +Each result yields the base mint at `+0`, the quote mint at `+32`, and the freshness limit at +`+64`. Decimals come from the two mint accounts. `tessera_catalog_matches_live_markets` fails when +the catalog and the chain disagree, so a stale catalog is caught rather than shipped. + +## Builder and tool + +One builder exists, for the one thing a template cannot express: turning a human price into the +pair of reciprocal atomic ratios, which needs both mints' decimals. It is a pure function over +account data. `create_tessera_fair_value_scenario` reads the market and both mints through the +surfnet's own RPC, so local state wins and only missing accounts fall back to the datasource, then +stages the scenario through the shared path. + +The price override deliberately does not set `fetchBeforeUse`. Reading the accounts at creation +hydrates them into local state, so the values apply to the same bytes they were derived from; a +Play-time refetch would reinstall remote bytes over any local edit and patch a different read. + +The paired freshness override is persisted. Its slot encoder writes the slot it materializes at, +so the prepared price stays inside the market's freshness window however long the scenario runs. + +Depth and curve have no builder here. Their templates expose every field, and the scaling helpers +that read a live ladder and preserve its ordering are parked until a product flow asks for them. + +## Behavioral evidence + +The live suite loads the pinned deployed ELF from ProgramData into LiteSVM and fails if the +ProgramData address, deploy slot, or ELF hash changes. It proves each price field controls only its +matching direction, that active-side capacities and factors alter large fills while the opposite +side stays byte-for-byte identical, that first-level output in both directions equals the +price-times-factor formula to the atom, that age 19 succeeds and age 20 fails with error 65535, +that disabling both required first levels fails both directions, that an unordered single curve +factor fails with error 8, and that the catalog matches the live market set. + +Run it serially. The public endpoint sheds queued requests right after a `getProgramAccounts` scan, +sometimes as a 413 that looks like a request-size error: + +```bash +SURFPOOL_TEST_RPC_URL= cargo test -p surfpool-core --features integration-tests \ + tests::tessera -- --test-threads=1 --nocapture +``` + +`SURFPOOL_TEST_RPC_URL` is optional and defaults to the public mainnet endpoint. Set it to a +private endpoint when the public one rate-limits. + +## Known boundaries + +The remaining header and trailing fields carry no assigned semantics. No separate fee field is +exposed: the proven first-level output has no deduction beyond its directional price and factor, +but that does not establish how Tessera decomposes the factor into spread, fee, or another price +adjustment. The structured region from 1120 onward stays unexposed because its economic meaning has +not been behaviorally proven. Vault depletion is not exposed either; the generic SPL Token balance +template follows the Anchor discriminator path and does not materialize a non-Anchor token account. diff --git a/crates/core/src/scenarios/protocols/tessera/mod.rs b/crates/core/src/scenarios/protocols/tessera/mod.rs new file mode 100644 index 000000000..a3a6d96c3 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs new file mode 100644 index 000000000..f3d78287c --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs @@ -0,0 +1,413 @@ +//! Tessera fair-value state preparation. +//! +//! Tessera publishes no IDL. Every write goes through the raw layout in `overrides.yaml`; this +//! module exists only for the one thing a template cannot express: turning a human price into the +//! pair of reciprocal atomic ratios the program reads, which needs both mints' decimals. + +use std::{collections::HashMap, sync::LazyLock}; + +use solana_account::Account; +use solana_pubkey::Pubkey; +use surfpool_types::{AccountAddress, OverrideInstance, OverrideTemplate, RawLayout, Scenario}; + +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + scenarios::TemplateRegistry, + types::MintAccount, +}; + +pub const TESSERA_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH"); +pub const TESSERA_DEFAULT_MARKET: Pubkey = + Pubkey::from_str_const("FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n"); + +/// The two mint offsets are read, never written, so no template declares them. +const BASE_MINT_OFFSET: usize = 24; +const QUOTE_MINT_OFFSET: usize = 56; + +/// The size and layout tag a Tessera market must have, taken from the manifest the raw templates +/// are written against so there is one definition of them. Built once; the manifest is compiled in. +static MARKET_LAYOUT: LazyLock = LazyLock::new(|| { + template(&TemplateRegistry::new(), FAIR_VALUE_TEMPLATE) + .and_then(|template| { + template.raw_layout.clone().ok_or_else(|| { + SurfpoolError::internal("the Tessera manifest carries no raw layout") + }) + }) + .expect("the Tessera manifest is compiled in and always parses") +}); + +const FAIR_VALUE_TEMPLATE: &str = "tessera-fair-value"; +const FRESHNESS_TEMPLATE: &str = "tessera-freshness"; + +/// Both ratio fields are integers scaled by 10^15, so their product is 10^30. +const ATOMIC_RATIO_SCALE: u128 = 1_000_000_000_000_000; +const RECIPROCAL_PRODUCT: u128 = ATOMIC_RATIO_SCALE * ATOMIC_RATIO_SCALE; + +/// Both overrides apply on Play, before any slot advance. +const PREPARATION_SLOT: u64 = 0; + +/// The parts of a Tessera market a price needs: which mints it quotes, and at what scale. +#[derive(Clone, Debug, PartialEq)] +pub struct TesseraMarket { + pub address: Pubkey, + pub base_mint: Pubkey, + pub quote_mint: Pubkey, + pub base_decimals: u8, + pub quote_decimals: u8, +} + +impl TesseraMarket { + pub fn mint_addresses(market_account: &Account) -> SurfpoolResult<(Pubkey, Pubkey)> { + validate_tessera_market_layout(market_account)?; + let base_mint = read_pubkey(&market_account.data, BASE_MINT_OFFSET)?; + let quote_mint = read_pubkey(&market_account.data, QUOTE_MINT_OFFSET)?; + if base_mint == Pubkey::default() + || quote_mint == Pubkey::default() + || base_mint == quote_mint + { + return Err(invalid("market has invalid mint identities")); + } + Ok((base_mint, quote_mint)) + } + + pub fn validate( + address: Pubkey, + market_account: &Account, + base_mint_account: &Account, + quote_mint_account: &Account, + ) -> SurfpoolResult { + let (base_mint, quote_mint) = Self::mint_addresses(market_account)?; + + validate_mint_owner(base_mint_account, "base")?; + validate_mint_owner(quote_mint_account, "quote")?; + let base_decimals = MintAccount::unpack(&base_mint_account.data) + .map_err(|_| invalid("base mint account is invalid"))? + .decimals(); + let quote_decimals = MintAccount::unpack("e_mint_account.data) + .map_err(|_| invalid("quote mint account is invalid"))? + .decimals(); + + Ok(Self { + address, + base_mint, + quote_mint, + base_decimals, + quote_decimals, + }) + } +} + +/// Rejects an account that is not a Tessera market. +/// +/// The shared raw-layout guard has no owner predicate, so a foreign account of the same size +/// carrying the same layout tag would pass it. Every builder-made scenario comes through here, +/// which adds the ownership check the schema cannot express. +pub fn validate_tessera_market_layout(account: &Account) -> SurfpoolResult<()> { + if account.owner != TESSERA_PROGRAM_ID { + return Err(invalid("market is not owned by Tessera")); + } + MARKET_LAYOUT.guard(&account.data).map_err(invalid) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TesseraFairValuePreparation { + pub scenario: Scenario, + pub market: Pubkey, + pub quote_atoms_per_base_atom_x1e15: u64, + pub base_atoms_per_quote_atom_x1e15: u64, +} + +pub fn build_tessera_fair_value_scenario( + market: &TesseraMarket, + price: &str, +) -> SurfpoolResult { + let quote_atoms_per_base_atom_x1e15 = + human_price_to_atomic_ratio(price, market.base_decimals, market.quote_decimals)?; + let reciprocal = RECIPROCAL_PRODUCT / u128::from(quote_atoms_per_base_atom_x1e15); + let base_atoms_per_quote_atom_x1e15 = u64::try_from(reciprocal) + .map_err(|_| invalid("price is too small for Tessera's reciprocal u64 field"))?; + + let registry = TemplateRegistry::new(); + let fair_value = template(®istry, FAIR_VALUE_TEMPLATE)?; + let freshness = template(®istry, FRESHNESS_TEMPLATE)?; + let market_name = market_display_name(fair_value, &market.address); + let target = AccountAddress::Pubkey(market.address.to_string()); + + // No fetch_before_use: these values were derived from the market account this scenario was + // built against, which creation already hydrated into local state. A Play-time refetch would + // reinstall remote bytes over any local edit and apply numbers derived from a different read. + let price_override = + OverrideInstance::new(fair_value.id.clone(), PREPARATION_SLOT, target.clone()) + .with_values(HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(quote_atoms_per_base_atom_x1e15.to_string()), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(base_atoms_per_quote_atom_x1e15.to_string()), + ), + ])) + .with_label(format!("Tessera {market_name} fair value")); + + // Null, not zero: the slot encoder reads a supplied number AS the lead, so only null takes the + // template's own lead of zero. Persisted, so the prepared price stays inside the market's + // freshness window however long the scenario is left running. + let freshness_override = OverrideInstance::new(freshness.id.clone(), PREPARATION_SLOT, target) + .with_values(HashMap::from([( + "last_update_slot".to_string(), + serde_json::Value::Null, + )])) + .with_label("Keep Tessera quote fresh".to_string()) + .with_persist(true); + + let normalized_price = price.trim(); + let mut scenario = Scenario::new( + format!("Tessera {market_name} at {normalized_price}"), + format!( + "Prepare Tessera market {} to quote one base token at {normalized_price} quote tokens; no swap is sent.", + market.address + ), + ); + scenario.tags = vec![ + "tessera".to_string(), + "pmm".to_string(), + "price-dislocation".to_string(), + ]; + scenario.add_override(price_override); + scenario.add_override(freshness_override); + + Ok(TesseraFairValuePreparation { + scenario, + market: market.address, + quote_atoms_per_base_atom_x1e15, + base_atoms_per_quote_atom_x1e15, + }) +} + +fn read_pubkey(data: &[u8], offset: usize) -> SurfpoolResult { + let bytes: [u8; 32] = data[offset..offset + 32] + .try_into() + .map_err(|_| invalid("market mint bytes are truncated"))?; + Ok(Pubkey::new_from_array(bytes)) +} + +fn validate_mint_owner(account: &Account, side: &str) -> SurfpoolResult<()> { + if account.owner != spl_token_interface::ID && account.owner != spl_token_2022_interface::ID { + return Err(invalid(format!( + "{side} mint is not owned by a supported token program" + ))); + } + Ok(()) +} + +fn human_price_to_atomic_ratio( + price: &str, + base_decimals: u8, + quote_decimals: u8, +) -> SurfpoolResult { + let value = price.trim(); + let mut parts = value.split('.'); + let whole = parts.next().unwrap_or_default(); + let fractional = parts.next().unwrap_or_default(); + if parts.next().is_some() + || whole.is_empty() + || !whole.bytes().all(|byte| byte.is_ascii_digit()) + || !fractional.bytes().all(|byte| byte.is_ascii_digit()) + { + return Err(invalid("price must be a positive decimal string")); + } + + let digits = format!("{whole}{fractional}") + .parse::() + .map_err(|_| invalid("price is too large"))?; + let exponent = i32::from(quote_decimals) + 15 + - i32::from(base_decimals) + - i32::try_from(fractional.len()).map_err(|_| invalid("price is too precise"))?; + let scaled = if exponent >= 0 { + digits + .checked_mul(checked_power_of_ten(exponent as u32)?) + .ok_or_else(|| invalid("price is too large"))? + } else { + digits / checked_power_of_ten(exponent.unsigned_abs())? + }; + if scaled == 0 { + return Err(invalid( + "price is too small for this market's mint decimals", + )); + } + u64::try_from(scaled).map_err(|_| { + let scale = i32::from(quote_decimals) + 15 - i32::from(base_decimals); + let max_price = checked_power_of_ten(scale.unsigned_abs()) + .map(|power| u128::from(u64::MAX) / power) + .unwrap_or_default(); + invalid(format!( + "price is too large for Tessera's u64 field; this market accepts at most about {max_price} quote per base" + )) + }) +} + +fn checked_power_of_ten(exponent: u32) -> SurfpoolResult { + 10u128 + .checked_pow(exponent) + .ok_or_else(|| invalid("price scale exceeds supported precision")) +} + +/// The catalog pair for a listed market ("SOL/USDC"), a shortened address for one it does not list. +fn market_display_name(template: &OverrideTemplate, market: &Pubkey) -> String { + let address = market.to_string(); + template + .constants + .get("market") + .and_then(|constant| { + constant + .options + .iter() + .find(|option| option.value == address) + }) + .map(|option| option.label.clone()) + .unwrap_or_else(|| format!("{}…{}", &address[..4], &address[address.len() - 4..])) +} + +fn template<'a>(registry: &'a TemplateRegistry, id: &str) -> SurfpoolResult<&'a OverrideTemplate> { + registry + .get(id) + .ok_or_else(|| SurfpoolError::internal(format!("Tessera template {id} is unavailable"))) +} + +fn invalid(message: impl Into) -> SurfpoolError { + SurfpoolError::internal(message.into()) +} + +#[cfg(test)] +mod tests { + use solana_program_pack::Pack; + + use super::*; + + fn mint_account(decimals: u8) -> Account { + let mut data = vec![0; spl_token_interface::state::Mint::LEN]; + spl_token_interface::state::Mint { + decimals, + is_initialized: true, + ..Default::default() + } + .pack_into_slice(&mut data); + Account { + data, + owner: spl_token_interface::ID, + ..Account::default() + } + } + + fn market_account(base_mint: &Pubkey, quote_mint: &Pubkey) -> Account { + let mut data = vec![0; MARKET_LAYOUT.account_size]; + data[BASE_MINT_OFFSET..BASE_MINT_OFFSET + 32].copy_from_slice(base_mint.as_ref()); + data[QUOTE_MINT_OFFSET..QUOTE_MINT_OFFSET + 32].copy_from_slice(quote_mint.as_ref()); + let magic = MARKET_LAYOUT.magic.as_ref().expect("manifest layout tag"); + data[magic.offset..magic.offset + magic.bytes.len()].copy_from_slice(&magic.bytes); + Account { + data, + owner: TESSERA_PROGRAM_ID, + ..Account::default() + } + } + + fn market(base_decimals: u8, quote_decimals: u8) -> TesseraMarket { + let base_mint = Pubkey::new_unique(); + let quote_mint = Pubkey::new_unique(); + TesseraMarket::validate( + Pubkey::new_unique(), + &market_account(&base_mint, "e_mint), + &mint_account(base_decimals), + &mint_account(quote_decimals), + ) + .expect("valid Tessera market") + } + + #[test] + fn builds_atomic_fair_value_for_wsol_usdc_decimals() { + let market = market(9, 6); + let preparation = build_tessera_fair_value_scenario(&market, "100.25").unwrap(); + assert_eq!( + preparation.quote_atoms_per_base_atom_x1e15, + 100_250_000_000_000 + ); + assert_eq!( + preparation.base_atoms_per_quote_atom_x1e15, + (RECIPROCAL_PRODUCT / 100_250_000_000_000u128) as u64 + ); + assert_eq!(preparation.scenario.overrides.len(), 2); + assert_eq!( + preparation.scenario.overrides[0].account, + AccountAddress::Pubkey(market.address.to_string()) + ); + } + + #[test] + fn derives_price_scale_from_market_mint_decimals() { + let market = market(8, 6); + let preparation = build_tessera_fair_value_scenario(&market, "78.8477010015472512") + .expect("build CBB/USDC price"); + assert_eq!( + preparation.quote_atoms_per_base_atom_x1e15, + 788_477_010_015_472 + ); + assert_eq!( + preparation.base_atoms_per_quote_atom_x1e15, + (RECIPROCAL_PRODUCT / 788_477_010_015_472u128) as u64 + ); + } + + /// The price is computed from one read of the market; a Play-time refetch would apply it to a + /// different one and overwrite local edits. The freshness value must stay null, because the + /// slot encoder reads a supplied number as the lead rather than ignoring it. + #[test] + fn price_applies_to_the_read_it_came_from_and_freshness_keeps_the_template_lead() { + let preparation = build_tessera_fair_value_scenario(&market(9, 6), "100.25").unwrap(); + let [price, freshness] = &preparation.scenario.overrides[..] else { + panic!("expected exactly a price and a freshness override"); + }; + assert!(!price.fetch_before_use); + assert!(!price.persist); + assert!(!freshness.fetch_before_use); + assert!(freshness.persist); + assert_eq!( + freshness.values.get("last_update_slot"), + Some(&serde_json::Value::Null) + ); + } + + #[test] + fn rejects_invalid_price_and_market_inputs() { + let market = market(9, 6); + for price in ["0", "-1", "1.2.3", "not-a-price", ""] { + assert!(build_tessera_fair_value_scenario(&market, price).is_err()); + } + + let base_mint = mint_account(9); + let quote_mint = mint_account(6); + let wrong_owner = Account { + owner: Pubkey::new_unique(), + ..market_account(&Pubkey::new_unique(), &Pubkey::new_unique()) + }; + assert!( + TesseraMarket::validate(Pubkey::new_unique(), &wrong_owner, &base_mint, "e_mint) + .is_err() + ); + // The raw guard cannot see the owner, which is the whole reason this check sits on top. + assert!(MARKET_LAYOUT.guard(&wrong_owner.data).is_ok()); + + let same_mint = Pubkey::new_unique(); + assert!( + TesseraMarket::validate( + Pubkey::new_unique(), + &market_account(&same_mint, &same_mint), + &base_mint, + "e_mint, + ) + .is_err() + ); + } +} diff --git a/crates/core/src/scenarios/protocols/tessera/v1/mod.rs b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs new file mode 100644 index 000000000..37db50bb7 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs @@ -0,0 +1,6 @@ +mod fair_value; + +pub use fair_value::{ + TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraFairValuePreparation, TesseraMarket, + build_tessera_fair_value_scenario, validate_tessera_market_layout, +}; diff --git a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml new file mode 100644 index 000000000..2f62f7454 --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml @@ -0,0 +1,874 @@ +protocol: Tessera +version: deployed-438800691 +account_type: MarketState + +raw_layout: + account_size: 1264 + magic: + offset: 96 + bytes: [5, 0, 0, 0, 0, 0, 0, 0] + +tags: + - pmm + - prop-amm + - swap + +constants: + # Every live Tessera market, captured 2026-08-31 by the documented refresh command in + # ../README.md. Tessera lists markets continuously: a new one reaches this catalog on the + # next refresh, and until then the raw scenario API still accepts its address directly. + # + # freshness_limit_slots is the age at which the deployed program rejects a quote. It picks + # the matching tessera-stale-quote template, which is why it travels with the address. + market: + label: Tessera Market + description: Select a live Tessera market account + options: + - id: sol_usdc + label: SOL/USDC + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + description: >- + Default market. Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: So11111111111111111111111111111111111111112 + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 9 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: 2z_usdc + label: 2Z/USDC + value: ETZYG4pXscxNchSobJHnN4aDrVNL45gscnzcxM7sEDKe + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: J6pQQ3FAcJQeWPPGppWRb4nM8jU3wLyYbRrLh7feMfvd + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 8 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: bonk_usdc + label: Bonk/USDC + value: 8B8KYcFPMjZyDtJ1BENsqhW3fEmMrB4ekoEQJiM6z3SC + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263 + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 5 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: cbbtc_usdc + label: cbBTC/USDC + value: 9NkuAWB4LgCVFV77omEkJEjXqgV5PGupwMTu3B3pBRhc + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 8 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: fartcoin_sol + label: Fartcoin/SOL + value: rJ554NghHHwuUXL2mGJJviGrpYk7P2qSGTruwwD5KKb + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: 9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump + quote_mint: So11111111111111111111111111111111111111112 + base_decimals: 6 + quote_decimals: 9 + freshness_limit_slots: 20 + - id: fartcoin_usdc + label: Fartcoin/USDC + value: F45HLDGN3mYBaJAKB1UGkx8Y7o2ruS5hgBox1Us6AZF9 + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: 9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: hype_usdc + label: HYPE/USDC + value: FvQds9kfSi7opoW7Lf3KcsgcuKfJZB2HdrVNkdLSLQbi + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: 98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 9 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: jlp_sol + label: JLP/SOL + value: H3chk8rgniKXnToGTdPUFieuHGLQQfBMXVbGp6bR1uMD + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: 27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4 + quote_mint: So11111111111111111111111111111111111111112 + base_decimals: 6 + quote_decimals: 9 + freshness_limit_slots: 25 + - id: jto_usdc + label: JTO/USDC + value: DoKKUBzWcv6TYg3vr6kvVzvadnibieYctse6oD6d7Hxs + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 9 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: jup_usdc + label: JUP/USDC + value: Drx7X5bUajSPY6m5f2QhD2CK8c3RBMKgueSpZgCgUzkP + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: launchcoin_usdc + label: LAUNCHCOIN/USDC + value: ESaTtQcbtqk3eLNUQvND9uuMKjqfEtgmzwCspr5EbALo + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: Ey59PH7Z4BFU4HjyKnyMdWt5GGN76KazTAwQihoUXRnk + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 9 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: met_usdc + label: MET/USDC + value: 9WSspMLnTfaec7whu4kG3peCCn4gWGBvKayamxDEndqR + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: mon_usdc + label: MON/USDC + value: E5Zmb4JQbHd4ThBXj1Fix9dMGsdszEGQ17txAHLWPwvS + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: CrAr4RRJMBVwRsZtT62pEhfA9H5utymC2mVx8e7FreP2 + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 8 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: mu_usdc + label: MU/USDC + value: FCVmVJbiwiHc1ndWvwshFyjPYSzyRN12YNKRQn3AirHA + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: MUxEsUKSMACyw5fZf68wxf5FLnZVhtU9CwH8uNNGay1 + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: pengu_usdc + label: PENGU/USDC + value: 9nyLnejbWhNvzhg6c4RvFW5ei9DT1G3X8MhNXCkYj2hQ + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: 2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: pump_sol + label: PUMP/SOL + value: AbfkmuyTqhGJkPLKM5tmAeUdthbyLqAh8bNwR8AumPpE + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn + quote_mint: So11111111111111111111111111111111111111112 + base_decimals: 6 + quote_decimals: 9 + freshness_limit_slots: 20 + - id: pump_usdc + label: PUMP/USDC + value: DNhfyh75AApg1L1Yig3fErvERKutYRqfWLGb496iViSZ + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: sol_usdt + label: SOL/USDT + value: 7sJf1SmKDDAFtBmMtg253rTbjG7zVFm3zTNounSgSNc9 + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: So11111111111111111111111111111111111111112 + quote_mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB + base_decimals: 9 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: spcx_usdc + label: SPCX/USDC + value: 5X9A6PpFQEsc9D5VdTGfgVyfVn8HnsArQpMMUZZfFg1a + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: spyx_usdc + label: SPYx/USDC + value: 8fseq2pHJCG1W4JFDAwgZp42BnaTWEDBVMYXwWBkGNY8 + description: >- + Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. + metadata: + base_mint: XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 8 + quote_decimals: 6 + freshness_limit_slots: 25 + - id: trump_usdc + label: TRUMP/USDC + value: FwxwM23qYD8qNR6LRVFSUX7eDfeFF3xK9NbpSmRCQy8f + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: 6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: usd1_usdc + label: USD1/USDC + value: CtPSM9bhrCz1RQrRVTpwXQLcdo82eX1nahSfq1fo4qwF + description: >- + Rejects a quote at age 55; pass lead -55 to tessera-stale-quote. + metadata: + base_mint: USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 55 + - id: useless_usdc + label: USELESS/USDC + value: BDqQBspbipXxnTX2kw4FPM9pzfcf9kwieGCy4yUZ9tCC + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: wbtc_usdc + label: WBTC/USDC + value: 2qHQyHwugThrRBYAFxV3ANuRWEWHskSjGhwr73Ua6heb + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: 3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 8 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: weth_usdc + label: WETH/USDC + value: Ce8WKGKeNPrtk85inFtkpskekaNibZiogSZBrcP7yhTN + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: 7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 8 + quote_decimals: 6 + freshness_limit_slots: 20 + - id: wlfi_usdc + label: WLFI/USDC + value: 8G6QssZGNGpc54bH59pWn621xgCP1GzWmWV39NpvrhAH + description: >- + Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. + metadata: + base_mint: WLFinEv6ypjkczcS83FZqFpgFZYwQXutRbxGe7oC16g + quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + base_decimals: 6 + quote_decimals: 6 + freshness_limit_slots: 20 + +templates: + - id: tessera-fair-value + name: Override Tessera Fair Value + description: Move a Tessera market's reference price atomically in both directions + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: quote_atoms_per_base_atom_x1e15 + offset: 128 + encoding: u64 + label: Quote per base + description: "Quote atomic units per base atomic unit multiplied by 10^15. Use the builder to derive this from mint decimals." + - path: base_atoms_per_quote_atom_x1e15 + offset: 144 + encoding: u64 + label: Base per quote + description: "Base atomic units per quote atomic unit multiplied by 10^15. Use the builder to derive the reciprocal atomically." + llm_context: | + SET BOTH FIELDS AS ONE INVARIANT. Their product is approximately 10^30, with integer-floor + rounding in the reciprocal field. Changing only offset 128 moves base-to-quote sells but + leaves quote-to-base buys unchanged; changing only offset 144 does the opposite. + + Use the Tessera fair-value builder when starting from a human price. It calculates both exact + integer fields from the selected market's mint decimals and rejects zero or overflow. If + composing the raw template directly, use decimal integer strings rather than JSON numbers. + + Tessera rejects a quote once it reaches the market's freshness limit, with program error + 0xffff. Pair long-running scenarios with tessera-freshness. + + EXAMPLE - WSOL/USDC at 100 quote tokens per base token: + quote_atoms_per_base_atom_x1e15: "100000000000000" + base_atoms_per_quote_atom_x1e15: "10000000000000000" + + - id: tessera-depth + name: Override Tessera Depth + description: Change the directional capacities of Tessera's sell and buy ladders + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: sell_levels.0.amount + offset: 160 + encoding: u64 + label: Sell level 1 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.1.amount + offset: 184 + encoding: u64 + label: Sell level 2 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.2.amount + offset: 208 + encoding: u64 + label: Sell level 3 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.3.amount + offset: 232 + encoding: u64 + label: Sell level 4 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.4.amount + offset: 256 + encoding: u64 + label: Sell level 5 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.5.amount + offset: 280 + encoding: u64 + label: Sell level 6 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.6.amount + offset: 304 + encoding: u64 + label: Sell level 7 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.7.amount + offset: 328 + encoding: u64 + label: Sell level 8 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.8.amount + offset: 352 + encoding: u64 + label: Sell level 9 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.9.amount + offset: 376 + encoding: u64 + label: Sell level 10 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.10.amount + offset: 400 + encoding: u64 + label: Sell level 11 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.11.amount + offset: 424 + encoding: u64 + label: Sell level 12 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.12.amount + offset: 448 + encoding: u64 + label: Sell level 13 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.13.amount + offset: 472 + encoding: u64 + label: Sell level 14 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.14.amount + offset: 496 + encoding: u64 + label: Sell level 15 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.15.amount + offset: 520 + encoding: u64 + label: Sell level 16 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.16.amount + offset: 544 + encoding: u64 + label: Sell level 17 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.17.amount + offset: 568 + encoding: u64 + label: Sell level 18 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.18.amount + offset: 592 + encoding: u64 + label: Sell level 19 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: sell_levels.19.amount + offset: 616 + encoding: u64 + label: Sell level 20 capacity + description: Base-to-quote directional capacity in base atomic units. + - path: buy_levels.0.amount + offset: 640 + encoding: u64 + label: Buy level 1 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.1.amount + offset: 664 + encoding: u64 + label: Buy level 2 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.2.amount + offset: 688 + encoding: u64 + label: Buy level 3 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.3.amount + offset: 712 + encoding: u64 + label: Buy level 4 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.4.amount + offset: 736 + encoding: u64 + label: Buy level 5 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.5.amount + offset: 760 + encoding: u64 + label: Buy level 6 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.6.amount + offset: 784 + encoding: u64 + label: Buy level 7 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.7.amount + offset: 808 + encoding: u64 + label: Buy level 8 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.8.amount + offset: 832 + encoding: u64 + label: Buy level 9 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.9.amount + offset: 856 + encoding: u64 + label: Buy level 10 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.10.amount + offset: 880 + encoding: u64 + label: Buy level 11 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.11.amount + offset: 904 + encoding: u64 + label: Buy level 12 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.12.amount + offset: 928 + encoding: u64 + label: Buy level 13 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.13.amount + offset: 952 + encoding: u64 + label: Buy level 14 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.14.amount + offset: 976 + encoding: u64 + label: Buy level 15 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.15.amount + offset: 1000 + encoding: u64 + label: Buy level 16 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.16.amount + offset: 1024 + encoding: u64 + label: Buy level 17 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.17.amount + offset: 1048 + encoding: u64 + label: Buy level 18 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.18.amount + offset: 1072 + encoding: u64 + label: Buy level 19 capacity + description: Quote-to-base directional capacity in quote atomic units. + - path: buy_levels.19.amount + offset: 1096 + encoding: u64 + label: Buy level 20 capacity + description: Quote-to-base directional capacity in quote atomic units. + llm_context: | + These are twenty directional capacity fields, not token vault balances. Do not assume they + are monotonic cumulative breakpoints. To make one direction shallower, scale the currently + enabled amount fields for that direction by the same ratio. Lower sell_levels for a large + base-to-quote sell and buy_levels for a large quote-to-base buy. The opposite ladder is + behaviorally inactive for that direction. + + Small trades may not reveal a depth change. The live-fork proof uses large trades and shows + that dividing all active-side capacities by ten worsens the fill, while changing the + inactive-side capacities leaves it unchanged. + + Do not invent missing levels or enable disabled levels. Fetch the live account first and copy + all twenty current values before applying a uniform ratio only to enabled levels. A ratio that + rounds a live nonzero capacity down to zero leaves the level flagged enabled with nothing + behind it, which is not a state the market produces on its own; raise the ratio instead. + + - id: tessera-curve + name: Override Tessera Curve + description: Scale the directional output factors while preserving the live ladder ordering + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: sell_levels.0.factor + offset: 168 + encoding: u64 + label: Sell level 1 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.1.factor + offset: 192 + encoding: u64 + label: Sell level 2 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.2.factor + offset: 216 + encoding: u64 + label: Sell level 3 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.3.factor + offset: 240 + encoding: u64 + label: Sell level 4 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.4.factor + offset: 264 + encoding: u64 + label: Sell level 5 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.5.factor + offset: 288 + encoding: u64 + label: Sell level 6 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.6.factor + offset: 312 + encoding: u64 + label: Sell level 7 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.7.factor + offset: 336 + encoding: u64 + label: Sell level 8 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.8.factor + offset: 360 + encoding: u64 + label: Sell level 9 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.9.factor + offset: 384 + encoding: u64 + label: Sell level 10 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.10.factor + offset: 408 + encoding: u64 + label: Sell level 11 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.11.factor + offset: 432 + encoding: u64 + label: Sell level 12 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.12.factor + offset: 456 + encoding: u64 + label: Sell level 13 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.13.factor + offset: 480 + encoding: u64 + label: Sell level 14 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.14.factor + offset: 504 + encoding: u64 + label: Sell level 15 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.15.factor + offset: 528 + encoding: u64 + label: Sell level 16 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.16.factor + offset: 552 + encoding: u64 + label: Sell level 17 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.17.factor + offset: 576 + encoding: u64 + label: Sell level 18 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.18.factor + offset: 600 + encoding: u64 + label: Sell level 19 output factor + description: Base-to-quote directional output factor. + - path: sell_levels.19.factor + offset: 624 + encoding: u64 + label: Sell level 20 output factor + description: Base-to-quote directional output factor. + - path: buy_levels.0.factor + offset: 648 + encoding: u64 + label: Buy level 1 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.1.factor + offset: 672 + encoding: u64 + label: Buy level 2 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.2.factor + offset: 696 + encoding: u64 + label: Buy level 3 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.3.factor + offset: 720 + encoding: u64 + label: Buy level 4 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.4.factor + offset: 744 + encoding: u64 + label: Buy level 5 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.5.factor + offset: 768 + encoding: u64 + label: Buy level 6 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.6.factor + offset: 792 + encoding: u64 + label: Buy level 7 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.7.factor + offset: 816 + encoding: u64 + label: Buy level 8 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.8.factor + offset: 840 + encoding: u64 + label: Buy level 9 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.9.factor + offset: 864 + encoding: u64 + label: Buy level 10 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.10.factor + offset: 888 + encoding: u64 + label: Buy level 11 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.11.factor + offset: 912 + encoding: u64 + label: Buy level 12 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.12.factor + offset: 936 + encoding: u64 + label: Buy level 13 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.13.factor + offset: 960 + encoding: u64 + label: Buy level 14 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.14.factor + offset: 984 + encoding: u64 + label: Buy level 15 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.15.factor + offset: 1008 + encoding: u64 + label: Buy level 16 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.16.factor + offset: 1032 + encoding: u64 + label: Buy level 17 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.17.factor + offset: 1056 + encoding: u64 + label: Buy level 18 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.18.factor + offset: 1080 + encoding: u64 + label: Buy level 19 output factor + description: Quote-to-base directional output factor. + - path: buy_levels.19.factor + offset: 1104 + encoding: u64 + label: Buy level 20 output factor + description: Quote-to-base directional output factor. + llm_context: | + These forty fields control the directional output curve. Scaling every nonzero factor on the + active side by one half produces approximately one half of the baseline output on six live + markets, while scaling the inactive side leaves output unchanged. + + Read the live account first and copy all twenty current factors per direction, then apply one + ratio to every nonzero factor on the side you are stressing. Two rules are not optional. The + factors descend across the ladder, and a single factor that breaks that order is rejected by + the deployed program with custom error 8. A ratio that rounds a live nonzero factor down to + zero disables a level that the market has enabled. Check both before you write, and do not + edit one factor independently. + + - id: tessera-halt + name: Halt Tessera Liquidity + description: Disable the first required level on both directional ladders + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: sell_level_0_enabled + offset: 176 + encoding: u8 + label: Sell liquidity enabled + description: Set to zero to make base-to-quote quotes fail deterministically. + - path: buy_level_0_enabled + offset: 656 + encoding: u8 + label: Buy liquidity enabled + description: Set to zero to make quote-to-base quotes fail deterministically. + llm_context: | + SET BOTH FIELDS TO ZERO AS ONE INVARIANT. The current deployed program rejects either quote + direction with custom error 0xffff when its required first level is disabled. Use this as a + deterministic liquidity-outage scenario, not as a depth or price scenario. Do not persist it. + + - id: tessera-stale-quote + name: Make Tessera Quote Stale + description: Age a Tessera quote to its market's rejection boundary + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: last_update_slot + offset: 120 + encoding: + slot: + lead: -20 + label: Slot lead + description: >- + How far behind the materialization slot to place the quote, as a negative integer. + Pass null to use -20, the limit of most markets and of the default one. + llm_context: | + The value you pass IS the lead: Surfpool writes the materialization slot plus it, clamped at + zero. Pass null to take the -20 default. This is one template for every market, not one per + limit. + + Rejection is at age greater than or equal to the market's own limit, so the lead must be at + most minus that limit. Read freshness_limit_slots for the chosen address in the market + constant and negate it; offset 88 on the live account is the same number. As of the catalog's + capture date fourteen markets are at 20, eleven at 25 and one at 55, but pass whatever the + catalog says rather than assuming one of those three. + + At the boundary the current deployed program rejects the swap with custom error 65535, while + one slot younger succeeds. Do not persist this override: the quote should stay stale. + + Set fetchBeforeUse: true so the live market is forked first. Keep override labels short + ("SOL/USDC stale quote"). + + - id: tessera-freshness + name: Refresh Tessera Quote + description: Publish the materialization slot into Tessera's freshness field + idl_account_name: MarketState + address: + type: pubkey + value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n + properties: + - path: last_update_slot + offset: 120 + encoding: + slot: + lead: 0 + label: Current materialization slot + description: Slot lead, as an integer. Pass null to take the lead of zero and write the materialization slot itself. + llm_context: | + Tessera accepts a quote up to 19 slots old. At age 20 the current deployed program rejects the + swap with custom error 0xffff. This differs from BisonFi, which can silently return zero. + + Pass null for last_update_slot to take this template's lead of zero, which writes the exact + materialization slot. A number would be read as the lead instead, so passing 0 happens to mean + the same thing here and -5 would quietly age the quote by five slots. + + Use persist: true when the prepared state must remain executable beyond the market's freshness + window; each application then writes its own slot rather than repeating the first one. diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index a421a3f91..c343bbfcd 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -23,6 +23,9 @@ pub const METEORA_DLMM_OVERRIDES_CONTENT: &str = pub const KAMINO_V1_IDL_CONTENT: &str = include_str!("./protocols/kamino/v1/idl.json"); pub const KAMINO_V1_OVERRIDES_CONTENT: &str = include_str!("./protocols/kamino/v1/overrides.yaml"); +pub const TESSERA_V1_OVERRIDES_CONTENT: &str = + include_str!("./protocols/tessera/v1/overrides.yaml"); + pub const KAMINO_SCOPE_IDL_CONTENT: &str = include_str!("./protocols/kamino/scope/v1/idl.json"); pub const KAMINO_SCOPE_OVERRIDES_CONTENT: &str = include_str!("./protocols/kamino/scope/v1/overrides.yaml"); @@ -76,6 +79,7 @@ impl TemplateRegistry { default.load_raydium_overrides(); default.load_meteora_overrides(); default.load_kamino_overrides(); + default.load_tessera_overrides(); default.load_drift_overrides(); default.load_whirlpool_overrides(); default.load_spl_token_overrides(); @@ -116,6 +120,10 @@ impl TemplateRegistry { ); } + pub fn load_tessera_overrides(&mut self) { + self.load_raw_layout_overrides(TESSERA_V1_OVERRIDES_CONTENT, "tessera"); + } + pub fn load_kamino_overrides(&mut self) { self.load_protocol_overrides(KAMINO_V1_IDL_CONTENT, KAMINO_V1_OVERRIDES_CONTENT, "kamino"); @@ -516,11 +524,11 @@ mod tests { // Pyth (1) + Jupiter (1) + Raydium CLMM (1) + Raydium AMM v4 (4) + Drift (4) + Meteora (2) // + Kamino (Lend 17, Scope 3, Farms 5, Swap 2, Vault 5, Liquidity 4 = 36) - // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) = 62 + // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + Tessera (6) = 68 assert_eq!( registry.count(), - 62, - "Registry should load 62 templates total" + 68, + "Registry should load 68 templates total" ); assert!(registry.contains("pyth-price-feed-v2")); diff --git a/crates/core/src/tests/live.rs b/crates/core/src/tests/live.rs new file mode 100644 index 000000000..853cb2b77 --- /dev/null +++ b/crates/core/src/tests/live.rs @@ -0,0 +1,60 @@ +//! Shared plumbing for tests that read mainnet. +//! +//! Set `SURFPOOL_TEST_RPC_URL` to use a private endpoint if the public one rate-limits. + +use solana_account::Account; +use solana_commitment_config::CommitmentConfig; +use solana_pubkey::Pubkey; + +use crate::surfnet::remote::SurfnetRemoteClient; + +pub const RPC_URL_ENV: &str = "SURFPOOL_TEST_RPC_URL"; +pub const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com"; + +pub fn client() -> SurfnetRemoteClient { + SurfnetRemoteClient::new( + std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_RPC_URL.to_string()), + ) +} + +/// Fetches the accounts in one request, so every account returned is from the same slot. +pub async fn fetch(addresses: &[Pubkey]) -> Vec { + // The public endpoint throttles and intermittently 503s, which has nothing to do with what + // the callers assert. Retry a few times with backoff so a transient refusal is not read as a + // failure. + let mut attempt = 0; + let results = loop { + match client() + .get_multiple_accounts(addresses, CommitmentConfig::confirmed()) + .await + { + Ok(results) => break results, + Err(error) if attempt < 4 => { + attempt += 1; + tokio::time::sleep(std::time::Duration::from_millis(500 * attempt)).await; + let _ = error; + } + Err(error) => panic!("failed to fetch {addresses:?} from mainnet: {error}"), + } + }; + + results + .into_iter() + .zip(addresses) + .map(|(result, address)| { + result.map_account().unwrap_or_else(|_| { + panic!("{address} no longer exists on mainnet; the integration needs a new address") + }) + }) + .collect() +} + +/// The offsets at which two buffers differ. +pub fn diff_indices(left: &[u8], right: &[u8]) -> Vec { + left.iter() + .zip(right) + .enumerate() + .filter(|(_, (a, b))| a != b) + .map(|(index, _)| index) + .collect() +} diff --git a/crates/core/src/tests/mod.rs b/crates/core/src/tests/mod.rs index b2dd37925..cb11aa467 100644 --- a/crates/core/src/tests/mod.rs +++ b/crates/core/src/tests/mod.rs @@ -2,7 +2,11 @@ pub mod helpers; pub mod integration; #[cfg(feature = "integration-tests")] pub mod kamino; +#[cfg(feature = "integration-tests")] +pub mod live; pub mod plugin; #[cfg(feature = "integration-tests")] pub mod pump; pub mod simnet_events; +#[cfg(feature = "integration-tests")] +pub mod tessera; diff --git a/crates/core/src/tests/tessera/mod.rs b/crates/core/src/tests/tessera/mod.rs new file mode 100644 index 000000000..f1c4d0819 --- /dev/null +++ b/crates/core/src/tests/tessera/mod.rs @@ -0,0 +1,1369 @@ +//! Behavioral proofs for Tessera's raw market layout against the current deployed program. + +use std::collections::HashMap; + +use sha2::{Digest, Sha256}; +use solana_account::Account; +use solana_instruction::{AccountMeta, Instruction}; +use solana_program_runtime::{ + declare_process_instruction, solana_sbpf::program::BuiltinFunctionDefinition, +}; +use solana_pubkey::Pubkey; + +use crate::{ + scenarios::{ + TemplateRegistry, + protocols::tessera::v1::{TesseraMarket, build_tessera_fair_value_scenario}, + }, + surfnet::svm::SurfnetSvm, + tests::live, +}; + +const TESSERA_PROGRAM: &str = "TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH"; +const TESSERA_PROGRAMDATA: &str = "BzSXM6KLDpHQQChzr7Fdgbzwp8r8zRYWFFrHK2uZmDYV"; +const TESSERA_GLOBAL_STATE: &str = "8ekCy2jHHUbW2yeNGFWYJT9Hm9FW7SvZcZK66dSZCDiF"; +const TESSERA_SOL_USDC_MARKET: &str = "FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n"; +const TESSERA_CBB_USDC_MARKET: &str = "9NkuAWB4LgCVFV77omEkJEjXqgV5PGupwMTu3B3pBRhc"; +const TESSERA_CBB_VAULT: &str = "37hggNyT4Ec8GEcxMLrWrZyrMSSFMSiFT6VBayRYceZH"; +const CBB_MINT: &str = "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij"; +const JUPITER_PROGRAM: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; +const TESSERA_SOL_VAULT: &str = "5pVN5XZB8cYBjNLFrsBCPWkCQBan5K5Mq2dWGzwPgGJV"; +const TESSERA_USDC_VAULT: &str = "9t4P5wMwfFkyn92Z7hf463qYKEZf8ERVZsGBEPNp8uJx"; +const TESSERA_V11_SENTINEL: &str = "8xeaWCsJYxRoudEZGJWURdfrtFhLYZz9b4iHJnW5tb3d"; +const TESSERA_V11_CONFIG: &str = "BAT1Ndpu5gbLTp2AZkSXP79LJBZfCH4B3zGhi6LtvdhK"; +const TESSERA_V11_MARKET_RECORD: &str = "4cG31VNF9TzFinNc7BmnjhFvGjxkY3sCETVMtMgbrhPs"; +const WSOL_MINT: &str = "So11111111111111111111111111111111111111112"; +const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; +const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; +const DFLOW_PROGRAM: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"; +const CURRENT_DEPLOY_SLOT: u64 = 438_800_691; +const CURRENT_ELF_SHA256: &str = "433f2a857ffe2045310a478b4aca0fd824308d01f283719275baec60e2aecb3b"; +const MAX_PRICE_AGE_SLOTS: u64 = 19; +/// Where the deployed program reads each market's quote rejection age. +const FRESHNESS_LIMIT_OFFSET: usize = 88; + +#[derive(Clone, Copy)] +struct JupiterMarketSpec { + address: &'static str, + base_vault: &'static str, + quote_vault: &'static str, + base_mint: &'static str, + quote_mint: &'static str, + amount_in: u64, + direction: u8, +} + +const JUPITER_MARKETS: [JupiterMarketSpec; 5] = [ + JupiterMarketSpec { + address: TESSERA_CBB_USDC_MARKET, + base_vault: TESSERA_CBB_VAULT, + quote_vault: TESSERA_USDC_VAULT, + base_mint: CBB_MINT, + quote_mint: USDC_MINT, + amount_in: 125_853, + direction: 1, + }, + JupiterMarketSpec { + address: "5X9A6PpFQEsc9D5VdTGfgVyfVn8HnsArQpMMUZZfFg1a", + base_vault: "8FNRrFbq5APT6uZGH6U5DcMNo3U6SDpKoQD3CQMZ5RTU", + quote_vault: TESSERA_USDC_VAULT, + base_mint: "SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb", + quote_mint: USDC_MINT, + amount_in: 4_000_000, + direction: 0, + }, + JupiterMarketSpec { + address: "7sJf1SmKDDAFtBmMtg253rTbjG7zVFm3zTNounSgSNc9", + base_vault: TESSERA_SOL_VAULT, + quote_vault: "Ci3HZCb6fr5YiYLG9R6XbxHcjm2mDb1R3ugQ3bPZ7oKZ", + base_mint: WSOL_MINT, + quote_mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + amount_in: 77_868_902, + direction: 0, + }, + JupiterMarketSpec { + address: "Ce8WKGKeNPrtk85inFtkpskekaNibZiogSZBrcP7yhTN", + base_vault: "GYaM9Coc9gG4vzqTLRzGAZS6HFaBrebDohMUMYkyADRm", + quote_vault: TESSERA_USDC_VAULT, + base_mint: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + quote_mint: USDC_MINT, + amount_in: 3_931_067, + direction: 1, + }, + JupiterMarketSpec { + address: "DNhfyh75AApg1L1Yig3fErvERKutYRqfWLGb496iViSZ", + base_vault: "FhdiaEWUX8ZrW5TT2iNjWivMCzuBZhUJutrpfw6CvsxU", + quote_vault: TESSERA_USDC_VAULT, + base_mint: "pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn", + quote_mint: USDC_MINT, + amount_in: 8_702, + direction: 0, + }, +]; + +struct JupiterMarketFork { + spec: JupiterMarketSpec, + market: Account, + base_vault: Account, + quote_vault: Account, + base_mint: Account, + quote_mint: Account, +} + +struct TesseraFork { + elf: Vec, + global_state: Account, + market: Account, + base_vault: Account, + quote_vault: Account, + base_mint: Account, + quote_mint: Account, + sentinel: Account, + config: Account, + market_record: Account, + jupiter_markets: Vec, +} + +declare_process_instruction!(TesseraCpiWrapper, 1, |invoke_context| { + let instruction = { + let context = invoke_context + .transaction_context + .get_current_instruction_context()?; + let accounts = (1..context.get_number_of_instruction_accounts()) + .map(|index| { + Ok(AccountMeta { + pubkey: *context.get_key_of_instruction_account(index)?, + is_signer: context.is_instruction_account_signer(index)?, + is_writable: context.is_instruction_account_writable(index)?, + }) + }) + .collect::, solana_instruction::error::InstructionError>>()?; + Instruction { + program_id: Pubkey::from_str_const(TESSERA_PROGRAM), + accounts, + data: context.get_instruction_data().to_vec(), + } + }; + invoke_context.native_invoke_signed(instruction, &[]) +}); + +async fn fetch_accounts(addresses: &[&str]) -> Vec { + let pubkeys: Vec = addresses + .iter() + .map(|address| Pubkey::from_str_const(address)) + .collect(); + live::fetch(&pubkeys).await +} + +async fn tessera_fork() -> TesseraFork { + let (cbb, spcx, wsol_usdt, weth, pump) = tokio::join!( + fetch_jupiter_market(JUPITER_MARKETS[0]), + fetch_jupiter_market(JUPITER_MARKETS[1]), + fetch_jupiter_market(JUPITER_MARKETS[2]), + fetch_jupiter_market(JUPITER_MARKETS[3]), + fetch_jupiter_market(JUPITER_MARKETS[4]), + ); + let jupiter_markets = vec![cbb, spcx, wsol_usdt, weth, pump]; + let mut accounts = fetch_accounts(&[ + TESSERA_PROGRAMDATA, + TESSERA_GLOBAL_STATE, + TESSERA_SOL_USDC_MARKET, + TESSERA_SOL_VAULT, + TESSERA_USDC_VAULT, + WSOL_MINT, + USDC_MINT, + TESSERA_V11_SENTINEL, + TESSERA_V11_CONFIG, + TESSERA_V11_MARKET_RECORD, + ]) + .await; + let programdata = accounts.remove(0); + assert_eq!(programdata.data.len(), 576_977, "ProgramData size changed"); + assert_eq!( + u64::from_le_bytes(programdata.data[4..12].try_into().unwrap()), + CURRENT_DEPLOY_SLOT, + "Tessera was redeployed; revalidate the raw layout" + ); + let elf = programdata.data[45..].to_vec(); + assert_eq!( + hex::encode(Sha256::digest(&elf)), + CURRENT_ELF_SHA256, + "Tessera ELF changed without a ProgramData address change" + ); + + TesseraFork { + elf, + global_state: accounts.remove(0), + market: accounts.remove(0), + base_vault: accounts.remove(0), + quote_vault: accounts.remove(0), + base_mint: accounts.remove(0), + quote_mint: accounts.remove(0), + sentinel: accounts.remove(0), + config: accounts.remove(0), + market_record: accounts.remove(0), + jupiter_markets, + } +} + +async fn fetch_jupiter_market(spec: JupiterMarketSpec) -> JupiterMarketFork { + let mut accounts = fetch_accounts(&[ + spec.address, + spec.base_vault, + spec.quote_vault, + spec.base_mint, + spec.quote_mint, + ]) + .await; + JupiterMarketFork { + spec, + market: accounts.remove(0), + base_vault: accounts.remove(0), + quote_vault: accounts.remove(0), + base_mint: accounts.remove(0), + quote_mint: accounts.remove(0), + } +} + +fn token_account(mint: &Pubkey, owner: &Pubkey, amount: u64) -> Vec { + let mut data = vec![0u8; 165]; + data[0..32].copy_from_slice(mint.as_ref()); + data[32..64].copy_from_slice(owner.as_ref()); + data[64..72].copy_from_slice(&amount.to_le_bytes()); + data[108] = 1; + data +} + +fn native_token_account(mint: &Pubkey, owner: &Pubkey, amount: u64) -> Vec { + let mut data = token_account(mint, owner, amount); + data[109..113].copy_from_slice(&1u32.to_le_bytes()); + data[113..121].copy_from_slice(&2_039_280u64.to_le_bytes()); + data +} + +fn token_amount(data: &[u8]) -> u64 { + u64::from_le_bytes(data[64..72].try_into().unwrap()) +} + +fn token_owner(data: &[u8]) -> Pubkey { + Pubkey::new_from_array(data[32..64].try_into().expect("token owner")) +} + +fn tessera_run( + fork: &TesseraFork, + amount_in: u64, + direction: u8, + sentinel_signer: bool, + global_writable: bool, + mutate: impl FnOnce(&mut Vec), +) -> Result { + use litesvm::LiteSVM; + use solana_keypair::Keypair; + use solana_signer::Signer; + use solana_transaction::Transaction; + + let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); + let global_state_key = Pubkey::from_str_const(TESSERA_GLOBAL_STATE); + let market_key = Pubkey::from_str_const(TESSERA_SOL_USDC_MARKET); + let base_vault_key = Pubkey::from_str_const(TESSERA_SOL_VAULT); + let quote_vault_key = Pubkey::from_str_const(TESSERA_USDC_VAULT); + let base_mint_key = Pubkey::from_str_const(WSOL_MINT); + let quote_mint_key = Pubkey::from_str_const(USDC_MINT); + let token_program = Pubkey::from_str_const(TOKEN_PROGRAM); + let sentinel_key = Pubkey::from_str_const(TESSERA_V11_SENTINEL); + let config_key = Pubkey::from_str_const(TESSERA_V11_CONFIG); + let market_record_key = Pubkey::from_str_const(TESSERA_V11_MARKET_RECORD); + let mut market = fork.market.data.clone(); + let market_slot = u64::from_le_bytes(market[120..128].try_into().unwrap()); + mutate(&mut market); + + let mut svm = LiteSVM::new() + .with_sigverify(false) + .with_blockhash_check(false); + svm.add_program(program_id, &fork.elf) + .map_err(|error| format!("add_program: {error:?}"))?; + svm.add_builtin( + Pubkey::from_str_const(DFLOW_PROGRAM), + TesseraCpiWrapper::register, + ); + let mut clock: solana_clock::Clock = svm.get_sysvar(); + clock.slot = market_slot + 1; + clock.unix_timestamp = 1_787_551_143; + svm.set_sysvar(&clock); + svm.set_account( + Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), + Account { + lamports: 1_000_000, + data: 246_464_040u64.to_le_bytes().to_vec(), + owner: Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"), + executable: false, + rent_epoch: 0, + }, + ) + .map_err(|error| format!("set last restart slot: {error:?}"))?; + svm.set_account(global_state_key, fork.global_state.clone()) + .map_err(|error| format!("set global state: {error:?}"))?; + let mut market_account = fork.market.clone(); + market_account.data = market; + svm.set_account(market_key, market_account) + .map_err(|error| format!("set market: {error:?}"))?; + svm.set_account(base_vault_key, fork.base_vault.clone()) + .map_err(|error| format!("set base vault: {error:?}"))?; + svm.set_account(quote_vault_key, fork.quote_vault.clone()) + .map_err(|error| format!("set quote vault: {error:?}"))?; + svm.set_account(base_mint_key, fork.base_mint.clone()) + .map_err(|error| format!("set base mint: {error:?}"))?; + svm.set_account(quote_mint_key, fork.quote_mint.clone()) + .map_err(|error| format!("set quote mint: {error:?}"))?; + svm.set_account(sentinel_key, fork.sentinel.clone()) + .map_err(|error| format!("set sentinel: {error:?}"))?; + svm.set_account(config_key, fork.config.clone()) + .map_err(|error| format!("set config: {error:?}"))?; + svm.set_account(market_record_key, fork.market_record.clone()) + .map_err(|error| format!("set market record: {error:?}"))?; + + let taker = Keypair::new(); + svm.airdrop(&taker.pubkey(), 10_000_000_000) + .map_err(|error| format!("airdrop: {error:?}"))?; + let source_key = Pubkey::new_unique(); + let destination_key = Pubkey::new_unique(); + let (source_mint, destination_mint) = if direction == 1 { + (base_mint_key, quote_mint_key) + } else { + (quote_mint_key, base_mint_key) + }; + let (user_base_key, user_quote_key) = if direction == 1 { + (source_key, destination_key) + } else { + (destination_key, source_key) + }; + let user_account = |mint: &Pubkey, amount: u64| { + let is_native = mint == &base_mint_key; + Account { + lamports: if is_native { + amount.saturating_add(2_039_280) + } else { + 10_000_000 + }, + data: if is_native { + native_token_account(mint, &taker.pubkey(), amount) + } else { + token_account(mint, &taker.pubkey(), amount) + }, + owner: token_program, + executable: false, + rent_epoch: 0, + } + }; + svm.set_account(source_key, user_account(&source_mint, amount_in)) + .map_err(|error| format!("set source: {error:?}"))?; + svm.set_account(destination_key, user_account(&destination_mint, 0)) + .map_err(|error| format!("set destination: {error:?}"))?; + + let mut data = vec![0x11, direction]; + data.extend_from_slice(&amount_in.to_le_bytes()); + data.extend_from_slice(&0u64.to_le_bytes()); + data.push(0); + let mut budget = vec![2u8]; + budget.extend_from_slice(&1_400_000u32.to_le_bytes()); + let global_state_meta = if global_writable { + AccountMeta::new(global_state_key, false) + } else { + AccountMeta::new_readonly(global_state_key, false) + }; + let instructions = vec![ + Instruction { + program_id: Pubkey::from_str_const("ComputeBudget111111111111111111111111111111"), + accounts: vec![], + data: budget, + }, + Instruction { + program_id: Pubkey::from_str_const(DFLOW_PROGRAM), + accounts: vec![ + AccountMeta::new_readonly(program_id, false), + global_state_meta, + AccountMeta::new(market_key, false), + AccountMeta::new(taker.pubkey(), true), + AccountMeta::new(base_vault_key, false), + AccountMeta::new(quote_vault_key, false), + AccountMeta::new(user_base_key, false), + AccountMeta::new(user_quote_key, false), + AccountMeta::new_readonly(base_mint_key, false), + AccountMeta::new_readonly(quote_mint_key, false), + AccountMeta::new_readonly(token_program, false), + AccountMeta::new_readonly(token_program, false), + AccountMeta::new_readonly(sentinel_key, sentinel_signer), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(market_record_key, false), + ], + data, + }, + ]; + let mut message = solana_message::Message::new(&instructions, Some(&taker.pubkey())); + message.recent_blockhash = svm.latest_blockhash(); + let signature_count = message.header.num_required_signatures as usize; + let mut transaction = Transaction::new_unsigned(message); + transaction.signatures = vec![solana_signature::Signature::default(); signature_count]; + transaction.signatures[0] = taker.sign_message(&transaction.message.serialize()); + + svm.send_transaction(transaction) + .map_err(|error| format!("{error:?}"))?; + Ok(token_amount( + &svm.get_account(&destination_key) + .expect("destination account") + .data, + )) +} + +fn tessera_run_jupiter( + fork: &TesseraFork, + market_fork: &JupiterMarketFork, + amount_in: u64, + direction: u8, + mutate: impl FnOnce(&mut Vec), +) -> Result { + use litesvm::LiteSVM; + use solana_keypair::Keypair; + use solana_signer::Signer; + use solana_transaction::Transaction; + + let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); + let global_state_key = Pubkey::from_str_const(TESSERA_GLOBAL_STATE); + let market_key = Pubkey::from_str_const(market_fork.spec.address); + let base_vault_key = Pubkey::from_str_const(market_fork.spec.base_vault); + let quote_vault_key = Pubkey::from_str_const(market_fork.spec.quote_vault); + let base_mint_key = Pubkey::from_str_const(market_fork.spec.base_mint); + let quote_mint_key = Pubkey::from_str_const(market_fork.spec.quote_mint); + let base_token_program = market_fork.base_mint.owner; + let quote_token_program = market_fork.quote_mint.owner; + let instructions_sysvar = Pubkey::from_str_const("Sysvar1nstructions1111111111111111111111111"); + let mut market = market_fork.market.data.clone(); + let market_slot = read_u64(&market, 120); + mutate(&mut market); + + let mut svm = LiteSVM::new() + .with_sigverify(false) + .with_blockhash_check(false); + svm.add_program(program_id, &fork.elf) + .map_err(|error| format!("add_program: {error:?}"))?; + svm.add_builtin( + Pubkey::from_str_const(JUPITER_PROGRAM), + TesseraCpiWrapper::register, + ); + let mut clock: solana_clock::Clock = svm.get_sysvar(); + clock.slot = market_slot + 1; + clock.unix_timestamp = 1_787_662_925; + svm.set_sysvar(&clock); + svm.set_account( + Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), + Account { + lamports: 1_000_000, + data: 246_464_040u64.to_le_bytes().to_vec(), + owner: Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"), + executable: false, + rent_epoch: 0, + }, + ) + .map_err(|error| format!("set last restart slot: {error:?}"))?; + svm.set_account(global_state_key, fork.global_state.clone()) + .map_err(|error| format!("set global state: {error:?}"))?; + let mut market_account = market_fork.market.clone(); + market_account.data = market; + svm.set_account(market_key, market_account) + .map_err(|error| format!("set market: {error:?}"))?; + svm.set_account(base_vault_key, market_fork.base_vault.clone()) + .map_err(|error| format!("set base vault: {error:?}"))?; + svm.set_account(quote_vault_key, market_fork.quote_vault.clone()) + .map_err(|error| format!("set quote vault: {error:?}"))?; + svm.set_account(base_mint_key, market_fork.base_mint.clone()) + .map_err(|error| format!("set base mint: {error:?}"))?; + svm.set_account(quote_mint_key, market_fork.quote_mint.clone()) + .map_err(|error| format!("set quote mint: {error:?}"))?; + + let taker = Keypair::new(); + svm.airdrop(&taker.pubkey(), 10_000_000_000) + .map_err(|error| format!("airdrop: {error:?}"))?; + let source_key = Pubkey::new_unique(); + let destination_key = Pubkey::new_unique(); + let (source_mint, source_program, destination_mint, destination_program) = if direction == 1 { + ( + base_mint_key, + base_token_program, + quote_mint_key, + quote_token_program, + ) + } else { + ( + quote_mint_key, + quote_token_program, + base_mint_key, + base_token_program, + ) + }; + let (user_base_key, user_quote_key) = if direction == 1 { + (source_key, destination_key) + } else { + (destination_key, source_key) + }; + let user_account = |mint: &Pubkey, token_program: Pubkey, amount: u64| { + let is_native = mint == &Pubkey::from_str_const(WSOL_MINT); + Account { + lamports: if is_native { + amount.saturating_add(2_039_280) + } else { + 10_000_000 + }, + data: if is_native { + native_token_account(mint, &taker.pubkey(), amount) + } else { + token_account(mint, &taker.pubkey(), amount) + }, + owner: token_program, + executable: false, + rent_epoch: 0, + } + }; + svm.set_account( + source_key, + user_account(&source_mint, source_program, amount_in), + ) + .map_err(|error| format!("set source: {error:?}"))?; + svm.set_account( + destination_key, + user_account(&destination_mint, destination_program, 0), + ) + .map_err(|error| format!("set destination: {error:?}"))?; + + let mut data = vec![0x10, direction]; + data.extend_from_slice(&amount_in.to_le_bytes()); + data.extend_from_slice(&0u64.to_le_bytes()); + let instruction = Instruction { + program_id: Pubkey::from_str_const(JUPITER_PROGRAM), + accounts: vec![ + AccountMeta::new_readonly(program_id, false), + AccountMeta::new_readonly(global_state_key, false), + AccountMeta::new(market_key, false), + AccountMeta::new(taker.pubkey(), true), + AccountMeta::new(base_vault_key, false), + AccountMeta::new(quote_vault_key, false), + AccountMeta::new(user_base_key, false), + AccountMeta::new(user_quote_key, false), + AccountMeta::new_readonly(base_mint_key, false), + AccountMeta::new_readonly(quote_mint_key, false), + AccountMeta::new_readonly(base_token_program, false), + AccountMeta::new_readonly(quote_token_program, false), + AccountMeta::new_readonly(instructions_sysvar, false), + ], + data, + }; + let mut message = solana_message::Message::new(&[instruction], Some(&taker.pubkey())); + message.recent_blockhash = svm.latest_blockhash(); + let signature_count = message.header.num_required_signatures as usize; + let mut transaction = Transaction::new_unsigned(message); + transaction.signatures = vec![solana_signature::Signature::default(); signature_count]; + transaction.signatures[0] = taker.sign_message(&transaction.message.serialize()); + + svm.send_transaction(transaction) + .map_err(|error| format!("{error:?}"))?; + Ok(token_amount( + &svm.get_account(&destination_key) + .expect("destination account") + .data, + )) +} + +fn write_u64(data: &mut [u8], offset: usize, value: u64) { + data[offset..offset + 8].copy_from_slice(&value.to_le_bytes()); +} + +fn read_u64(data: &[u8], offset: usize) -> u64 { + u64::from_le_bytes(data[offset..offset + 8].try_into().expect("u64 field")) +} + +fn expected_first_level_output(market: &[u8], amount_in: u64, direction: u8) -> u64 { + let (price_offset, factor_offset) = match direction { + 0 => (144, 648), + 1 => (128, 168), + _ => panic!("unsupported Tessera direction {direction}"), + }; + let output = u128::from(amount_in) + .checked_mul(u128::from(read_u64(market, price_offset))) + .and_then(|value| value.checked_mul(u128::from(read_u64(market, factor_offset)))) + .expect("Tessera first-level quote multiplication") + / 1_000_000_000_000_000_000_000_u128; + u64::try_from(output).expect("Tessera first-level quote fits u64") +} + +/// Scales one side of a ladder the way a caller composing the raw template would. +/// +/// `first_offset` is the field's offset in level 0; records are 24 bytes apart. Returns the +/// template values for both directions, only one of which is actually scaled. +fn scale_ladder( + market: &[u8], + field: &str, + sell_bps: u16, + buy_bps: u16, +) -> HashMap { + const SELL_AMOUNT: usize = 160; + const BUY_AMOUNT: usize = 640; + const FACTOR_IN_RECORD: usize = 8; + + let base = if field == "factor" { + FACTOR_IN_RECORD + } else { + 0 + }; + let mut values = HashMap::with_capacity(LADDER_LEVELS * 2); + for (side, first_offset, bps) in [ + ("sell_levels", SELL_AMOUNT + base, sell_bps), + ("buy_levels", BUY_AMOUNT + base, buy_bps), + ] { + for level in 0..LADDER_LEVELS { + let live = read_u64(market, first_offset + level * LADDER_RECORD_SIZE); + let scaled = (u128::from(live) * u128::from(bps) / 10_000) as u64; + assert!( + live == 0 || scaled > 0, + "{side}.{level}.{field} rounds a live nonzero value to zero at {bps} bps" + ); + values.insert( + format!("{side}.{level}.{field}"), + serde_json::json!(scaled.to_string()), + ); + } + } + values +} + +const LADDER_LEVELS: usize = 20; +const LADDER_RECORD_SIZE: usize = 24; + +fn apply_template( + data: &mut Vec, + template_id: &str, + values: HashMap, + target_slot: u64, +) { + let registry = TemplateRegistry::new(); + let template = registry.get(template_id).expect("Tessera template"); + *data = template + .raw_layout + .as_ref() + .expect("Tessera raw layout") + .materialize(data, &template.properties, &values, target_slot) + .unwrap_or_else(|error| panic!("{template_id} did not materialize: {error}")); +} + +fn assert_only_ranges_changed(before: &[u8], after: &[u8], ranges: &[(usize, usize)]) { + assert_eq!(after.len(), before.len()); + for index in live::diff_indices(before, after) { + assert!( + ranges + .iter() + .any(|(start, end)| (*start..*end).contains(&index)), + "unexpected changed byte at {index}" + ); + } +} + +#[tokio::test] +async fn tessera_templates_guard_market_and_preserve_unwritten_bytes() { + let fork = tessera_fork().await; + let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); + assert_eq!(fork.market.owner, program_id); + assert_eq!(fork.market.data.len(), 1264); + let vault_authority = token_owner(&fork.base_vault.data); + assert_eq!(token_owner(&fork.quote_vault.data), vault_authority); + for market in &fork.jupiter_markets { + assert_eq!(token_owner(&market.base_vault.data), vault_authority); + assert_eq!(token_owner(&market.quote_vault.data), vault_authority); + } + eprintln!("Tessera shared vault authority: {vault_authority}"); + + let registry = TemplateRegistry::new(); + let fair_value = registry.get("tessera-fair-value").expect("fair value"); + let layout = fair_value.raw_layout.as_ref().expect("raw layout"); + assert!(layout.guard(&fork.market.data).is_ok()); + for market in &fork.jupiter_markets { + assert_eq!(market.market.owner, program_id); + assert_eq!(market.market.data.len(), 1264); + assert!(layout.guard(&market.market.data).is_ok()); + } + let mut wrong_layout_tag = fork.market.data.clone(); + wrong_layout_tag[96] ^= 1; + assert!(layout.guard(&wrong_layout_tag).is_err()); + assert!(layout.guard(&fork.market.data[..1263]).is_err()); + + let price_values = HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(100_000_000_000_000u64), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(10_000_000_000_000_000u64), + ), + ]); + let repriced = layout + .materialize(&fork.market.data, &fair_value.properties, &price_values, 0) + .expect("fair value materializes"); + assert_eq!( + u64::from_le_bytes(repriced[128..136].try_into().unwrap()), + 100_000_000_000_000 + ); + assert_eq!( + u64::from_le_bytes(repriced[144..152].try_into().unwrap()), + 10_000_000_000_000_000 + ); + assert_only_ranges_changed(&fork.market.data, &repriced, &[(128, 136), (144, 152)]); + + let template = registry.get("tessera-depth").expect("ladder template"); + let values: HashMap = template + .properties + .iter() + .map(|property| { + let offset = property.offset.expect("raw property offset"); + let current = + u64::from_le_bytes(fork.market.data[offset..offset + 8].try_into().unwrap()); + (property.path.clone(), serde_json::json!(current / 2)) + }) + .collect(); + let forged = template + .raw_layout + .as_ref() + .expect("raw layout") + .materialize(&fork.market.data, &template.properties, &values, 0) + .expect("depth materializes"); + let ranges: Vec<(usize, usize)> = template + .properties + .iter() + .map(|property| { + let offset = property.offset.expect("raw property offset"); + let expected = values[&property.path].as_u64().expect("u64 value"); + assert_eq!( + u64::from_le_bytes(forged[offset..offset + 8].try_into().unwrap()), + expected + ); + (offset, offset + 8) + }) + .collect(); + assert_only_ranges_changed(&fork.market.data, &forged, &ranges); + + let freshness = registry.get("tessera-freshness").expect("freshness"); + let refreshed = freshness + .raw_layout + .as_ref() + .expect("raw layout") + .materialize( + &fork.market.data, + &freshness.properties, + &HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + 987_654, + ) + .expect("freshness materializes"); + assert_eq!( + u64::from_le_bytes(refreshed[120..128].try_into().unwrap()), + 987_654 + ); + assert_only_ranges_changed(&fork.market.data, &refreshed, &[(120, 128)]); +} + +#[tokio::test] +async fn tessera_builder_scenario_materializes_atomically_and_keeps_quotes_fresh() { + const BASE_SLOT: u64 = 1_000_000; + + let fork = tessera_fork().await; + let market_key = Pubkey::from_str_const(TESSERA_SOL_USDC_MARKET); + let market = + TesseraMarket::validate(market_key, &fork.market, &fork.base_mint, &fork.quote_mint) + .expect("validate WSOL/USDC market"); + let preparation = build_tessera_fair_value_scenario(&market, "100.25") + .expect("build Tessera fair-value scenario"); + let original = fork.market.data.clone(); + let (mut svm, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::default(); + svm.inner + .set_account(market_key, fork.market) + .expect("seed Tessera market"); + svm.register_scenario(preparation.scenario, Some(BASE_SLOT)) + .expect("register Tessera scenario"); + + svm.materialize_overrides_for_slot(&None, BASE_SLOT) + .await + .expect("materialize Tessera scenario"); + let materialized = svm + .inner + .get_account(&market_key) + .expect("get Tessera market") + .expect("Tessera market present") + .data; + assert_eq!(read_u64(&materialized, 120), BASE_SLOT); + assert_eq!( + read_u64(&materialized, 128), + preparation.quote_atoms_per_base_atom_x1e15 + ); + assert_eq!( + read_u64(&materialized, 144), + preparation.base_atoms_per_quote_atom_x1e15 + ); + assert_only_ranges_changed( + &original, + &materialized, + &[(120, 128), (128, 136), (144, 152)], + ); + + svm.materialize_overrides_for_slot(&None, BASE_SLOT + 1) + .await + .expect("materialize persistent Tessera freshness"); + let next_slot = svm + .inner + .get_account(&market_key) + .expect("get Tessera market") + .expect("Tessera market present") + .data; + assert_eq!(read_u64(&next_slot, 120), BASE_SLOT + 1); + assert_eq!( + read_u64(&next_slot, 128), + preparation.quote_atoms_per_base_atom_x1e15 + ); + assert_eq!( + read_u64(&next_slot, 144), + preparation.base_atoms_per_quote_atom_x1e15 + ); + assert_only_ranges_changed(&materialized, &next_slot, &[(120, 128)]); +} + +#[tokio::test] +async fn tessera_stale_quote_template_lands_every_configured_rejection_boundary() { + let fork = tessera_fork().await; + let base_slot = read_u64(&fork.market.data, 120) + 1; + let amount_in = 238_781_608; + let baseline = tessera_run(&fork, amount_in, 1, true, false, |_| {}) + .expect("fresh quote must fill the control"); + assert!(baseline > 0); + + // One template covers every limit because the supplied value is the lead. Null takes the + // template's own -20, which is what the default market needs. + for (lead, age_slots) in [ + (serde_json::Value::Null, 20), + (serde_json::json!(-20), 20), + (serde_json::json!(-25), 25), + (serde_json::json!(-55), 55), + ] { + let mut configured = fork.market.clone(); + write_u64(&mut configured.data, 88, age_slots); + let original = configured.data.clone(); + + let mut staged = configured.data.clone(); + apply_template( + &mut staged, + "tessera-stale-quote", + HashMap::from([("last_update_slot".to_string(), lead.clone())]), + base_slot, + ); + assert_eq!( + read_u64(&staged, 120), + base_slot - age_slots, + "lead {lead} must write the materialization slot minus {age_slots}" + ); + assert_only_ranges_changed(&original, &staged, &[(88, 96), (120, 128)]); + + let stale = tessera_run(&fork, amount_in, 1, true, false, |market| { + *market = staged; + }) + .expect_err("a quote at the configured rejection age must be rejected"); + assert!(stale.contains("Custom(65535)"), "lead {lead}: {stale}"); + } + + // One slot younger than the boundary still fills, which is what makes the boundary a boundary. + let mut fresh_enough = fork.market.data.clone(); + apply_template( + &mut fresh_enough, + "tessera-stale-quote", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(-19))]), + base_slot, + ); + let accepted = tessera_run(&fork, amount_in, 1, true, false, |market| { + *market = fresh_enough; + }) + .expect("age 19 must still fill"); + assert!(accepted > 0); +} + +#[tokio::test] +async fn tessera_current_layout_controls_price_depth_and_freshness() { + let fork = tessera_fork().await; + assert_eq!(fork.market.data.len(), 1264, "market layout size changed"); + assert_eq!( + &fork.market.data[24..56], + Pubkey::from_str_const(WSOL_MINT).as_ref() + ); + assert_eq!( + &fork.market.data[56..88], + Pubkey::from_str_const(USDC_MINT).as_ref() + ); + + let amount_in = 238_781_608; + let baseline = tessera_run(&fork, amount_in, 1, true, false, |_| {}).expect("baseline sell"); + let inverse_only = tessera_run(&fork, amount_in, 1, true, false, |market| { + let inverse = u64::from_le_bytes(market[144..152].try_into().unwrap()); + write_u64(market, 144, inverse / 2); + }) + .expect("sell with buy-side-only mutation"); + let doubled = tessera_run(&fork, amount_in, 1, true, false, |market| { + let price = u64::from_le_bytes(market[128..136].try_into().unwrap()); + let inverse = u64::from_le_bytes(market[144..152].try_into().unwrap()); + apply_template( + market, + "tessera-fair-value", + HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(price * 2), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(inverse / 2), + ), + ]), + 0, + ); + }) + .expect("doubled-price swap"); + let buy_amount_in = 22_000_000; + let baseline_buy = + tessera_run(&fork, buy_amount_in, 0, true, false, |_| {}).expect("baseline buy"); + let direct_only_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { + let price = u64::from_le_bytes(market[128..136].try_into().unwrap()); + write_u64(market, 128, price * 2); + }) + .expect("buy with sell-side-only mutation"); + let doubled_price_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { + let price = u64::from_le_bytes(market[128..136].try_into().unwrap()); + let inverse = u64::from_le_bytes(market[144..152].try_into().unwrap()); + apply_template( + market, + "tessera-fair-value", + HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!(price * 2), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!(inverse / 2), + ), + ]), + 0, + ); + }) + .expect("doubled-price buy"); + let large_sell = 30_000_000_000; + let thin_sell_values = scale_ladder(&fork.market.data, "amount", 1_000, 10_000); + let thin_buy_values = scale_ladder(&fork.market.data, "amount", 10_000, 1_000); + let thin_sell_curve_values = scale_ladder(&fork.market.data, "factor", 5_000, 10_000); + let thin_buy_curve_values = scale_ladder(&fork.market.data, "factor", 10_000, 5_000); + let half_sell_curve = tessera_run(&fork, amount_in, 1, true, false, |market| { + apply_template(market, "tessera-curve", thin_sell_curve_values.clone(), 0); + }) + .expect("sell with half sell factors"); + let inactive_sell_curve = tessera_run(&fork, amount_in, 1, true, false, |market| { + apply_template(market, "tessera-curve", thin_buy_curve_values.clone(), 0); + }) + .expect("sell with half buy factors"); + let half_buy_curve = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { + apply_template(market, "tessera-curve", thin_buy_curve_values.clone(), 0); + }) + .expect("buy with half buy factors"); + let inactive_buy_curve = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { + apply_template(market, "tessera-curve", thin_sell_curve_values.clone(), 0); + }) + .expect("buy with half sell factors"); + let large_sell_baseline = + tessera_run(&fork, large_sell, 1, true, false, |_| {}).expect("large sell"); + let thin_sell = tessera_run(&fork, large_sell, 1, true, false, |market| { + apply_template(market, "tessera-depth", thin_sell_values.clone(), 0); + }) + .expect("large sell with thin sell ladder"); + let inactive_sell_depth = tessera_run(&fork, large_sell, 1, true, false, |market| { + apply_template(market, "tessera-depth", thin_buy_values.clone(), 0); + }) + .expect("large sell with buy-side depth mutation"); + let large_buy = 3_000_000_000; + let large_buy_baseline = + tessera_run(&fork, large_buy, 0, true, false, |_| {}).expect("large buy"); + let thin_buy = tessera_run(&fork, large_buy, 0, true, false, |market| { + apply_template(market, "tessera-depth", thin_buy_values.clone(), 0); + }) + .expect("large buy with thin buy ladder"); + let inactive_buy_depth = tessera_run(&fork, large_buy, 0, true, false, |market| { + apply_template(market, "tessera-depth", thin_sell_values.clone(), 0); + }) + .expect("large buy with sell-side depth mutation"); + let market_slot = u64::from_le_bytes(fork.market.data[120..128].try_into().unwrap()); + let clock_slot = market_slot + 1; + let fresh_at_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { + apply_template( + market, + "tessera-freshness", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + clock_slot.saturating_sub(MAX_PRICE_AGE_SLOTS), + ); + }); + let stale_after_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { + apply_template( + market, + "tessera-freshness", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + clock_slot.saturating_sub(MAX_PRICE_AGE_SLOTS + 1), + ); + }); + let configured_freshness_boundary = 5; + let fresh_at_configured_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { + write_u64(market, 88, configured_freshness_boundary); + apply_template( + market, + "tessera-freshness", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + clock_slot.saturating_sub(configured_freshness_boundary - 1), + ); + }); + let stale_at_configured_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { + write_u64(market, 88, configured_freshness_boundary); + apply_template( + market, + "tessera-freshness", + HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), + clock_slot.saturating_sub(configured_freshness_boundary), + ); + }); + let unsigned_sentinel = tessera_run(&fork, amount_in, 1, false, false, |_| {}) + .expect_err("unsigned DFlow sentinel must be rejected"); + let writable_global = tessera_run(&fork, amount_in, 1, true, true, |_| {}) + .expect_err("writable global state must be rejected"); + + let expected_first_level_sell_output = + expected_first_level_output(&fork.market.data, amount_in, 1); + let expected_first_level_buy_output = + expected_first_level_output(&fork.market.data, buy_amount_in, 0); + + eprintln!( + "Tessera sell={baseline}, expected_sell={expected_first_level_sell_output}, inverse_only={inverse_only}, doubled={doubled}, buy={baseline_buy}, expected_buy={expected_first_level_buy_output}, direct_only_buy={direct_only_buy}, doubled_price_buy={doubled_price_buy}, large_sell={large_sell_baseline}, thin_sell={thin_sell}, large_buy={large_buy_baseline}, thin_buy={thin_buy}, freshness_boundary={MAX_PRICE_AGE_SLOTS}, fresh={fresh_at_boundary:?}, stale={stale_after_boundary:?}" + ); + assert!( + baseline > 0, + "the current deployed program must fill the control" + ); + assert_eq!(baseline, expected_first_level_sell_output); + assert_eq!(baseline_buy, expected_first_level_buy_output); + assert!( + doubled > baseline * 19 / 10 && doubled < baseline * 21 / 10, + "atomic price override should approximately double the quote" + ); + assert_eq!( + inverse_only, baseline, + "the buy-side inverse must not affect a base-to-quote sell" + ); + assert!( + doubled_price_buy > baseline_buy * 4 / 10 && doubled_price_buy < baseline_buy * 6 / 10, + "doubling quote/base price must approximately halve base bought with quote" + ); + assert_eq!( + direct_only_buy, baseline_buy, + "the sell-side direct price must not affect a quote-to-base buy" + ); + assert!(unsigned_sentinel.contains("Custom(0)")); + assert!(writable_global.contains("Custom(1)")); + assert!(thin_sell < large_sell_baseline); + assert_eq!(inactive_sell_depth, large_sell_baseline); + assert!(thin_buy < large_buy_baseline); + assert_eq!(inactive_buy_depth, large_buy_baseline); + assert!(half_sell_curve > baseline * 49 / 100 && half_sell_curve < baseline * 51 / 100); + assert_eq!(inactive_sell_curve, baseline); + assert!(half_buy_curve > baseline_buy * 49 / 100 && half_buy_curve < baseline_buy * 51 / 100); + assert_eq!(inactive_buy_curve, baseline_buy); + assert!(fresh_at_boundary.is_ok()); + assert!( + stale_after_boundary + .expect_err("age 20 must be rejected") + .contains("Custom(65535)") + ); + assert!(fresh_at_configured_boundary.is_ok()); + assert!( + stale_at_configured_boundary + .expect_err("configured freshness boundary must reject") + .contains("Custom(65535)") + ); +} + +#[tokio::test] +async fn tessera_cbb_market_proves_generic_price_and_curve_layout() { + let fork = tessera_fork().await; + let cbb = &fork.jupiter_markets[0]; + let market_key = Pubkey::from_str_const(TESSERA_CBB_USDC_MARKET); + let market = TesseraMarket::validate(market_key, &cbb.market, &cbb.base_mint, &cbb.quote_mint) + .expect("validate CBB/USDC market"); + assert_eq!(market.base_mint, Pubkey::from_str_const(CBB_MINT)); + assert_eq!(market.quote_mint, Pubkey::from_str_const(USDC_MINT)); + assert_eq!(market.base_decimals, 8); + assert_eq!(market.quote_decimals, 6); + + let preparation = build_tessera_fair_value_scenario(&market, "78.8477010015472512") + .expect("build decimal-aware CBB fair value"); + assert_eq!(preparation.market, market_key); + assert_eq!( + preparation.quote_atoms_per_base_atom_x1e15, + 788_477_010_015_472 + ); + + let amount_in = cbb.spec.amount_in; + let active_curve_values = scale_ladder(&cbb.market.data, "factor", 5_000, 10_000); + let inactive_curve_values = scale_ladder(&cbb.market.data, "factor", 10_000, 5_000); + let baseline = + tessera_run_jupiter(&fork, cbb, amount_in, 1, |_| {}).expect("CBB baseline sell"); + let doubled = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + let direct = read_u64(market, 128); + let inverse = read_u64(market, 144); + apply_template( + market, + "tessera-fair-value", + HashMap::from([ + ( + "quote_atoms_per_base_atom_x1e15".to_string(), + serde_json::json!((direct * 2).to_string()), + ), + ( + "base_atoms_per_quote_atom_x1e15".to_string(), + serde_json::json!((inverse / 2).to_string()), + ), + ]), + 0, + ); + }) + .expect("CBB doubled-price sell"); + let factors_half = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + apply_template(market, "tessera-curve", active_curve_values.clone(), 0); + }) + .expect("CBB half-factor sell"); + let inactive_factors_half = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + apply_template(market, "tessera-curve", inactive_curve_values.clone(), 0); + }) + .expect("CBB inactive-factor control"); + let trailing_candidates_double = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + for offset in [1140usize, 1152, 1164, 1176, 1188] { + let value = u32::from_le_bytes(market[offset..offset + 4].try_into().unwrap()); + market[offset..offset + 4].copy_from_slice(&(value * 2).to_le_bytes()); + } + }) + .expect("CBB doubled trailing-candidate sell"); + let trailing_candidates_lower = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + for offset in [1140usize, 1152, 1164, 1176, 1188] { + market[offset..offset + 4].copy_from_slice(&999_999u32.to_le_bytes()); + } + }); + let invalid_single_factor = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + write_u64(market, 168, 500_000); + }) + .expect_err("a single unordered factor must be rejected"); + let disabled_first_level = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { + market[176] = 0; + }) + .expect_err("disabling the required first level must reject the quote"); + + assert!(baseline > 0); + assert_eq!( + baseline, + expected_first_level_output(&cbb.market.data, amount_in, 1) + ); + assert!(doubled > baseline * 19 / 10 && doubled < baseline * 21 / 10); + assert!(factors_half > baseline * 49 / 100 && factors_half < baseline * 51 / 100); + assert_eq!(inactive_factors_half, baseline); + assert_eq!(trailing_candidates_double, baseline); + assert_eq!(trailing_candidates_lower, Ok(baseline)); + assert!(invalid_single_factor.contains("Custom(8)")); + assert!(disabled_first_level.contains("Custom(65535)")); +} + +#[tokio::test] +async fn tessera_halt_template_rejects_both_quote_directions() { + let fork = tessera_fork().await; + let cbb = &fork.jupiter_markets[0]; + let apply_halt = |market: &mut Vec| { + apply_template( + market, + "tessera-halt", + HashMap::from([ + ("sell_level_0_enabled".to_string(), serde_json::json!(0)), + ("buy_level_0_enabled".to_string(), serde_json::json!(0)), + ]), + 0, + ); + }; + + let sell_amount = cbb.spec.amount_in; + let buy_amount = 4_000_000; + assert!(tessera_run_jupiter(&fork, cbb, sell_amount, 1, |_| {}).is_ok()); + assert!(tessera_run_jupiter(&fork, cbb, buy_amount, 0, |_| {}).is_ok()); + + let halted_sell = tessera_run_jupiter(&fork, cbb, sell_amount, 1, apply_halt) + .expect_err("halted sell direction must fail"); + let halted_buy = tessera_run_jupiter(&fork, cbb, buy_amount, 0, apply_halt) + .expect_err("halted buy direction must fail"); + assert!(halted_sell.contains("Custom(65535)")); + assert!(halted_buy.contains("Custom(65535)")); +} + +#[tokio::test] +async fn tessera_four_additional_markets_prove_price_and_curve_directions() { + let fork = tessera_fork().await; + for market_fork in &fork.jupiter_markets[1..] { + let market_key = Pubkey::from_str_const(market_fork.spec.address); + // Every one of these markets must clear the same owner, size and layout-tag guard. + TesseraMarket::validate( + market_key, + &market_fork.market, + &market_fork.base_mint, + &market_fork.quote_mint, + ) + .unwrap_or_else(|error| panic!("{} validation failed: {error}", market_fork.spec.address)); + + let direction = market_fork.spec.direction; + let amount_in = market_fork.spec.amount_in; + let baseline = tessera_run_jupiter(&fork, market_fork, amount_in, direction, |_| {}) + .unwrap_or_else(|error| { + panic!("{} baseline failed: {error}", market_fork.spec.address) + }); + let repriced = tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { + let direct = read_u64(market, 128); + let inverse = read_u64(market, 144); + write_u64(market, 128, direct * 2); + write_u64(market, 144, inverse / 2); + }) + .unwrap_or_else(|error| { + panic!("{} repriced swap failed: {error}", market_fork.spec.address) + }); + let (active_sell_bps, active_buy_bps) = if direction == 1 { + (5_000, 10_000) + } else { + (10_000, 5_000) + }; + let active_curve_values = scale_ladder( + &market_fork.market.data, + "factor", + active_sell_bps, + active_buy_bps, + ); + let inactive_curve_values = scale_ladder( + &market_fork.market.data, + "factor", + active_buy_bps, + active_sell_bps, + ); + let active_factors_half = + tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { + apply_template(market, "tessera-curve", active_curve_values.clone(), 0) + }) + .unwrap_or_else(|error| { + panic!( + "{} active-factor swap failed: {error}", + market_fork.spec.address + ) + }); + let inactive_factors_half = + tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { + apply_template(market, "tessera-curve", inactive_curve_values.clone(), 0) + }) + .unwrap_or_else(|error| { + panic!( + "{} inactive-factor swap failed: {error}", + market_fork.spec.address + ) + }); + + eprintln!( + "Tessera market={} direction={} baseline={} repriced={} active_factors_half={} inactive_factors_half={}", + market_fork.spec.address, + direction, + baseline, + repriced, + active_factors_half, + inactive_factors_half + ); + assert!(baseline > 0); + assert_eq!( + baseline, + expected_first_level_output(&market_fork.market.data, amount_in, direction) + ); + if direction == 1 { + assert!(repriced > baseline * 19 / 10 && repriced < baseline * 21 / 10); + } else { + assert!(repriced > baseline * 4 / 10 && repriced < baseline * 6 / 10); + } + assert!( + active_factors_half > baseline * 49 / 100 && active_factors_half < baseline * 51 / 100 + ); + assert_eq!(inactive_factors_half, baseline); + } +} + +#[tokio::test] +async fn tessera_catalog_matches_live_markets() { + let registry = TemplateRegistry::new(); + let template = registry + .get("tessera-fair-value") + .expect("Tessera fair-value template"); + let catalog = template + .constants + .get("market") + .expect("Tessera market catalog"); + + let addresses: Vec = catalog + .options + .iter() + .map(|option| { + option + .value + .parse() + .unwrap_or_else(|_| panic!("catalog entry {} is not a pubkey", option.id)) + }) + .collect(); + let markets = live::fetch(&addresses).await; + + let mut mints: Vec = Vec::with_capacity(addresses.len() * 2); + for market in &markets { + let (base, quote) = + TesseraMarket::mint_addresses(market).expect("every catalog entry is a live market"); + mints.extend([base, quote]); + } + mints.sort_unstable(); + mints.dedup(); + let mint_accounts = live::fetch(&mints).await; + + for (option, market_account) in catalog.options.iter().zip(&markets) { + let address: Pubkey = option.value.parse().expect("catalog pubkey"); + let (base, quote) = TesseraMarket::mint_addresses(market_account).expect("live market"); + let index = |mint: &Pubkey| mints.binary_search(mint).expect("fetched mint"); + let market = TesseraMarket::validate( + address, + market_account, + &mint_accounts[index(&base)], + &mint_accounts[index("e)], + ) + .unwrap_or_else(|error| panic!("{} failed validation: {error}", option.id)); + + let metadata = |key: &str| { + option + .metadata + .get(key) + .unwrap_or_else(|| panic!("{} has no {key}", option.id)) + }; + assert_eq!(metadata("base_mint"), &market.base_mint.to_string()); + assert_eq!(metadata("quote_mint"), &market.quote_mint.to_string()); + assert_eq!(metadata("base_decimals"), &market.base_decimals); + assert_eq!(metadata("quote_decimals"), &market.quote_decimals); + // The catalog's freshness limit is what picks the stale template, so it has to be the + // number the deployed program actually reads at offset 88. + assert_eq!( + metadata("freshness_limit_slots"), + &read_u64(&market_account.data, FRESHNESS_LIMIT_OFFSET) + ); + } +} diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 0ef4ebe29..37651d608 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -21,6 +21,8 @@ rmcp = { workspace = true, features = ["transport-io", "transport-sse-server", " serde = { workspace = true } serde_json = { workspace = true } serde_yaml = "0.9" +solana-account = { workspace = true } +solana-commitment-config = { workspace = true } solana-keypair = { workspace = true } solana-pubkey = { workspace = true } solana-signer = { workspace = true } diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index 7ce4c3e08..831158165 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -22,6 +22,7 @@ use surfpool_core::{ protocols::pump::v1::graduation_builder::{ build_pump_graduation_scenario, pump_graduation_addresses, }, + protocols::tessera::v1::{TesseraMarket, build_tessera_fair_value_scenario}, }, solana_account::Account, solana_commitment_config::CommitmentConfig, @@ -37,6 +38,29 @@ use crate::helpers::find_next_available_surfnet_port; mod set_token_account; mod start_surfnet; +fn scenario_tool_error(message: String) -> CallToolResult { + let response = RegisterScenarioResponse::error(message); + CallToolResult::success(vec![Content::text( + serde_json::to_string(&response).unwrap_or_default(), + )]) +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct CreateTesseraFairValueScenarioParams { + #[schemars( + description = "The Tessera market account. Resolve one through search_constant_options on the tessera-fair-value template's `market` constant; omit to use the default SOL/USDC market." + )] + pub market: Option, + #[schemars( + description = "The price of one base token in quote tokens, as a positive decimal string such as \"100.25\". Not atomic units: the builder derives the scale from the market's mint decimals." + )] + pub price: String, + #[schemars( + description = "The port of the target running local surfnet instance (e.g., 8899, 18899, 28899, etc.). Omit to use the default port, 8899." + )] + pub surfnet_port: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct StartSurfnetParams { #[schemars( @@ -332,13 +356,6 @@ impl RegisterScenarioResponse { } } -fn scenario_tool_error(message: String) -> CallToolResult { - let response = RegisterScenarioResponse::error(message); - CallToolResult::success(vec![Content::text( - serde_json::to_string(&response).unwrap_or_default(), - )]) -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct GetTokenAddressParams { #[schemars(description = "The token symbol to look up (e.g., 'USDC', 'SOL', 'JUP')")] @@ -399,6 +416,10 @@ impl Surfpool { .collect()) } + /// Posts a scenario to this process's own scenarios API and returns the editor link. + /// + /// The self-call uses the bind address; the advertised Studio URL is a different axis and + /// may not be reachable from here. async fn stage_scenario(&self, scenario: Scenario) -> Result { let endpoint = format!( "http://127.0.0.1:{}/v1/scenarios", @@ -413,49 +434,42 @@ impl Surfpool { { Ok(response) => response, Err(error) => { - let response = RegisterScenarioResponse::error(format!( + return Ok(scenario_tool_error(format!( "Failed to load scenarios at {endpoint}: {error}" - )); - let json = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json)])); + ))); } }; let status = response.status(); let body = match response.text().await { Ok(body) => body, Err(error) => { - let response = RegisterScenarioResponse::error(format!( + return Ok(scenario_tool_error(format!( "Failed to read response text: {error}" - )); - let json = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json)])); + ))); } }; - let response: serde_json::Value = match serde_json::from_str(&body) { - Ok(response) => response, + let parsed: serde_json::Value = match serde_json::from_str(&body) { + Ok(parsed) => parsed, Err(error) => { - let response = RegisterScenarioResponse::error(format!( + return Ok(scenario_tool_error(format!( "Failed to parse JSON response: {error}. Response: {body}" - )); - let json = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json)])); + ))); } }; + + // A different scenario already occupies this id: say so instead of + // reporting a success the model would trust if status == reqwest::StatusCode::CONFLICT { - let response = RegisterScenarioResponse::error(format!( + return Ok(scenario_tool_error(format!( "A different scenario is already stored under id {:?}. Pick another id, or delete the existing one first.", scenario.id - )); - let json = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json)])); + ))); } - if let Some(error) = response.get("error") { - let response = RegisterScenarioResponse::error(format!("RPC error: {error}")); - let json = serde_json::to_string(&response).unwrap_or_default(); - return Ok(CallToolResult::success(vec![Content::text(json)])); + if let Some(error) = parsed.get("error") { + return Ok(scenario_tool_error(format!("RPC error: {error}"))); } - let scenario_id = response + let scenario_id = parsed .get("id") .and_then(|value| value.as_str()) .unwrap_or(&scenario.id); @@ -464,8 +478,9 @@ impl Surfpool { CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED ); let response = RegisterScenarioResponse::success(url); - let json = serde_json::to_string(&response).unwrap_or_default(); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![Content::text( + serde_json::to_string(&response).unwrap_or_default(), + )])) } } @@ -1001,6 +1016,75 @@ impl Surfpool { self.stage_scenario(preparation.scenario).await } + #[tool( + description = "Creates one editable Tessera fair-value scenario for a live market. Reads the market and both mint accounts from the running surfnet, derives the pair of reciprocal atomic ratios from their decimals, and keeps the quote fresh while the scenario runs. Prepares state; sends no swap. Resolve `market` through search_constant_options on the tessera-fair-value template's `market` constant." + )] + async fn create_tessera_fair_value_scenario( + &self, + Parameters(params): Parameters, + ) -> Result { + let market_address = match params.market.as_deref().map(str::trim) { + None | Some("") => { + surfpool_core::scenarios::protocols::tessera::v1::TESSERA_DEFAULT_MARKET + } + Some(address) => match Pubkey::from_str(address) { + Ok(market) => market, + Err(error) => { + return Ok(scenario_tool_error(format!( + "Invalid Tessera market pubkey: {error}" + ))); + } + }, + }; + + let accounts = match self + .fetch_surfnet_accounts(params.surfnet_port, &[market_address]) + .await + { + Ok(accounts) => accounts, + Err(error) => return Ok(scenario_tool_error(error)), + }; + let Some(market_account) = accounts[0].as_ref() else { + return Ok(scenario_tool_error(format!( + "Tessera market account {market_address} was not found" + ))); + }; + let (base_mint, quote_mint) = match TesseraMarket::mint_addresses(market_account) { + Ok(mints) => mints, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + + let mints = match self + .fetch_surfnet_accounts(params.surfnet_port, &[base_mint, quote_mint]) + .await + { + Ok(mints) => mints, + Err(error) => return Ok(scenario_tool_error(error)), + }; + let (Some(base_account), Some(quote_account)) = (mints[0].as_ref(), mints[1].as_ref()) + else { + return Ok(scenario_tool_error(format!( + "Tessera market {market_address} references a mint that was not found" + ))); + }; + + let market = match TesseraMarket::validate( + market_address, + market_account, + base_account, + quote_account, + ) { + Ok(market) => market, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + let preparation = match build_tessera_fair_value_scenario(&market, ¶ms.price) { + Ok(preparation) => preparation, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + + self.stage_scenario(preparation.scenario).await + } + #[tool( description = "Fetches ALL available override templates. MUST be called before create_scenario to get valid templateId values and property names. Constants are summarized as {label, description, optionsCount} - resolve an actual option value with search_constant_options." )] @@ -1315,6 +1399,24 @@ mod tests { }) } + #[tokio::test] + async fn tessera_fair_value_rejects_a_bad_market_before_any_rpc() { + let surfpool = Surfpool::new(); + let result = surfpool + .create_tessera_fair_value_scenario(Parameters(CreateTesseraFairValueScenarioParams { + surfnet_port: None, + market: Some("not-a-pubkey".to_string()), + price: "100.25".to_string(), + })) + .await + .expect("the tool reports input errors in its payload, not as a protocol error"); + let text = format!("{:?}", result.content); + assert!( + text.contains("Invalid Tessera market pubkey"), + "unexpected payload: {text}" + ); + } + #[tokio::test] async fn get_override_templates_summarizes_constants_instead_of_inlining_options() { let surfpool = Surfpool::new(); From 74f9057bff71c84bbfc8d8fdb93767fb97b485e4 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 8 Sep 2026 17:11:05 +0300 Subject: [PATCH 2/8] feat(tessera): discover live markets and prepare depth stress Replace the static market catalog with on-chain discovery and add exact depth preparation with live swap coverage. Accept decimal u64/i64 overrides through the IDL encoder for lossless Studio editing. --- .../src/scenarios/protocols/tessera/README.md | 64 ++-- .../scenarios/protocols/tessera/v1/depth.rs | 172 ++++++++++ .../protocols/tessera/v1/fair_value.rs | 58 ++-- .../scenarios/protocols/tessera/v1/markets.rs | 87 +++++ .../src/scenarios/protocols/tessera/v1/mod.rs | 5 + .../protocols/tessera/v1/overrides.yaml | 318 +----------------- crates/core/src/surfnet/svm.rs | 27 ++ crates/core/src/tests/tessera/mod.rs | 229 ++++++++++--- crates/mcp/src/surfpool/mod.rs | 121 ++++++- 9 files changed, 667 insertions(+), 414 deletions(-) create mode 100644 crates/core/src/scenarios/protocols/tessera/v1/depth.rs create mode 100644 crates/core/src/scenarios/protocols/tessera/v1/markets.rs diff --git a/crates/core/src/scenarios/protocols/tessera/README.md b/crates/core/src/scenarios/protocols/tessera/README.md index 33ba18c65..898688a22 100644 --- a/crates/core/src/scenarios/protocols/tessera/README.md +++ b/crates/core/src/scenarios/protocols/tessera/README.md @@ -18,10 +18,9 @@ before trusting the templates again. ## The guard, and what it does not cover -A market is 1264 bytes with the eight-byte layout tag `05 00 00 00 00 00 00 00` at offset 96. All -26 live markets carry that tag, and no other Tessera account is 1264 bytes, so size alone already -separates markets from the program's other accounts today. The tag is the version half of the -guard: it is what rejects a future market layout that reuses the size. +A market is 1264 bytes with the eight-byte layout tag `05 00 00 00 00 00 00 00` at offset 96. +Discovery selects accounts with this size and tag, then validates the mint identities. The tag is +the version half of the guard: it is what rejects a future market layout that reuses the size. The shared raw-layout schema has no owner predicate, so a foreign account of the same size carrying the same eight bytes would pass a raw template. `validate_tessera_market_layout` adds the ownership @@ -57,39 +56,35 @@ with custom error 65535 on a market configured at 20. Both slot templates take the lead from the caller: the value supplied for `last_update_slot` is added to the materialization slot, and only `null` falls back to the template's own lead. One stale template therefore covers every market, including one configured at a limit nobody has seen yet. -Each market's limit travels with its address in the catalog, so a caller reads it there and passes -its negation. Passing a number where you meant the default is the one trap: `0` on the stale +`list_tessera_markets` returns each market's limit alongside its address; callers pass its negation. Passing a number where you meant the default is the one trap: `0` on the stale template writes a perfectly fresh quote. -## The market catalog +## Live market discovery -`v1/overrides.yaml` carries a `market` constant listing every live market with its mints, their -decimals, and its freshness limit. The UI constrains the choice to it and `search_constant_options` -resolves it for models. It is a snapshot, captured 2026-08-31 (26 markets: -fourteen with freshness limit 20, eleven at 25, one at 55): Tessera lists markets continuously, -a new one reaches the catalog on the next refresh, and until then the raw scenario API still -accepts its address directly. +`list_tessera_markets` queries Tessera program accounts through the selected Surfnet RPC. +It filters by the manifest's account size and layout tag, validates ownership and mint identities, +and reads decimals from the referenced mint accounts. The freshness limit comes from offset 88. +The existing Surfnet account resolver merges remote discovery with local accounts, preferring local +state. Discovery needs a datasource that supports `getProgramAccounts`; offline instances can list +only their local accounts. -Refresh it by reading the live set and rewriting the `options` block: +Studio loads this list when opening the fair-value dialog. The model uses the same tool to select +an override account and its freshness limit. There is no market list in `overrides.yaml`; the six +shared templates retain the SOL/USDC default address for callers that omit an account. -```bash -curl -s -X POST "$RPC_URL" -H 'Content-Type: application/json' -d '{ - "jsonrpc":"2.0","id":1,"method":"getProgramAccounts", - "params":["TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH", - {"encoding":"base64","commitment":"confirmed", - "filters":[{"dataSize":1264}], - "dataSlice":{"offset":24,"length":72}}]}' -``` +Labels use mint symbols from Surfpool's existing token metadata. An unknown mint is displayed by +its full address, so missing symbol metadata never hides a discovered market. Addresses are the +identities; symbols are not unique. Market membership, decimals and freshness limits are not taken +from the token metadata catalog. -Each result yields the base mint at `+0`, the quote mint at `+32`, and the freshness limit at -`+64`. Decimals come from the two mint accounts. `tessera_catalog_matches_live_markets` fails when -the catalog and the chain disagree, so a stale catalog is caught rather than shipped. +`tessera_discovers_live_markets` exercises the production discovery function and checks the returned +mint identities, decimals and freshness limits against fetched accounts. It does not pin a market +count, so newly listed markets are included without changing the test or templates. -## Builder and tool +## Builders and tools -One builder exists, for the one thing a template cannot express: turning a human price into the -pair of reciprocal atomic ratios, which needs both mints' decimals. It is a pure function over -account data. `create_tessera_fair_value_scenario` reads the market and both mints through the +The fair-value builder converts a human price into reciprocal atomic ratios using both mints' +decimals. It is a pure function over account data. `create_tessera_fair_value_scenario` reads the market and both mints through the surfnet's own RPC, so local state wins and only missing accounts fall back to the datasource, then stages the scenario through the shared path. @@ -100,8 +95,13 @@ Play-time refetch would reinstall remote bytes over any local edit and patch a d The paired freshness override is persisted. Its slot encoder writes the slot it materializes at, so the prepared price stays inside the market's freshness window however long the scenario runs. -Depth and curve have no builder here. Their templates expose every field, and the scaling helpers -that read a live ladder and preserve its ordering are parked until a product flow asks for them. +The `Tessera Depth Stress` AI chip requests a 90% reduction in both directions. The +`create_tessera_depth_scenario` tool reads current Surfnet state and takes remaining basis points +per direction: 1000 retains 10%, 10000 leaves that direction unchanged. It scales only enabled +capacities, with integer-floor rounding, and rejects zero capacities or increases. Prices, factors +and disabled levels are preserved. The scenario combines `tessera-depth` with persisted freshness; +depth itself is applied once. Creating another scenario reads the then-current state again. +Curve changes remain available through the raw template. ## Behavioral evidence @@ -111,7 +111,7 @@ matching direction, that active-side capacities and factors alter large fills wh side stays byte-for-byte identical, that first-level output in both directions equals the price-times-factor formula to the atom, that age 19 succeeds and age 20 fails with error 65535, that disabling both required first levels fails both directions, that an unordered single curve -factor fails with error 8, and that the catalog matches the live market set. +factor fails with error 8, and that live market discovery returns valid mint metadata. Run it serially. The public endpoint sheds queued requests right after a `getProgramAccounts` scan, sometimes as a 413 that looks like a request-size error: diff --git a/crates/core/src/scenarios/protocols/tessera/v1/depth.rs b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs new file mode 100644 index 000000000..a09dcf87a --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs @@ -0,0 +1,172 @@ +use std::collections::HashMap; + +use solana_account::Account; +use solana_pubkey::Pubkey; +use surfpool_types::{AccountAddress, OverrideInstance, Scenario}; + +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + scenarios::TemplateRegistry, +}; + +use super::TesseraMarket; + +pub fn build_tessera_depth_scenario( + market: Pubkey, + account: &Account, + sell_remaining_bps: u16, + buy_remaining_bps: u16, +) -> SurfpoolResult { + if [sell_remaining_bps, buy_remaining_bps] + .iter() + .any(|bps| !(1..=10_000).contains(bps)) + { + return Err(SurfpoolError::internal( + "Remaining depth must be 1..10000 basis points; 1000 keeps 10%, 10000 leaves a side unchanged", + )); + } + TesseraMarket::mint_addresses(account)?; + let registry = TemplateRegistry::new(); + let template = registry + .get("tessera-depth") + .expect("compiled Tessera template"); + let mut values = HashMap::new(); + for property in &template.properties { + let bps = if property.path.starts_with("sell_levels.") { + sell_remaining_bps + } else { + buy_remaining_bps + }; + let offset = property.offset.expect("Tessera capacity offset"); + // The enabled flag follows capacity and price factor in each 24-byte level. + if bps == 10_000 || account.data[offset + 16] == 0 { + continue; + } + let current = u64::from_le_bytes(account.data[offset..offset + 8].try_into().unwrap()); + let scaled = (u128::from(current) * u128::from(bps) / 10_000) as u64; + if scaled == 0 { + return Err(SurfpoolError::internal(format!( + "{} would have zero capacity while enabled; retain more depth", + property.path + ))); + } + values.insert(property.path.clone(), serde_json::json!(scaled.to_string())); + } + if values.is_empty() { + return Err(SurfpoolError::internal( + "No enabled levels selected for depth reduction", + )); + } + let percent = |bps: u16| format!("{}.{:02}%", bps / 100, bps % 100); + let mut scenario = Scenario::new( + "Tessera depth stress".to_string(), + format!( + "Keep {} of sell depth and {} of buy depth on market {market}, preserving prices and keeping quotes fresh.", + percent(sell_remaining_bps), + percent(buy_remaining_bps) + ), + ); + let target = AccountAddress::Pubkey(market.to_string()); + scenario.add_override( + OverrideInstance::new(template.id.clone(), 0, target.clone()) + .with_values(values) + .with_label("Reduce Tessera depth".to_string()), + ); + scenario.add_override( + OverrideInstance::new("tessera-freshness".to_string(), 0, target) + .with_values(HashMap::from([( + "last_update_slot".to_string(), + serde_json::Value::Null, + )])) + .with_label("Keep Tessera quote fresh".to_string()) + .with_persist(true), + ); + scenario.tags = vec![ + "tessera".to_string(), + "pmm".to_string(), + "depth-stress".to_string(), + ]; + Ok(scenario) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scenarios::protocols::tessera::v1::{TESSERA_PROGRAM_ID, fair_value::MARKET_LAYOUT}; + + fn market() -> Account { + let mut data = vec![0; MARKET_LAYOUT.account_size]; + let magic = MARKET_LAYOUT.magic.as_ref().unwrap(); + data[magic.offset..magic.offset + magic.bytes.len()].copy_from_slice(&magic.bytes); + data[24..56].copy_from_slice(Pubkey::new_unique().as_ref()); + data[56..88].copy_from_slice(Pubkey::new_unique().as_ref()); + for (offset, amount, enabled) in [(160, u64::MAX, 1), (184, 101, 0), (640, 12345, 1)] { + data[offset..offset + 8].copy_from_slice(&amount.to_le_bytes()); + data[offset + 16] = enabled; + } + Account { + data, + owner: TESSERA_PROGRAM_ID, + ..Account::default() + } + } + + #[test] + fn scales_exactly_and_preserves_unselected_bytes() { + let account = market(); + let address = Pubkey::new_unique(); + for (sell, buy, expected_sell, expected_buy) in [ + (5000, 10000, 9223372036854775807u64, 12345u64), + (1000, 2500, 1844674407370955161u64, 3086), + ] { + let scenario = build_tessera_depth_scenario(address, &account, sell, buy).unwrap(); + assert_eq!(scenario.overrides.len(), 2); + let mut actual = account.data.clone(); + let mut expected = actual.clone(); + expected[160..168].copy_from_slice(&expected_sell.to_le_bytes()); + expected[640..648].copy_from_slice(&expected_buy.to_le_bytes()); + expected[120..128].copy_from_slice(&42u64.to_le_bytes()); + let registry = TemplateRegistry::new(); + for instance in &scenario.overrides { + assert_eq!( + instance.account, + AccountAddress::Pubkey(address.to_string()) + ); + assert_eq!(instance.scenario_relative_slot, 0); + assert!(!instance.fetch_before_use); + let template = registry.get(&instance.template_id).unwrap(); + actual = template + .raw_layout + .as_ref() + .unwrap() + .materialize(&actual, &template.properties, &instance.values, 42) + .unwrap(); + } + assert_eq!(actual, expected); + assert!(!scenario.overrides[0].persist); + assert!(scenario.overrides[1].persist); + assert!( + scenario.overrides[0] + .values + .values() + .all(|value| value.is_string()) + ); + } + } + + #[test] + fn rejects_invalid_reductions_and_accounts() { + let mut account = market(); + let address = Pubkey::new_unique(); + for (sell, buy) in [(0, 10000), (10000, 10001), (10000, 10000)] { + assert!(build_tessera_depth_scenario(address, &account, sell, buy).is_err()); + } + account.data[160..168].copy_from_slice(&1u64.to_le_bytes()); + assert!(build_tessera_depth_scenario(address, &account, 1000, 10000).is_err()); + account.owner = Pubkey::new_unique(); + assert!(build_tessera_depth_scenario(address, &account, 1000, 1000).is_err()); + account.owner = TESSERA_PROGRAM_ID; + account.data.truncate(100); + assert!(build_tessera_depth_scenario(address, &account, 1000, 1000).is_err()); + } +} diff --git a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs index f3d78287c..6d5113dba 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs @@ -8,7 +8,10 @@ use std::{collections::HashMap, sync::LazyLock}; use solana_account::Account; use solana_pubkey::Pubkey; -use surfpool_types::{AccountAddress, OverrideInstance, OverrideTemplate, RawLayout, Scenario}; +use surfpool_types::{ + AccountAddress, OverrideInstance, OverrideTemplate, RawLayout, Scenario, + VERIFIED_TOKENS_BY_SYMBOL, +}; use crate::{ error::{SurfpoolError, SurfpoolResult}, @@ -27,7 +30,7 @@ const QUOTE_MINT_OFFSET: usize = 56; /// The size and layout tag a Tessera market must have, taken from the manifest the raw templates /// are written against so there is one definition of them. Built once; the manifest is compiled in. -static MARKET_LAYOUT: LazyLock = LazyLock::new(|| { +pub(super) static MARKET_LAYOUT: LazyLock = LazyLock::new(|| { template(&TemplateRegistry::new(), FAIR_VALUE_TEMPLATE) .and_then(|template| { template.raw_layout.clone().ok_or_else(|| { @@ -55,6 +58,7 @@ pub struct TesseraMarket { pub quote_mint: Pubkey, pub base_decimals: u8, pub quote_decimals: u8, + pub freshness_limit_slots: u64, } impl TesseraMarket { @@ -94,8 +98,25 @@ impl TesseraMarket { quote_mint, base_decimals, quote_decimals, + freshness_limit_slots: u64::from_le_bytes( + market_account.data[88..96].try_into().unwrap(), + ), }) } + + pub fn label(&self) -> String { + let symbol = |mint: &Pubkey| { + let address = mint.to_string(); + VERIFIED_TOKENS_BY_SYMBOL + .values() + .filter(|token| token.address == address) + .map(|token| token.symbol.as_str()) + .min() + .map(str::to_string) + .unwrap_or(address) + }; + format!("{}/{}", symbol(&self.base_mint), symbol(&self.quote_mint)) + } } /// Rejects an account that is not a Tessera market. @@ -131,7 +152,7 @@ pub fn build_tessera_fair_value_scenario( let registry = TemplateRegistry::new(); let fair_value = template(®istry, FAIR_VALUE_TEMPLATE)?; let freshness = template(®istry, FRESHNESS_TEMPLATE)?; - let market_name = market_display_name(fair_value, &market.address); + let market_name = market.label(); let target = AccountAddress::Pubkey(market.address.to_string()); // No fetch_before_use: these values were derived from the market account this scenario was @@ -254,22 +275,6 @@ fn checked_power_of_ten(exponent: u32) -> SurfpoolResult { .ok_or_else(|| invalid("price scale exceeds supported precision")) } -/// The catalog pair for a listed market ("SOL/USDC"), a shortened address for one it does not list. -fn market_display_name(template: &OverrideTemplate, market: &Pubkey) -> String { - let address = market.to_string(); - template - .constants - .get("market") - .and_then(|constant| { - constant - .options - .iter() - .find(|option| option.value == address) - }) - .map(|option| option.label.clone()) - .unwrap_or_else(|| format!("{}…{}", &address[..4], &address[address.len() - 4..])) -} - fn template<'a>(registry: &'a TemplateRegistry, id: &str) -> SurfpoolResult<&'a OverrideTemplate> { registry .get(id) @@ -326,6 +331,21 @@ mod tests { .expect("valid Tessera market") } + #[test] + fn reads_metadata_for_a_market_outside_the_token_catalog() { + let base = Pubkey::new_unique(); + let quote = Pubkey::new_unique(); + let address = Pubkey::new_unique(); + let mut account = market_account(&base, "e); + account.data[88..96].copy_from_slice(&37u64.to_le_bytes()); + let market = + TesseraMarket::validate(address, &account, &mint_account(8), &mint_account(6)).unwrap(); + assert_eq!(market.address, address); + assert_eq!((market.base_decimals, market.quote_decimals), (8, 6)); + assert_eq!(market.freshness_limit_slots, 37); + assert_eq!(market.label(), format!("{base}/{quote}")); + } + #[test] fn builds_atomic_fair_value_for_wsol_usdc_decimals() { let market = market(9, 6); diff --git a/crates/core/src/scenarios/protocols/tessera/v1/markets.rs b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs new file mode 100644 index 000000000..c1ba240ed --- /dev/null +++ b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +use solana_account::Account; +use solana_account_decoder::UiAccountEncoding; +use solana_client::{ + rpc_config::RpcAccountInfoConfig, + rpc_filter::{Memcmp, RpcFilterType}, +}; +use solana_commitment_config::CommitmentConfig; + +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + surfnet::remote::SurfnetRemoteClient, +}; + +use super::{TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, fair_value::MARKET_LAYOUT}; + +pub async fn discover_tessera_markets( + client: &SurfnetRemoteClient, +) -> SurfpoolResult> { + let mut filters = vec![RpcFilterType::DataSize(MARKET_LAYOUT.account_size as u64)]; + if let Some(magic) = &MARKET_LAYOUT.magic { + filters.push(RpcFilterType::Memcmp(Memcmp::new_raw_bytes( + magic.offset, + magic.bytes.clone(), + ))); + } + let accounts = client + .get_program_accounts( + &TESSERA_PROGRAM_ID, + RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + commitment: Some(CommitmentConfig::confirmed()), + ..Default::default() + }, + Some(filters), + ) + .await? + .into_result()?; + + let accounts = accounts + .into_iter() + .map(|(address, encoded)| { + let account: Account = encoded.to_account().ok_or_else(|| { + SurfpoolError::internal(format!("Could not decode Tessera market {address}")) + })?; + let mints = TesseraMarket::mint_addresses(&account)?; + Ok((address, account, mints)) + }) + .collect::>>()?; + + let mut mints = accounts + .iter() + .flat_map(|(_, _, (base, quote))| [*base, *quote]) + .collect::>(); + mints.sort_unstable(); + mints.dedup(); + let mut mint_accounts = HashMap::new(); + for batch in mints.chunks(100) { + let fetched = client + .get_multiple_accounts(batch, CommitmentConfig::confirmed()) + .await?; + for (address, account) in batch.iter().zip(fetched) { + mint_accounts.insert(*address, account.map_account()?); + } + } + + let mut markets = accounts + .iter() + .map(|(address, account, (base, quote))| { + let mint = |address| { + mint_accounts.get(address).ok_or_else(|| { + SurfpoolError::internal(format!("Tessera mint {address} was not found")) + }) + }; + TesseraMarket::validate(*address, account, mint(base)?, mint(quote)?) + }) + .collect::>>()?; + markets.sort_by_cached_key(|market| { + ( + market.address != TESSERA_DEFAULT_MARKET, + market.label(), + market.address, + ) + }); + Ok(markets) +} diff --git a/crates/core/src/scenarios/protocols/tessera/v1/mod.rs b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs index 37db50bb7..06e850948 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/mod.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs @@ -1,4 +1,9 @@ +mod depth; mod fair_value; +mod markets; + +pub use depth::build_tessera_depth_scenario; +pub use markets::discover_tessera_markets; pub use fair_value::{ TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraFairValuePreparation, TesseraMarket, diff --git a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml index 2f62f7454..28a241421 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml @@ -13,304 +13,6 @@ tags: - prop-amm - swap -constants: - # Every live Tessera market, captured 2026-08-31 by the documented refresh command in - # ../README.md. Tessera lists markets continuously: a new one reaches this catalog on the - # next refresh, and until then the raw scenario API still accepts its address directly. - # - # freshness_limit_slots is the age at which the deployed program rejects a quote. It picks - # the matching tessera-stale-quote template, which is why it travels with the address. - market: - label: Tessera Market - description: Select a live Tessera market account - options: - - id: sol_usdc - label: SOL/USDC - value: FLckHLGMJy5gEoXWwcE68Nprde1D4araK4TGLw4pQq2n - description: >- - Default market. Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: So11111111111111111111111111111111111111112 - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 9 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: 2z_usdc - label: 2Z/USDC - value: ETZYG4pXscxNchSobJHnN4aDrVNL45gscnzcxM7sEDKe - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: J6pQQ3FAcJQeWPPGppWRb4nM8jU3wLyYbRrLh7feMfvd - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 8 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: bonk_usdc - label: Bonk/USDC - value: 8B8KYcFPMjZyDtJ1BENsqhW3fEmMrB4ekoEQJiM6z3SC - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263 - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 5 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: cbbtc_usdc - label: cbBTC/USDC - value: 9NkuAWB4LgCVFV77omEkJEjXqgV5PGupwMTu3B3pBRhc - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 8 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: fartcoin_sol - label: Fartcoin/SOL - value: rJ554NghHHwuUXL2mGJJviGrpYk7P2qSGTruwwD5KKb - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: 9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump - quote_mint: So11111111111111111111111111111111111111112 - base_decimals: 6 - quote_decimals: 9 - freshness_limit_slots: 20 - - id: fartcoin_usdc - label: Fartcoin/USDC - value: F45HLDGN3mYBaJAKB1UGkx8Y7o2ruS5hgBox1Us6AZF9 - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: 9BB6NFEcjBCtnNLFko2FqVQBq8HHM13kCyYcdQbgpump - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: hype_usdc - label: HYPE/USDC - value: FvQds9kfSi7opoW7Lf3KcsgcuKfJZB2HdrVNkdLSLQbi - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: 98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 9 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: jlp_sol - label: JLP/SOL - value: H3chk8rgniKXnToGTdPUFieuHGLQQfBMXVbGp6bR1uMD - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: 27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4 - quote_mint: So11111111111111111111111111111111111111112 - base_decimals: 6 - quote_decimals: 9 - freshness_limit_slots: 25 - - id: jto_usdc - label: JTO/USDC - value: DoKKUBzWcv6TYg3vr6kvVzvadnibieYctse6oD6d7Hxs - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 9 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: jup_usdc - label: JUP/USDC - value: Drx7X5bUajSPY6m5f2QhD2CK8c3RBMKgueSpZgCgUzkP - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: launchcoin_usdc - label: LAUNCHCOIN/USDC - value: ESaTtQcbtqk3eLNUQvND9uuMKjqfEtgmzwCspr5EbALo - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: Ey59PH7Z4BFU4HjyKnyMdWt5GGN76KazTAwQihoUXRnk - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 9 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: met_usdc - label: MET/USDC - value: 9WSspMLnTfaec7whu4kG3peCCn4gWGBvKayamxDEndqR - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: mon_usdc - label: MON/USDC - value: E5Zmb4JQbHd4ThBXj1Fix9dMGsdszEGQ17txAHLWPwvS - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: CrAr4RRJMBVwRsZtT62pEhfA9H5utymC2mVx8e7FreP2 - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 8 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: mu_usdc - label: MU/USDC - value: FCVmVJbiwiHc1ndWvwshFyjPYSzyRN12YNKRQn3AirHA - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: MUxEsUKSMACyw5fZf68wxf5FLnZVhtU9CwH8uNNGay1 - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: pengu_usdc - label: PENGU/USDC - value: 9nyLnejbWhNvzhg6c4RvFW5ei9DT1G3X8MhNXCkYj2hQ - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: 2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: pump_sol - label: PUMP/SOL - value: AbfkmuyTqhGJkPLKM5tmAeUdthbyLqAh8bNwR8AumPpE - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn - quote_mint: So11111111111111111111111111111111111111112 - base_decimals: 6 - quote_decimals: 9 - freshness_limit_slots: 20 - - id: pump_usdc - label: PUMP/USDC - value: DNhfyh75AApg1L1Yig3fErvERKutYRqfWLGb496iViSZ - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: pumpCmXqMfrsAkQ5r49WcJnRayYRqmXz6ae8H7H9Dfn - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: sol_usdt - label: SOL/USDT - value: 7sJf1SmKDDAFtBmMtg253rTbjG7zVFm3zTNounSgSNc9 - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: So11111111111111111111111111111111111111112 - quote_mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB - base_decimals: 9 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: spcx_usdc - label: SPCX/USDC - value: 5X9A6PpFQEsc9D5VdTGfgVyfVn8HnsArQpMMUZZfFg1a - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: SPCXxcqXj6e5dJDVNovHN8744zkbhM2bYudU45BimGb - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: spyx_usdc - label: SPYx/USDC - value: 8fseq2pHJCG1W4JFDAwgZp42BnaTWEDBVMYXwWBkGNY8 - description: >- - Rejects a quote at age 25; pass lead -25 to tessera-stale-quote. - metadata: - base_mint: XsoCS1TfEyfFhfvj8EtZ528L3CaKBDBRqRapnBbDF2W - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 8 - quote_decimals: 6 - freshness_limit_slots: 25 - - id: trump_usdc - label: TRUMP/USDC - value: FwxwM23qYD8qNR6LRVFSUX7eDfeFF3xK9NbpSmRCQy8f - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: 6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: usd1_usdc - label: USD1/USDC - value: CtPSM9bhrCz1RQrRVTpwXQLcdo82eX1nahSfq1fo4qwF - description: >- - Rejects a quote at age 55; pass lead -55 to tessera-stale-quote. - metadata: - base_mint: USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 55 - - id: useless_usdc - label: USELESS/USDC - value: BDqQBspbipXxnTX2kw4FPM9pzfcf9kwieGCy4yUZ9tCC - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: wbtc_usdc - label: WBTC/USDC - value: 2qHQyHwugThrRBYAFxV3ANuRWEWHskSjGhwr73Ua6heb - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: 3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 8 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: weth_usdc - label: WETH/USDC - value: Ce8WKGKeNPrtk85inFtkpskekaNibZiogSZBrcP7yhTN - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: 7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 8 - quote_decimals: 6 - freshness_limit_slots: 20 - - id: wlfi_usdc - label: WLFI/USDC - value: 8G6QssZGNGpc54bH59pWn621xgCP1GzWmWV39NpvrhAH - description: >- - Rejects a quote at age 20; pass lead -20 to tessera-stale-quote. - metadata: - base_mint: WLFinEv6ypjkczcS83FZqFpgFZYwQXutRbxGe7oC16g - quote_mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - base_decimals: 6 - quote_decimals: 6 - freshness_limit_slots: 20 - templates: - id: tessera-fair-value name: Override Tessera Fair Value @@ -331,6 +33,7 @@ templates: label: Base per quote description: "Base atomic units per quote atomic unit multiplied by 10^15. Use the builder to derive the reciprocal atomically." llm_context: | + Discover markets with list_tessera_markets and set the override account to the chosen address. SET BOTH FIELDS AS ONE INVARIANT. Their product is approximately 10^30, with integer-floor rounding in the reciprocal field. Changing only offset 128 moves base-to-quote sells but leaves quote-to-base buys unchanged; changing only offset 144 does the opposite. @@ -555,7 +258,11 @@ templates: label: Buy level 20 capacity description: Quote-to-base directional capacity in quote atomic units. llm_context: | - These are twenty directional capacity fields, not token vault balances. Do not assume they + Discover markets with list_tessera_markets and set the override account to the chosen address. + For percentage reductions, use create_tessera_depth_scenario, which reads the current Surfnet + account and scales enabled levels exactly. Pass remaining basis points: 1000 keeps 10%, + 10000 leaves a direction unchanged. It also adds persistent freshness. + These are directional capacities, not token vault balances. Do not assume they are monotonic cumulative breakpoints. To make one direction shallower, scale the currently enabled amount fields for that direction by the same ratio. Lower sell_levels for a large base-to-quote sell and buy_levels for a large quote-to-base buy. The opposite ladder is @@ -779,6 +486,7 @@ templates: label: Buy level 20 output factor description: Quote-to-base directional output factor. llm_context: | + Discover markets with list_tessera_markets and set the override account to the chosen address. These forty fields control the directional output curve. Scaling every nonzero factor on the active side by one half produces approximately one half of the baseline output on six live markets, while scaling the inactive side leaves output unchanged. @@ -809,6 +517,7 @@ templates: label: Buy liquidity enabled description: Set to zero to make quote-to-base quotes fail deterministically. llm_context: | + Discover markets with list_tessera_markets and set the override account to the chosen address. SET BOTH FIELDS TO ZERO AS ONE INVARIANT. The current deployed program rejects either quote direction with custom error 0xffff when its required first level is disabled. Use this as a deterministic liquidity-outage scenario, not as a depth or price scenario. Do not persist it. @@ -836,10 +545,9 @@ templates: limit. Rejection is at age greater than or equal to the market's own limit, so the lead must be at - most minus that limit. Read freshness_limit_slots for the chosen address in the market - constant and negate it; offset 88 on the live account is the same number. As of the catalog's - capture date fourteen markets are at 20, eleven at 25 and one at 55, but pass whatever the - catalog says rather than assuming one of those three. + most minus that limit. Call list_tessera_markets, select the market address, and negate its + freshnessLimitSlots. Set the override account to that address. Offset 88 on the market + account stores the same limit; do not assume all markets share a limit. At the boundary the current deployed program rejects the swap with custom error 65535, while one slot younger succeeds. Do not persist this override: the quote should stay stale. @@ -863,8 +571,8 @@ templates: label: Current materialization slot description: Slot lead, as an integer. Pass null to take the lead of zero and write the materialization slot itself. llm_context: | - Tessera accepts a quote up to 19 slots old. At age 20 the current deployed program rejects the - swap with custom error 0xffff. This differs from BisonFi, which can silently return zero. + Call list_tessera_markets and set the override account to the chosen address. The returned + freshnessLimitSlots is the age at which that market rejects quotes with custom error 0xffff. Pass null for last_update_slot to take this template's lead of zero, which writes the exact materialization slot. A number would be read as the lead instead, so passing 0 happens to mean diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 831432580..6ce850e07 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -313,6 +313,13 @@ fn json_to_txtx_value_for_idl_type( (IdlType::Option(inner), _) if !json.is_null() => { json_to_txtx_value_for_idl_type(json, inner, idl_types) } + (IdlType::U64 | IdlType::I64, serde_json::Value::String(_)) => { + let digits = json_integer_digits(json, "64-bit integer")?; + digits + .parse::() + .map(Value::Integer) + .map_err(|e| SurfpoolError::internal(format!("Invalid integer '{digits}': {e}"))) + } (IdlType::U128, _) => { let digits = json_integer_digits(json, "u128")?; let value = digits @@ -4607,6 +4614,26 @@ mod tests { use super::*; use crate::storage::tests::TestType; + #[test_case("18446744073709551615", IdlType::U64, u64::MAX.to_le_bytes().to_vec(); "u64 max")] + #[test_case("-9223372036854775808", IdlType::I64, i64::MIN.to_le_bytes().to_vec(); "i64 min")] + fn decimal_integer_override_encodes_exactly(digits: &str, ty: IdlType, expected: Vec) { + let value = json_to_txtx_value_for_idl_type(&serde_json::json!(digits), &ty, &[]).unwrap(); + let encoded = borsh_encode_value_to_idl_type(&value, &ty, &Vec::new(), None).unwrap(); + assert_eq!(encoded, expected); + } + + #[test_case("18446744073709551616", IdlType::U64; "u64 overflow")] + #[test_case("-1", IdlType::U64; "u64 negative")] + #[test_case("9223372036854775808", IdlType::I64; "i64 overflow")] + #[test_case("-9223372036854775809", IdlType::I64; "i64 underflow")] + #[test_case("1.5", IdlType::U64; "fraction")] + fn decimal_integer_override_rejects_invalid_values(digits: &str, ty: IdlType) { + let converted = json_to_txtx_value_for_idl_type(&serde_json::json!(digits), &ty, &[]); + if let Ok(value) = converted { + assert!(borsh_encode_value_to_idl_type(&value, &ty, &Vec::new(), None).is_err()); + } + } + #[test] fn startup_status_subscription_tracks_accepted_transitions() { use surfpool_types::SurfnetStartupPhase; diff --git a/crates/core/src/tests/tessera/mod.rs b/crates/core/src/tests/tessera/mod.rs index f1c4d0819..82f7e6c6a 100644 --- a/crates/core/src/tests/tessera/mod.rs +++ b/crates/core/src/tests/tessera/mod.rs @@ -13,7 +13,10 @@ use solana_pubkey::Pubkey; use crate::{ scenarios::{ TemplateRegistry, - protocols::tessera::v1::{TesseraMarket, build_tessera_fair_value_scenario}, + protocols::tessera::v1::{ + TesseraMarket, build_tessera_depth_scenario, build_tessera_fair_value_scenario, + discover_tessera_markets, + }, }, surfnet::svm::SurfnetSvm, tests::live, @@ -768,7 +771,7 @@ async fn tessera_templates_guard_market_and_preserve_unwritten_bytes() { } #[tokio::test] -async fn tessera_builder_scenario_materializes_atomically_and_keeps_quotes_fresh() { +async fn tessera_builders_materialize_and_keep_quotes_fresh() { const BASE_SLOT: u64 = 1_000_000; let fork = tessera_fork().await; @@ -829,6 +832,116 @@ async fn tessera_builder_scenario_materializes_atomically_and_keeps_quotes_fresh preparation.base_atoms_per_quote_atom_x1e15 ); assert_only_ranges_changed(&materialized, &next_slot, &[(120, 128)]); + + let local = svm.inner.get_account(&market_key).unwrap().unwrap(); + let depth = build_tessera_depth_scenario(market_key, &local, 1000, 10000).unwrap(); + svm.register_scenario(depth, Some(BASE_SLOT + 2)).unwrap(); + assert_eq!( + svm.inner.get_account(&market_key).unwrap().unwrap().data, + local.data + ); + svm.materialize_overrides_for_slot(&None, BASE_SLOT + 2) + .await + .unwrap(); + let reduced = svm.inner.get_account(&market_key).unwrap().unwrap().data; + let mut expected = local.data.clone(); + for level in 0..20 { + let offset = 160 + level * 24; + if local.data[offset + 16] != 0 { + write_u64(&mut expected, offset, read_u64(&local.data, offset) / 10); + } + } + write_u64(&mut expected, 120, BASE_SLOT + 2); + assert_eq!(reduced, expected); + svm.materialize_overrides_for_slot(&None, BASE_SLOT + 3) + .await + .unwrap(); + write_u64(&mut expected, 120, BASE_SLOT + 3); + assert_eq!( + svm.inner.get_account(&market_key).unwrap().unwrap().data, + expected + ); +} + +#[tokio::test] +async fn tessera_depth_builder_prepares_cbb_swaps_in_both_directions() { + let fork = tessera_fork().await; + let cbb = &fork.jupiter_markets[0]; + let address = Pubkey::from_str_const(cbb.spec.address); + let slot = read_u64(&cbb.market.data, 120) + 1; + for (sell_bps, buy_bps) in [(1000, 10000), (10000, 1000), (5000, 2500)] { + let scenario = + build_tessera_depth_scenario(address, &cbb.market, sell_bps, buy_bps).unwrap(); + let mut expected = cbb.market.data.clone(); + for (start, bps) in [(160, sell_bps), (640, buy_bps)] { + for level in 0..20 { + let offset = start + level * 24; + if expected[offset + 16] != 0 { + let capacity = read_u64(&cbb.market.data, offset); + write_u64( + &mut expected, + offset, + (u128::from(capacity) * u128::from(bps) / 10000) as u64, + ); + } + } + } + write_u64(&mut expected, 120, slot); + eprintln!( + "cbBTC depth sell_bps={sell_bps} buy_bps={buy_bps}: expected sell level 1 {} -> {}, buy level 1 {} -> {}", + read_u64(&cbb.market.data, 160), + read_u64(&expected, 160), + read_u64(&cbb.market.data, 640), + read_u64(&expected, 640) + ); + let (mut svm, _events, _geyser) = SurfnetSvm::default(); + svm.inner.set_account(address, cbb.market.clone()).unwrap(); + svm.register_scenario(scenario, Some(slot)).unwrap(); + assert_eq!( + svm.inner.get_account(&address).unwrap().unwrap(), + cbb.market + ); + svm.materialize_overrides_for_slot(&None, slot) + .await + .unwrap(); + let prepared = svm.inner.get_account(&address).unwrap().unwrap(); + let mut expected_account = cbb.market.clone(); + expected_account.data = expected; + assert_eq!(prepared, expected_account); + for (direction, offset, bps) in [(1, 160, sell_bps), (0, 640, buy_bps)] { + let first_capacity = read_u64(&cbb.market.data, offset); + let small_input = first_capacity / 20; + let large_input = first_capacity.checked_mul(2).unwrap(); + assert!(small_input > 0); + let small_expected = + expected_first_level_output(&cbb.market.data, small_input, direction); + let small_before = + tessera_run_jupiter(&fork, cbb, small_input, direction, |_| {}).unwrap(); + let small_after = tessera_run_jupiter(&fork, cbb, small_input, direction, |data| { + *data = prepared.data.clone() + }) + .unwrap(); + assert_eq!(small_before, small_expected); + assert_eq!(small_after, small_expected); + let large_before = + tessera_run_jupiter(&fork, cbb, large_input, direction, |_| {}).unwrap(); + let large_after = tessera_run_jupiter(&fork, cbb, large_input, direction, |data| { + *data = prepared.data.clone() + }) + .unwrap(); + if bps == 10000 { + assert_eq!(large_after, large_before); + } else { + assert!( + large_after > 0 && large_after < large_before, + "reduced depth must worsen a large fill: {large_before} -> {large_after}" + ); + } + eprintln!( + "cbBTC direction={direction} small_in={small_input} expected_out={small_expected} actual_out={small_after}; large_in={large_input} baseline_out={large_before} prepared_out={large_after}" + ); + } + } } #[tokio::test] @@ -957,8 +1070,20 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { }) .expect("doubled-price buy"); let large_sell = 30_000_000_000; - let thin_sell_values = scale_ladder(&fork.market.data, "amount", 1_000, 10_000); - let thin_buy_values = scale_ladder(&fork.market.data, "amount", 10_000, 1_000); + let depth = |sell, buy| { + build_tessera_depth_scenario( + Pubkey::from_str_const(TESSERA_SOL_USDC_MARKET), + &fork.market, + sell, + buy, + ) + .expect("build depth scenario") + .overrides + .remove(0) + .values + }; + let thin_sell_values = depth(1_000, 10_000); + let thin_buy_values = depth(10_000, 1_000); let thin_sell_curve_values = scale_ladder(&fork.market.data, "factor", 5_000, 10_000); let thin_buy_curve_values = scale_ladder(&fork.market.data, "factor", 10_000, 5_000); let half_sell_curve = tessera_run(&fork, amount_in, 1, true, false, |market| { @@ -1305,65 +1430,59 @@ async fn tessera_four_additional_markets_prove_price_and_curve_directions() { } #[tokio::test] -async fn tessera_catalog_matches_live_markets() { - let registry = TemplateRegistry::new(); - let template = registry - .get("tessera-fair-value") - .expect("Tessera fair-value template"); - let catalog = template - .constants - .get("market") - .expect("Tessera market catalog"); - - let addresses: Vec = catalog - .options +async fn tessera_discovers_live_markets() { + let markets = discover_tessera_markets(&live::client()) + .await + .expect("discover live markets"); + assert!(!markets.is_empty(), "Tessera must expose market accounts"); + let addresses = markets .iter() - .map(|option| { - option - .value - .parse() - .unwrap_or_else(|_| panic!("catalog entry {} is not a pubkey", option.id)) - }) - .collect(); - let markets = live::fetch(&addresses).await; - - let mut mints: Vec = Vec::with_capacity(addresses.len() * 2); - for market in &markets { - let (base, quote) = - TesseraMarket::mint_addresses(market).expect("every catalog entry is a live market"); - mints.extend([base, quote]); + .map(|market| market.address) + .collect::>(); + let unique = addresses.iter().collect::>(); + assert_eq!(unique.len(), markets.len()); + let mut accounts = Vec::new(); + for batch in addresses.chunks(100) { + accounts.extend(live::fetch(batch).await); } + let mut mints = markets + .iter() + .flat_map(|market| [market.base_mint, market.quote_mint]) + .collect::>(); mints.sort_unstable(); mints.dedup(); - let mint_accounts = live::fetch(&mints).await; - - for (option, market_account) in catalog.options.iter().zip(&markets) { - let address: Pubkey = option.value.parse().expect("catalog pubkey"); - let (base, quote) = TesseraMarket::mint_addresses(market_account).expect("live market"); - let index = |mint: &Pubkey| mints.binary_search(mint).expect("fetched mint"); - let market = TesseraMarket::validate( - address, - market_account, + let mut mint_accounts = Vec::new(); + for batch in mints.chunks(100) { + mint_accounts.extend(live::fetch(batch).await); + } + for (market, account) in markets.iter().zip(&accounts) { + let (base, quote) = + TesseraMarket::mint_addresses(account).expect("valid discovered market"); + let index = |mint| mints.binary_search(mint).expect("fetched mint"); + let expected = TesseraMarket::validate( + market.address, + account, &mint_accounts[index(&base)], &mint_accounts[index("e)], ) - .unwrap_or_else(|error| panic!("{} failed validation: {error}", option.id)); - - let metadata = |key: &str| { - option - .metadata - .get(key) - .unwrap_or_else(|| panic!("{} has no {key}", option.id)) - }; - assert_eq!(metadata("base_mint"), &market.base_mint.to_string()); - assert_eq!(metadata("quote_mint"), &market.quote_mint.to_string()); - assert_eq!(metadata("base_decimals"), &market.base_decimals); - assert_eq!(metadata("quote_decimals"), &market.quote_decimals); - // The catalog's freshness limit is what picks the stale template, so it has to be the - // number the deployed program actually reads at offset 88. + .unwrap(); + assert_eq!(*market, expected); assert_eq!( - metadata("freshness_limit_slots"), - &read_u64(&market_account.data, FRESHNESS_LIMIT_OFFSET) + market.freshness_limit_slots, + read_u64(&account.data, FRESHNESS_LIMIT_OFFSET) ); + assert!(!market.label().is_empty()); } + let registry = TemplateRegistry::new(); + assert!( + !registry + .get("tessera-fair-value") + .unwrap() + .constants + .contains_key("market") + ); + eprintln!( + "Discovered {} Tessera markets from program accounts", + markets.len() + ); } diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index 831158165..a64a60c19 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -22,7 +22,10 @@ use surfpool_core::{ protocols::pump::v1::graduation_builder::{ build_pump_graduation_scenario, pump_graduation_addresses, }, - protocols::tessera::v1::{TesseraMarket, build_tessera_fair_value_scenario}, + protocols::tessera::v1::{ + TesseraMarket, build_tessera_depth_scenario, build_tessera_fair_value_scenario, + discover_tessera_markets, + }, }, solana_account::Account, solana_commitment_config::CommitmentConfig, @@ -48,7 +51,7 @@ fn scenario_tool_error(message: String) -> CallToolResult { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct CreateTesseraFairValueScenarioParams { #[schemars( - description = "The Tessera market account. Resolve one through search_constant_options on the tessera-fair-value template's `market` constant; omit to use the default SOL/USDC market." + description = "The Tessera market account. Resolve an address through list_tessera_markets; omit to use the default SOL/USDC market." )] pub market: Option, #[schemars( @@ -61,6 +64,28 @@ pub struct CreateTesseraFairValueScenarioParams { pub surfnet_port: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct ListTesseraMarketsParams { + #[schemars(description = "The target local Surfnet RPC port. Omit to use 8899.")] + pub surfnet_port: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct CreateTesseraDepthScenarioParams { + #[schemars(description = "Market account address from list_tessera_markets.")] + pub market: String, + #[schemars( + description = "Remaining base-to-quote sell depth in basis points, 1..10000. Reducing by 90% means 1000; 10000 leaves sells unchanged." + )] + pub sell_remaining_bps: u16, + #[schemars( + description = "Remaining quote-to-base buy depth in basis points, 1..10000. Reducing by 90% means 1000; 10000 leaves buys unchanged." + )] + pub buy_remaining_bps: u16, + #[schemars(description = "Target local Surfnet RPC port. Omit to use 8899.")] + pub surfnet_port: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct StartSurfnetParams { #[schemars( @@ -1017,7 +1042,39 @@ impl Surfpool { } #[tool( - description = "Creates one editable Tessera fair-value scenario for a live market. Reads the market and both mint accounts from the running surfnet, derives the pair of reciprocal atomic ratios from their decimals, and keeps the quote fresh while the scenario runs. Prepares state; sends no swap. Resolve `market` through search_constant_options on the tessera-fair-value template's `market` constant." + description = "Lists Tessera markets discovered from program accounts on the target Surfnet. Returns market addresses, pair labels, base/quote mints and decimals, and each market's freshness limit. Use addresses to create scenarios; labels are display names and unknown symbols use mint addresses." + )] + async fn list_tessera_markets( + &self, + Parameters(params): Parameters, + ) -> Result { + let port = params.surfnet_port.unwrap_or(DEFAULT_RPC_PORT); + let client = SurfnetRemoteClient::new(format!("http://127.0.0.1:{port}")); + let markets = match discover_tessera_markets(&client).await { + Ok(markets) => markets, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + let markets = markets + .iter() + .map(|market| { + serde_json::json!({ + "address": market.address.to_string(), + "label": market.label(), + "baseMint": market.base_mint.to_string(), + "quoteMint": market.quote_mint.to_string(), + "baseDecimals": market.base_decimals, + "quoteDecimals": market.quote_decimals, + "freshnessLimitSlots": market.freshness_limit_slots, + }) + }) + .collect::>(); + Ok(CallToolResult::success(vec![Content::text( + serde_json::json!({ "count": markets.len(), "markets": markets }).to_string(), + )])) + } + + #[tool( + description = "Creates one editable Tessera fair-value scenario for a live market. Reads the market and both mint accounts from the running surfnet, derives the pair of reciprocal atomic ratios from their decimals, and keeps the quote fresh while the scenario runs. Prepares state; sends no swap. Resolve `market` through list_tessera_markets." )] async fn create_tessera_fair_value_scenario( &self, @@ -1085,6 +1142,45 @@ impl Surfpool { self.stage_scenario(preparation.scenario).await } + #[tool( + description = "Creates one editable Tessera depth-reduction scenario from the selected market's current Surfnet state. Scales only enabled capacities with exact integer arithmetic, preserving prices, factors, disabled levels and any unchanged direction. Keeps quotes fresh. A 90% reduction means 1000 remaining basis points. Prepares state; does not Play or send swaps. Report validation failures without substituting another market." + )] + async fn create_tessera_depth_scenario( + &self, + Parameters(params): Parameters, + ) -> Result { + let market = match Pubkey::from_str(params.market.trim()) { + Ok(market) => market, + Err(error) => { + return Ok(scenario_tool_error(format!( + "Invalid Tessera market pubkey: {error}" + ))); + } + }; + let accounts = match self + .fetch_surfnet_accounts(params.surfnet_port, &[market]) + .await + { + Ok(accounts) => accounts, + Err(error) => return Ok(scenario_tool_error(error)), + }; + let Some(account) = accounts[0].as_ref() else { + return Ok(scenario_tool_error(format!( + "Tessera market account {market} was not found" + ))); + }; + let scenario = match build_tessera_depth_scenario( + market, + account, + params.sell_remaining_bps, + params.buy_remaining_bps, + ) { + Ok(scenario) => scenario, + Err(error) => return Ok(scenario_tool_error(error.to_string())), + }; + self.stage_scenario(scenario).await + } + #[tool( description = "Fetches ALL available override templates. MUST be called before create_scenario to get valid templateId values and property names. Constants are summarized as {label, description, optionsCount} - resolve an actual option value with search_constant_options." )] @@ -1417,6 +1513,25 @@ mod tests { ); } + #[tokio::test] + async fn tessera_depth_rejects_a_bad_market_before_any_rpc() { + let result = Surfpool::new() + .create_tessera_depth_scenario(Parameters(CreateTesseraDepthScenarioParams { + market: "not-a-pubkey".to_string(), + sell_remaining_bps: 1000, + buy_remaining_bps: 10000, + surfnet_port: None, + })) + .await + .unwrap(); + assert!( + json_of(&result)["error"] + .as_str() + .unwrap() + .contains("Invalid Tessera market pubkey") + ); + } + #[tokio::test] async fn get_override_templates_summarizes_constants_instead_of_inlining_options() { let surfpool = Surfpool::new(); From 51e2952c4d0e2ffdce173169436de9e8a4b9115d Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Wed, 9 Sep 2026 14:18:33 +0300 Subject: [PATCH 3/8] test(tessera): surface all retry errors from the live fetch helper The live fetch retry loop discarded each intermediate error and panicked with only the last one. Collect every attempt and include them in the panic so a persistent failure is diagnosable. --- crates/core/src/tests/live.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/core/src/tests/live.rs b/crates/core/src/tests/live.rs index 853cb2b77..a9891ecfb 100644 --- a/crates/core/src/tests/live.rs +++ b/crates/core/src/tests/live.rs @@ -23,6 +23,7 @@ pub async fn fetch(addresses: &[Pubkey]) -> Vec { // the callers assert. Retry a few times with backoff so a transient refusal is not read as a // failure. let mut attempt = 0; + let mut errors = Vec::new(); let results = loop { match client() .get_multiple_accounts(addresses, CommitmentConfig::confirmed()) @@ -31,10 +32,17 @@ pub async fn fetch(addresses: &[Pubkey]) -> Vec { Ok(results) => break results, Err(error) if attempt < 4 => { attempt += 1; + errors.push(format!("attempt {attempt}: {error}")); tokio::time::sleep(std::time::Duration::from_millis(500 * attempt)).await; - let _ = error; } - Err(error) => panic!("failed to fetch {addresses:?} from mainnet: {error}"), + Err(error) => { + errors.push(format!("attempt {}: {error}", attempt + 1)); + panic!( + "failed to fetch {addresses:?} from mainnet after {} attempts: {}", + errors.len(), + errors.join("; ") + ); + } } }; From 621dea618b65d1227ef4b5271828f40c413ac842 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Tue, 15 Sep 2026 07:57:58 +0300 Subject: [PATCH 4/8] fix(mcp): take camelCase tool arguments like the pump tool The Tessera, HumidiFi and GoonFi parameter structs deserialized their fields as snake_case, so a client sending surfnetPort the way the pump, get_template and search_constant_options tools expect it was ignored and the read fell back to port 8899. Scenario tools now share one convention. --- crates/mcp/src/surfpool/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index a64a60c19..f90637059 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -49,6 +49,7 @@ fn scenario_tool_error(message: String) -> CallToolResult { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct CreateTesseraFairValueScenarioParams { #[schemars( description = "The Tessera market account. Resolve an address through list_tessera_markets; omit to use the default SOL/USDC market." @@ -65,12 +66,14 @@ pub struct CreateTesseraFairValueScenarioParams { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct ListTesseraMarketsParams { #[schemars(description = "The target local Surfnet RPC port. Omit to use 8899.")] pub surfnet_port: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct CreateTesseraDepthScenarioParams { #[schemars(description = "Market account address from list_tessera_markets.")] pub market: String, From bb1d7f00f6bdb39555a5f2a596216231e1e358a5 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Wed, 16 Sep 2026 12:18:42 +0300 Subject: [PATCH 5/8] fix(tessera): track deploy 446053401, halt all levels, trim --- Cargo.lock | 2 - crates/core/Cargo.toml | 3 +- .../src/scenarios/protocols/tessera/README.md | 85 +-- .../scenarios/protocols/tessera/v1/depth.rs | 25 +- .../protocols/tessera/v1/fair_value.rs | 125 ++-- .../scenarios/protocols/tessera/v1/markets.rs | 3 +- .../src/scenarios/protocols/tessera/v1/mod.rs | 6 +- .../protocols/tessera/v1/overrides.yaml | 141 ++--- crates/core/src/scenarios/registry.rs | 7 +- crates/core/src/surfnet/svm.rs | 27 - crates/core/src/tests/tessera/mod.rs | 573 ++++++------------ crates/mcp/Cargo.toml | 2 - crates/mcp/src/surfpool/mod.rs | 298 +++++---- crates/types/src/scenarios.rs | 88 ++- 14 files changed, 573 insertions(+), 812 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eda85fc42..7b9e92d19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12219,8 +12219,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "solana-account 4.3.1", - "solana-commitment-config", "solana-keypair", "solana-pubkey 3.0.0", "solana-signer", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 7952c8145..441d7a581 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -115,8 +115,7 @@ axum = { version = "0.8", default-features = false, features = ["tokio", "http1" [dev-dependencies] ed25519-dalek = "1.0.1" -# Only the Tessera live suite uses it, to host the CPI wrapper that drives the deployed program. -solana-program-runtime = "4.1.2" +solana-program-runtime = "4.2.1" libsecp256k1 = "0.7.2" p256 = { version = "0.13", default-features = false, features = ["ecdsa"] } test-case = { workspace = true } diff --git a/crates/core/src/scenarios/protocols/tessera/README.md b/crates/core/src/scenarios/protocols/tessera/README.md index 898688a22..fc7d499cf 100644 --- a/crates/core/src/scenarios/protocols/tessera/README.md +++ b/crates/core/src/scenarios/protocols/tessera/README.md @@ -9,23 +9,25 @@ submit a swap. - Program: `TessVdML9pBGgG9yGks7o4HewRaXVAMuoVj4x83GLQH` - ProgramData: `BzSXM6KLDpHQQChzr7Fdgbzwp8r8zRYWFFrHK2uZmDYV` - Upgrade authority: `7bJ9xu9UGVZPtYzH1fMwdaKdvfhqeSJtoFc2eGrXBPhK` -- Deploy slot: `438800691` -- ELF SHA-256: `433f2a857ffe2045310a478b4aca0fd824308d01f283719275baec60e2aecb3b` +- Deploy slot: `446053401` +- ELF SHA-256: `82fd37995fcece47a253b1a00c1dd7e4c3fff706b2e42384110e2cbabf4201b3` -Every offset and behavior below was proven against exactly this deployment; the live suite pins -it and fails when any of these values move. A redeploy voids the layout evidence — re-verify +The live suite checks the layout and behavior against this pinned deployment and fails when +its identity changes. A redeploy voids the layout evidence — re-verify before trusting the templates again. ## The guard, and what it does not cover -A market is 1264 bytes with the eight-byte layout tag `05 00 00 00 00 00 00 00` at offset 96. -Discovery selects accounts with this size and tag, then validates the mint identities. The tag is -the version half of the guard: it is what rejects a future market layout that reuses the size. +The manifest requires 1264 bytes and `05 00 00 00 00 00 00 00` at offset 96, then discovery +validates ownership and mint identities. These bytes are a conservative observed-state filter, +not a version discriminator: the deployed program also reads this field during age adjustment. +A different value may exclude a valid market, and a matching value does not authenticate a future +layout. Deployment revalidation remains necessary. The shared raw-layout schema has no owner predicate, so a foreign account of the same size carrying -the same eight bytes would pass a raw template. `validate_tessera_market_layout` adds the ownership -check, and every scenario made through the fair-value builder goes through it. Composing the raw -template against an arbitrary address does not. That is a property of the shared schema, not of +the same eight bytes would pass a raw template. `TesseraMarket` validation adds the ownership +check used by discovery and both scenario tools; the depth builder also validates its input +account. Composing the raw template against an arbitrary address does not. That is a property of the shared schema, not of this integration, and the raw scenario API is unvalidated by contract. ## Templates @@ -35,7 +37,7 @@ this integration, and the raw scenario API is unvalidated by contract. | `tessera-fair-value` | both directional atomic-ratio fields | | `tessera-depth` | all twenty directional capacities on both ladders | | `tessera-curve` | all twenty directional output factors on both ladders | -| `tessera-halt` | both required first-level enabled flags | +| `tessera-halt` | all twenty enabled flags on both ladders | | `tessera-stale-quote` | offset 120, aged by the lead you pass (default -20) | | `tessera-freshness` | offset 120, the current materialization slot | @@ -47,8 +49,18 @@ where it was, which is why they are one invariant. The sell ladder occupies bytes 160 through 639 and the buy ladder 640 through 1119. Each holds twenty 24-byte records: directional capacity at `+0`, marginal-price factor at `+8`, enabled flag at `+16`. Capacity and factor changes affect only their active quote direction. For a fill -contained in the first level, both directions match -`floor(input_atoms * directional_price * first_level_factor / 10^21)` exactly. +starting and ending in level zero, the modeled output is +`floor(input_atoms * directional_price * first_level_factor / 10^21)`. The live tests check exact +atomic output after clearing the captured flow counters at 0/8 and neutralizing the five +selectable configurations at 1136 + 12*i (ppm adjustment 0, factor scale 1,000,000, no skipped +levels) in their local fixtures. A small input alone cannot +establish this condition: prior flow or the selected configuration can start at a later level. +These fixture controls are not exposed as scenario properties. + +`tessera-halt` writes zero to every enabled flag using two strided byte properties. Its existing +property names are retained, but each now covers twenty levels. Clearing only level zero can +leave later levels tradable. The live regression checks both captured state and an explicit +one-level skip, including a control where clearing only level zero still allows a swap. Offset 88 stores the age at which the program rejects a quote. Age 19 succeeds and age 20 fails with custom error 65535 on a market configured at 20. @@ -62,25 +74,19 @@ template writes a perfectly fresh quote. ## Live market discovery `list_tessera_markets` queries Tessera program accounts through the selected Surfnet RPC. -It filters by the manifest's account size and layout tag, validates ownership and mint identities, +It filters by the manifest's account size and pinned bytes, validates ownership and mint identities, and reads decimals from the referenced mint accounts. The freshness limit comes from offset 88. The existing Surfnet account resolver merges remote discovery with local accounts, preferring local state. Discovery needs a datasource that supports `getProgramAccounts`; offline instances can list only their local accounts. -Studio loads this list when opening the fair-value dialog. The model uses the same tool to select -an override account and its freshness limit. There is no market list in `overrides.yaml`; the six -shared templates retain the SOL/USDC default address for callers that omit an account. +The six shared templates retain the SOL/USDC default address for callers that omit an account. Labels use mint symbols from Surfpool's existing token metadata. An unknown mint is displayed by its full address, so missing symbol metadata never hides a discovered market. Addresses are the identities; symbols are not unique. Market membership, decimals and freshness limits are not taken from the token metadata catalog. -`tessera_discovers_live_markets` exercises the production discovery function and checks the returned -mint identities, decimals and freshness limits against fetched accounts. It does not pin a market -count, so newly listed markets are included without changing the test or templates. - ## Builders and tools The fair-value builder converts a human price into reciprocal atomic ratios using both mints' @@ -88,15 +94,13 @@ decimals. It is a pure function over account data. `create_tessera_fair_value_sc surfnet's own RPC, so local state wins and only missing accounts fall back to the datasource, then stages the scenario through the shared path. -The price override deliberately does not set `fetchBeforeUse`. Reading the accounts at creation -hydrates them into local state, so the values apply to the same bytes they were derived from; a -Play-time refetch would reinstall remote bytes over any local edit and patch a different read. - -The paired freshness override is persisted. Its slot encoder writes the slot it materializes at, -so the prepared price stays inside the market's freshness window however long the scenario runs. +Builder-created overrides keep `fetchBeforeUse: false`: creation has already read and hydrated +the target account, and the scenario must use that prepared local snapshot. When composing a +direct template scenario, set `fetchBeforeUse: true` on the first override for each account not +yet in local state. Freshness overrides use `persist: true` and `last_update_slot: null` to write +the current materialization slot on every application. -The `Tessera Depth Stress` AI chip requests a 90% reduction in both directions. The -`create_tessera_depth_scenario` tool reads current Surfnet state and takes remaining basis points +The `create_tessera_depth_scenario` tool reads current Surfnet state and takes remaining basis points per direction: 1000 retains 10%, 10000 leaves that direction unchanged. It scales only enabled capacities, with integer-floor rounding, and rejects zero capacities or increases. Prices, factors and disabled levels are preserved. The scenario combines `tessera-depth` with persisted freshness; @@ -105,13 +109,10 @@ Curve changes remain available through the raw template. ## Behavioral evidence -The live suite loads the pinned deployed ELF from ProgramData into LiteSVM and fails if the -ProgramData address, deploy slot, or ELF hash changes. It proves each price field controls only its -matching direction, that active-side capacities and factors alter large fills while the opposite -side stays byte-for-byte identical, that first-level output in both directions equals the -price-times-factor formula to the atom, that age 19 succeeds and age 20 fails with error 65535, -that disabling both required first levels fails both directions, that an unordered single curve -factor fails with error 8, and that live market discovery returns valid mint metadata. +The live suite loads the pinned ELF into LiteSVM and exercises price direction isolation, +depth reductions, curve factors, freshness boundaries, halted ladders, vault bindings, +invalid sentinel/global account metas, and live market discovery. Raw writes are checked against +complete expected buffers or permitted byte ranges. Run it serially. The public endpoint sheds queued requests right after a `getProgramAccounts` scan, sometimes as a 413 that looks like a request-size error: @@ -126,9 +127,9 @@ private endpoint when the public one rate-limits. ## Known boundaries -The remaining header and trailing fields carry no assigned semantics. No separate fee field is -exposed: the proven first-level output has no deduction beyond its directional price and factor, -but that does not establish how Tessera decomposes the factor into spread, fee, or another price -adjustment. The structured region from 1120 onward stays unexposed because its economic meaning has -not been behaviorally proven. Vault depletion is not exposed either; the generic SPL Token balance -template follows the Anchor discriminator path and does not materialize a non-Anchor token account. +No separate fee field is exposed. Exact controlled first-level output does not establish how +Tessera decomposes its price factor into spread, fee, or another adjustment. The region from 1120 +onward includes configuration-dependent quote adjustments and leading-level selection; its full +economic meaning remains unmodeled and it is not exposed by the templates. Vault depletion is +not exposed by the Tessera templates. The generic SPL Token balance template uses the shared +typed token-account writer, but Tessera vault depletion behavior is outside this suite's coverage. diff --git a/crates/core/src/scenarios/protocols/tessera/v1/depth.rs b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs index a09dcf87a..7e19fd152 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/depth.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs @@ -4,13 +4,15 @@ use solana_account::Account; use solana_pubkey::Pubkey; use surfpool_types::{AccountAddress, OverrideInstance, Scenario}; +use super::{ + TesseraMarket, + fair_value::{FRESHNESS_TEMPLATE, freshness_override, template}, +}; use crate::{ error::{SurfpoolError, SurfpoolResult}, scenarios::TemplateRegistry, }; -use super::TesseraMarket; - pub fn build_tessera_depth_scenario( market: Pubkey, account: &Account, @@ -27,11 +29,10 @@ pub fn build_tessera_depth_scenario( } TesseraMarket::mint_addresses(account)?; let registry = TemplateRegistry::new(); - let template = registry - .get("tessera-depth") - .expect("compiled Tessera template"); + let depth_template = template(®istry, "tessera-depth")?; + template(®istry, FRESHNESS_TEMPLATE)?; let mut values = HashMap::new(); - for property in &template.properties { + for property in &depth_template.properties { let bps = if property.path.starts_with("sell_levels.") { sell_remaining_bps } else { @@ -68,19 +69,11 @@ pub fn build_tessera_depth_scenario( ); let target = AccountAddress::Pubkey(market.to_string()); scenario.add_override( - OverrideInstance::new(template.id.clone(), 0, target.clone()) + OverrideInstance::new(depth_template.id.clone(), 0, target.clone()) .with_values(values) .with_label("Reduce Tessera depth".to_string()), ); - scenario.add_override( - OverrideInstance::new("tessera-freshness".to_string(), 0, target) - .with_values(HashMap::from([( - "last_update_slot".to_string(), - serde_json::Value::Null, - )])) - .with_label("Keep Tessera quote fresh".to_string()) - .with_persist(true), - ); + scenario.add_override(freshness_override(target)); scenario.tags = vec![ "tessera".to_string(), "pmm".to_string(), diff --git a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs index 6d5113dba..ea655da23 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs @@ -28,7 +28,7 @@ pub const TESSERA_DEFAULT_MARKET: Pubkey = const BASE_MINT_OFFSET: usize = 24; const QUOTE_MINT_OFFSET: usize = 56; -/// The size and layout tag a Tessera market must have, taken from the manifest the raw templates +/// The size and pinned bytes a Tessera market must have, taken from the manifest the raw templates /// are written against so there is one definition of them. Built once; the manifest is compiled in. pub(super) static MARKET_LAYOUT: LazyLock = LazyLock::new(|| { template(&TemplateRegistry::new(), FAIR_VALUE_TEMPLATE) @@ -41,7 +41,7 @@ pub(super) static MARKET_LAYOUT: LazyLock = LazyLock::new(|| { }); const FAIR_VALUE_TEMPLATE: &str = "tessera-fair-value"; -const FRESHNESS_TEMPLATE: &str = "tessera-freshness"; +pub(super) const FRESHNESS_TEMPLATE: &str = "tessera-freshness"; /// Both ratio fields are integers scaled by 10^15, so their product is 10^30. const ATOMIC_RATIO_SCALE: u128 = 1_000_000_000_000_000; @@ -122,7 +122,7 @@ impl TesseraMarket { /// Rejects an account that is not a Tessera market. /// /// The shared raw-layout guard has no owner predicate, so a foreign account of the same size -/// carrying the same layout tag would pass it. Every builder-made scenario comes through here, +/// carrying the same pinned bytes would pass it. Every builder-made scenario comes through here, /// which adds the ownership check the schema cannot express. pub fn validate_tessera_market_layout(account: &Account) -> SurfpoolResult<()> { if account.owner != TESSERA_PROGRAM_ID { @@ -151,13 +151,12 @@ pub fn build_tessera_fair_value_scenario( let registry = TemplateRegistry::new(); let fair_value = template(®istry, FAIR_VALUE_TEMPLATE)?; - let freshness = template(®istry, FRESHNESS_TEMPLATE)?; + template(®istry, FRESHNESS_TEMPLATE)?; let market_name = market.label(); let target = AccountAddress::Pubkey(market.address.to_string()); - // No fetch_before_use: these values were derived from the market account this scenario was - // built against, which creation already hydrated into local state. A Play-time refetch would - // reinstall remote bytes over any local edit and apply numbers derived from a different read. + // Creation hydrates the market into local state. Both overrides must use that prepared + // snapshot, so they leave fetch_before_use false. let price_override = OverrideInstance::new(fair_value.id.clone(), PREPARATION_SLOT, target.clone()) .with_values(HashMap::from([ @@ -172,17 +171,6 @@ pub fn build_tessera_fair_value_scenario( ])) .with_label(format!("Tessera {market_name} fair value")); - // Null, not zero: the slot encoder reads a supplied number AS the lead, so only null takes the - // template's own lead of zero. Persisted, so the prepared price stays inside the market's - // freshness window however long the scenario is left running. - let freshness_override = OverrideInstance::new(freshness.id.clone(), PREPARATION_SLOT, target) - .with_values(HashMap::from([( - "last_update_slot".to_string(), - serde_json::Value::Null, - )])) - .with_label("Keep Tessera quote fresh".to_string()) - .with_persist(true); - let normalized_price = price.trim(); let mut scenario = Scenario::new( format!("Tessera {market_name} at {normalized_price}"), @@ -197,7 +185,7 @@ pub fn build_tessera_fair_value_scenario( "price-dislocation".to_string(), ]; scenario.add_override(price_override); - scenario.add_override(freshness_override); + scenario.add_override(freshness_override(target)); Ok(TesseraFairValuePreparation { scenario, @@ -207,6 +195,17 @@ pub fn build_tessera_fair_value_scenario( }) } +pub(super) fn freshness_override(target: AccountAddress) -> OverrideInstance { + // Persisted, so the quote stays fresh however long the scenario runs. + OverrideInstance::new(FRESHNESS_TEMPLATE.to_string(), PREPARATION_SLOT, target) + .with_values(HashMap::from([( + "last_update_slot".to_string(), + serde_json::Value::Null, + )])) + .with_label("Keep Tessera quote fresh".to_string()) + .with_persist(true) +} + fn read_pubkey(data: &[u8], offset: usize) -> SurfpoolResult { let bytes: [u8; 32] = data[offset..offset + 32] .try_into() @@ -275,7 +274,10 @@ fn checked_power_of_ten(exponent: u32) -> SurfpoolResult { .ok_or_else(|| invalid("price scale exceeds supported precision")) } -fn template<'a>(registry: &'a TemplateRegistry, id: &str) -> SurfpoolResult<&'a OverrideTemplate> { +pub(super) fn template<'a>( + registry: &'a TemplateRegistry, + id: &str, +) -> SurfpoolResult<&'a OverrideTemplate> { registry .get(id) .ok_or_else(|| SurfpoolError::internal(format!("Tessera template {id} is unavailable"))) @@ -310,7 +312,7 @@ mod tests { let mut data = vec![0; MARKET_LAYOUT.account_size]; data[BASE_MINT_OFFSET..BASE_MINT_OFFSET + 32].copy_from_slice(base_mint.as_ref()); data[QUOTE_MINT_OFFSET..QUOTE_MINT_OFFSET + 32].copy_from_slice(quote_mint.as_ref()); - let magic = MARKET_LAYOUT.magic.as_ref().expect("manifest layout tag"); + let magic = MARKET_LAYOUT.magic.as_ref().expect("manifest byte guard"); data[magic.offset..magic.offset + magic.bytes.len()].copy_from_slice(&magic.bytes); Account { data, @@ -346,57 +348,36 @@ mod tests { assert_eq!(market.label(), format!("{base}/{quote}")); } - #[test] - fn builds_atomic_fair_value_for_wsol_usdc_decimals() { - let market = market(9, 6); - let preparation = build_tessera_fair_value_scenario(&market, "100.25").unwrap(); - assert_eq!( - preparation.quote_atoms_per_base_atom_x1e15, - 100_250_000_000_000 - ); - assert_eq!( - preparation.base_atoms_per_quote_atom_x1e15, - (RECIPROCAL_PRODUCT / 100_250_000_000_000u128) as u64 - ); - assert_eq!(preparation.scenario.overrides.len(), 2); - assert_eq!( - preparation.scenario.overrides[0].account, - AccountAddress::Pubkey(market.address.to_string()) - ); - } - #[test] fn derives_price_scale_from_market_mint_decimals() { - let market = market(8, 6); - let preparation = build_tessera_fair_value_scenario(&market, "78.8477010015472512") - .expect("build CBB/USDC price"); - assert_eq!( - preparation.quote_atoms_per_base_atom_x1e15, - 788_477_010_015_472 - ); - assert_eq!( - preparation.base_atoms_per_quote_atom_x1e15, - (RECIPROCAL_PRODUCT / 788_477_010_015_472u128) as u64 - ); - } - - /// The price is computed from one read of the market; a Play-time refetch would apply it to a - /// different one and overwrite local edits. The freshness value must stay null, because the - /// slot encoder reads a supplied number as the lead rather than ignoring it. - #[test] - fn price_applies_to_the_read_it_came_from_and_freshness_keeps_the_template_lead() { - let preparation = build_tessera_fair_value_scenario(&market(9, 6), "100.25").unwrap(); - let [price, freshness] = &preparation.scenario.overrides[..] else { - panic!("expected exactly a price and a freshness override"); - }; - assert!(!price.fetch_before_use); - assert!(!price.persist); - assert!(!freshness.fetch_before_use); - assert!(freshness.persist); - assert_eq!( - freshness.values.get("last_update_slot"), - Some(&serde_json::Value::Null) - ); + for (base_decimals, quote_decimals, price, expected) in [ + (9, 6, "100.25", 100_250_000_000_000u64), + (8, 6, "78.8477010015472512", 788_477_010_015_472), + ] { + let market = market(base_decimals, quote_decimals); + let preparation = build_tessera_fair_value_scenario(&market, price).unwrap(); + assert_eq!(preparation.market, market.address); + assert_eq!(preparation.quote_atoms_per_base_atom_x1e15, expected); + assert_eq!( + preparation.base_atoms_per_quote_atom_x1e15, + (RECIPROCAL_PRODUCT / u128::from(expected)) as u64 + ); + let [price, freshness] = &preparation.scenario.overrides[..] else { + panic!("expected exactly a price and a freshness override"); + }; + assert_eq!( + price.account, + AccountAddress::Pubkey(market.address.to_string()) + ); + assert!(!price.fetch_before_use); + assert!(!price.persist); + assert!(!freshness.fetch_before_use); + assert!(freshness.persist); + assert_eq!( + freshness.values.get("last_update_slot"), + Some(&serde_json::Value::Null) + ); + } } #[test] @@ -419,6 +400,10 @@ mod tests { // The raw guard cannot see the owner, which is the whole reason this check sits on top. assert!(MARKET_LAYOUT.guard(&wrong_owner.data).is_ok()); + let mut wrong_magic = market_account(&Pubkey::new_unique(), &Pubkey::new_unique()); + wrong_magic.data[MARKET_LAYOUT.magic.as_ref().unwrap().offset] ^= 1; + assert!(TesseraMarket::mint_addresses(&wrong_magic).is_err()); + let same_mint = Pubkey::new_unique(); assert!( TesseraMarket::validate( diff --git a/crates/core/src/scenarios/protocols/tessera/v1/markets.rs b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs index c1ba240ed..ebc7c2c74 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/markets.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs @@ -8,13 +8,12 @@ use solana_client::{ }; use solana_commitment_config::CommitmentConfig; +use super::{TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, fair_value::MARKET_LAYOUT}; use crate::{ error::{SurfpoolError, SurfpoolResult}, surfnet::remote::SurfnetRemoteClient, }; -use super::{TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, fair_value::MARKET_LAYOUT}; - pub async fn discover_tessera_markets( client: &SurfnetRemoteClient, ) -> SurfpoolResult> { diff --git a/crates/core/src/scenarios/protocols/tessera/v1/mod.rs b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs index 06e850948..1985c10c1 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/mod.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/mod.rs @@ -3,9 +3,7 @@ mod fair_value; mod markets; pub use depth::build_tessera_depth_scenario; -pub use markets::discover_tessera_markets; - pub use fair_value::{ - TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraFairValuePreparation, TesseraMarket, - build_tessera_fair_value_scenario, validate_tessera_market_layout, + TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, build_tessera_fair_value_scenario, }; +pub use markets::discover_tessera_markets; diff --git a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml index 28a241421..b8750dc3d 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml @@ -1,5 +1,5 @@ protocol: Tessera -version: deployed-438800691 +version: deployed-446053401 account_type: MarketState raw_layout: @@ -33,7 +33,11 @@ templates: label: Base per quote description: "Base atomic units per quote atomic unit multiplied by 10^15. Use the builder to derive the reciprocal atomically." llm_context: | - Discover markets with list_tessera_markets and set the override account to the chosen address. + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + Builder-created scenarios keep fetchBeforeUse: false to use the prepared local snapshot. + + Use list_tessera_markets to select the override account. SET BOTH FIELDS AS ONE INVARIANT. Their product is approximately 10^30, with integer-floor rounding in the reciprocal field. Changing only offset 128 moves base-to-quote sells but leaves quote-to-base buys unchanged; changing only offset 144 does the opposite. @@ -61,204 +65,168 @@ templates: offset: 160 encoding: u64 label: Sell level 1 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.1.amount offset: 184 encoding: u64 label: Sell level 2 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.2.amount offset: 208 encoding: u64 label: Sell level 3 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.3.amount offset: 232 encoding: u64 label: Sell level 4 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.4.amount offset: 256 encoding: u64 label: Sell level 5 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.5.amount offset: 280 encoding: u64 label: Sell level 6 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.6.amount offset: 304 encoding: u64 label: Sell level 7 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.7.amount offset: 328 encoding: u64 label: Sell level 8 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.8.amount offset: 352 encoding: u64 label: Sell level 9 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.9.amount offset: 376 encoding: u64 label: Sell level 10 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.10.amount offset: 400 encoding: u64 label: Sell level 11 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.11.amount offset: 424 encoding: u64 label: Sell level 12 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.12.amount offset: 448 encoding: u64 label: Sell level 13 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.13.amount offset: 472 encoding: u64 label: Sell level 14 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.14.amount offset: 496 encoding: u64 label: Sell level 15 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.15.amount offset: 520 encoding: u64 label: Sell level 16 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.16.amount offset: 544 encoding: u64 label: Sell level 17 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.17.amount offset: 568 encoding: u64 label: Sell level 18 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.18.amount offset: 592 encoding: u64 label: Sell level 19 capacity - description: Base-to-quote directional capacity in base atomic units. - path: sell_levels.19.amount offset: 616 encoding: u64 label: Sell level 20 capacity - description: Base-to-quote directional capacity in base atomic units. - path: buy_levels.0.amount offset: 640 encoding: u64 label: Buy level 1 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.1.amount offset: 664 encoding: u64 label: Buy level 2 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.2.amount offset: 688 encoding: u64 label: Buy level 3 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.3.amount offset: 712 encoding: u64 label: Buy level 4 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.4.amount offset: 736 encoding: u64 label: Buy level 5 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.5.amount offset: 760 encoding: u64 label: Buy level 6 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.6.amount offset: 784 encoding: u64 label: Buy level 7 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.7.amount offset: 808 encoding: u64 label: Buy level 8 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.8.amount offset: 832 encoding: u64 label: Buy level 9 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.9.amount offset: 856 encoding: u64 label: Buy level 10 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.10.amount offset: 880 encoding: u64 label: Buy level 11 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.11.amount offset: 904 encoding: u64 label: Buy level 12 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.12.amount offset: 928 encoding: u64 label: Buy level 13 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.13.amount offset: 952 encoding: u64 label: Buy level 14 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.14.amount offset: 976 encoding: u64 label: Buy level 15 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.15.amount offset: 1000 encoding: u64 label: Buy level 16 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.16.amount offset: 1024 encoding: u64 label: Buy level 17 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.17.amount offset: 1048 encoding: u64 label: Buy level 18 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.18.amount offset: 1072 encoding: u64 label: Buy level 19 capacity - description: Quote-to-base directional capacity in quote atomic units. - path: buy_levels.19.amount offset: 1096 encoding: u64 label: Buy level 20 capacity - description: Quote-to-base directional capacity in quote atomic units. llm_context: | - Discover markets with list_tessera_markets and set the override account to the chosen address. + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + Builder-created scenarios keep fetchBeforeUse: false to use the prepared local snapshot. + + Use list_tessera_markets to select the override account. For percentage reductions, use create_tessera_depth_scenario, which reads the current Surfnet account and scales enabled levels exactly. Pass remaining basis points: 1000 keeps 10%, 10000 leaves a direction unchanged. It also adds persistent freshness. @@ -268,9 +236,7 @@ templates: base-to-quote sell and buy_levels for a large quote-to-base buy. The opposite ladder is behaviorally inactive for that direction. - Small trades may not reveal a depth change. The live-fork proof uses large trades and shows - that dividing all active-side capacities by ten worsens the fill, while changing the - inactive-side capacities leaves it unchanged. + Small trades may not reveal a depth change; use a large fill to observe reduced capacity. Do not invent missing levels or enable disabled levels. Fetch the live account first and copy all twenty current values before applying a uniform ratio only to enabled levels. A ratio that @@ -289,207 +255,169 @@ templates: offset: 168 encoding: u64 label: Sell level 1 output factor - description: Base-to-quote directional output factor. - path: sell_levels.1.factor offset: 192 encoding: u64 label: Sell level 2 output factor - description: Base-to-quote directional output factor. - path: sell_levels.2.factor offset: 216 encoding: u64 label: Sell level 3 output factor - description: Base-to-quote directional output factor. - path: sell_levels.3.factor offset: 240 encoding: u64 label: Sell level 4 output factor - description: Base-to-quote directional output factor. - path: sell_levels.4.factor offset: 264 encoding: u64 label: Sell level 5 output factor - description: Base-to-quote directional output factor. - path: sell_levels.5.factor offset: 288 encoding: u64 label: Sell level 6 output factor - description: Base-to-quote directional output factor. - path: sell_levels.6.factor offset: 312 encoding: u64 label: Sell level 7 output factor - description: Base-to-quote directional output factor. - path: sell_levels.7.factor offset: 336 encoding: u64 label: Sell level 8 output factor - description: Base-to-quote directional output factor. - path: sell_levels.8.factor offset: 360 encoding: u64 label: Sell level 9 output factor - description: Base-to-quote directional output factor. - path: sell_levels.9.factor offset: 384 encoding: u64 label: Sell level 10 output factor - description: Base-to-quote directional output factor. - path: sell_levels.10.factor offset: 408 encoding: u64 label: Sell level 11 output factor - description: Base-to-quote directional output factor. - path: sell_levels.11.factor offset: 432 encoding: u64 label: Sell level 12 output factor - description: Base-to-quote directional output factor. - path: sell_levels.12.factor offset: 456 encoding: u64 label: Sell level 13 output factor - description: Base-to-quote directional output factor. - path: sell_levels.13.factor offset: 480 encoding: u64 label: Sell level 14 output factor - description: Base-to-quote directional output factor. - path: sell_levels.14.factor offset: 504 encoding: u64 label: Sell level 15 output factor - description: Base-to-quote directional output factor. - path: sell_levels.15.factor offset: 528 encoding: u64 label: Sell level 16 output factor - description: Base-to-quote directional output factor. - path: sell_levels.16.factor offset: 552 encoding: u64 label: Sell level 17 output factor - description: Base-to-quote directional output factor. - path: sell_levels.17.factor offset: 576 encoding: u64 label: Sell level 18 output factor - description: Base-to-quote directional output factor. - path: sell_levels.18.factor offset: 600 encoding: u64 label: Sell level 19 output factor - description: Base-to-quote directional output factor. - path: sell_levels.19.factor offset: 624 encoding: u64 label: Sell level 20 output factor - description: Base-to-quote directional output factor. - path: buy_levels.0.factor offset: 648 encoding: u64 label: Buy level 1 output factor - description: Quote-to-base directional output factor. - path: buy_levels.1.factor offset: 672 encoding: u64 label: Buy level 2 output factor - description: Quote-to-base directional output factor. - path: buy_levels.2.factor offset: 696 encoding: u64 label: Buy level 3 output factor - description: Quote-to-base directional output factor. - path: buy_levels.3.factor offset: 720 encoding: u64 label: Buy level 4 output factor - description: Quote-to-base directional output factor. - path: buy_levels.4.factor offset: 744 encoding: u64 label: Buy level 5 output factor - description: Quote-to-base directional output factor. - path: buy_levels.5.factor offset: 768 encoding: u64 label: Buy level 6 output factor - description: Quote-to-base directional output factor. - path: buy_levels.6.factor offset: 792 encoding: u64 label: Buy level 7 output factor - description: Quote-to-base directional output factor. - path: buy_levels.7.factor offset: 816 encoding: u64 label: Buy level 8 output factor - description: Quote-to-base directional output factor. - path: buy_levels.8.factor offset: 840 encoding: u64 label: Buy level 9 output factor - description: Quote-to-base directional output factor. - path: buy_levels.9.factor offset: 864 encoding: u64 label: Buy level 10 output factor - description: Quote-to-base directional output factor. - path: buy_levels.10.factor offset: 888 encoding: u64 label: Buy level 11 output factor - description: Quote-to-base directional output factor. - path: buy_levels.11.factor offset: 912 encoding: u64 label: Buy level 12 output factor - description: Quote-to-base directional output factor. - path: buy_levels.12.factor offset: 936 encoding: u64 label: Buy level 13 output factor - description: Quote-to-base directional output factor. - path: buy_levels.13.factor offset: 960 encoding: u64 label: Buy level 14 output factor - description: Quote-to-base directional output factor. - path: buy_levels.14.factor offset: 984 encoding: u64 label: Buy level 15 output factor - description: Quote-to-base directional output factor. - path: buy_levels.15.factor offset: 1008 encoding: u64 label: Buy level 16 output factor - description: Quote-to-base directional output factor. - path: buy_levels.16.factor offset: 1032 encoding: u64 label: Buy level 17 output factor - description: Quote-to-base directional output factor. - path: buy_levels.17.factor offset: 1056 encoding: u64 label: Buy level 18 output factor - description: Quote-to-base directional output factor. - path: buy_levels.18.factor offset: 1080 encoding: u64 label: Buy level 19 output factor - description: Quote-to-base directional output factor. - path: buy_levels.19.factor offset: 1104 encoding: u64 label: Buy level 20 output factor - description: Quote-to-base directional output factor. llm_context: | - Discover markets with list_tessera_markets and set the override account to the chosen address. - These forty fields control the directional output curve. Scaling every nonzero factor on the - active side by one half produces approximately one half of the baseline output on six live - markets, while scaling the inactive side leaves output unchanged. + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + + Use list_tessera_markets to select the override account. + These forty fields control the directional output curve. Scaling every nonzero factor on + the active side scales its output; the opposite side is inactive for that quote direction. Read the live account first and copy all twenty current factors per direction, then apply one ratio to every nonzero factor on the side you are stressing. Two rules are not optional. The @@ -500,7 +428,7 @@ templates: - id: tessera-halt name: Halt Tessera Liquidity - description: Disable the first required level on both directional ladders + description: Disable every level on both directional ladders idl_account_name: MarketState address: type: pubkey @@ -508,19 +436,26 @@ templates: properties: - path: sell_level_0_enabled offset: 176 - encoding: u8 + encoding: + u8_strided: { count: 20, stride: 24 } label: Sell liquidity enabled - description: Set to zero to make base-to-quote quotes fail deterministically. + description: Set to zero to disable all twenty sell levels. Only zero is supported. - path: buy_level_0_enabled offset: 656 - encoding: u8 + encoding: + u8_strided: { count: 20, stride: 24 } label: Buy liquidity enabled - description: Set to zero to make quote-to-base quotes fail deterministically. + description: Set to zero to disable all twenty buy levels. Only zero is supported. llm_context: | - Discover markets with list_tessera_markets and set the override account to the chosen address. + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + + Use list_tessera_markets to select the override account. SET BOTH FIELDS TO ZERO AS ONE INVARIANT. The current deployed program rejects either quote - direction with custom error 0xffff when its required first level is disabled. Use this as a - deterministic liquidity-outage scenario, not as a depth or price scenario. Do not persist it. + direction with custom error 0xffff when every level is disabled. The existing field names + address the start of each twenty-level write. Disabling only level zero is insufficient: + consumed depth or the selected configuration may start a quote at a later level. Use only + zero; enabling previously disabled levels is not supported. Do not persist this scenario. - id: tessera-stale-quote name: Make Tessera Quote Stale @@ -540,6 +475,9 @@ templates: How far behind the materialization slot to place the quote, as a negative integer. Pass null to use -20, the limit of most markets and of the default one. llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + The value you pass IS the lead: Surfpool writes the materialization slot plus it, clamped at zero. Pass null to take the -20 default. This is one template for every market, not one per limit. @@ -552,8 +490,7 @@ templates: At the boundary the current deployed program rejects the swap with custom error 65535, while one slot younger succeeds. Do not persist this override: the quote should stay stale. - Set fetchBeforeUse: true so the live market is forked first. Keep override labels short - ("SOL/USDC stale quote"). + Keep override labels short ("SOL/USDC stale quote"). - id: tessera-freshness name: Refresh Tessera Quote @@ -571,6 +508,10 @@ templates: label: Current materialization slot description: Slot lead, as an integer. Pass null to take the lead of zero and write the materialization slot itself. llm_context: | + For a direct template scenario, set fetchBeforeUse: true on the first override for each + account not yet in local state. + Builder-created scenarios keep fetchBeforeUse: false to use the prepared local snapshot. + Call list_tessera_markets and set the override account to the chosen address. The returned freshnessLimitSlots is the age at which that market rejects quotes with custom error 0xffff. diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index c343bbfcd..3338662c2 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -294,9 +294,12 @@ impl TemplateRegistry { #[cfg(test)] mod tests { - use anchor_lang_idl::types::IdlType; - use std::{collections::BTreeSet, collections::HashMap, str::FromStr}; + use std::{ + collections::{BTreeSet, HashMap}, + str::FromStr, + }; + use anchor_lang_idl::types::IdlType; use solana_pubkey::Pubkey; use surfpool_types::{AccountAddress, PdaSeed}; diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 6ce850e07..831432580 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -313,13 +313,6 @@ fn json_to_txtx_value_for_idl_type( (IdlType::Option(inner), _) if !json.is_null() => { json_to_txtx_value_for_idl_type(json, inner, idl_types) } - (IdlType::U64 | IdlType::I64, serde_json::Value::String(_)) => { - let digits = json_integer_digits(json, "64-bit integer")?; - digits - .parse::() - .map(Value::Integer) - .map_err(|e| SurfpoolError::internal(format!("Invalid integer '{digits}': {e}"))) - } (IdlType::U128, _) => { let digits = json_integer_digits(json, "u128")?; let value = digits @@ -4614,26 +4607,6 @@ mod tests { use super::*; use crate::storage::tests::TestType; - #[test_case("18446744073709551615", IdlType::U64, u64::MAX.to_le_bytes().to_vec(); "u64 max")] - #[test_case("-9223372036854775808", IdlType::I64, i64::MIN.to_le_bytes().to_vec(); "i64 min")] - fn decimal_integer_override_encodes_exactly(digits: &str, ty: IdlType, expected: Vec) { - let value = json_to_txtx_value_for_idl_type(&serde_json::json!(digits), &ty, &[]).unwrap(); - let encoded = borsh_encode_value_to_idl_type(&value, &ty, &Vec::new(), None).unwrap(); - assert_eq!(encoded, expected); - } - - #[test_case("18446744073709551616", IdlType::U64; "u64 overflow")] - #[test_case("-1", IdlType::U64; "u64 negative")] - #[test_case("9223372036854775808", IdlType::I64; "i64 overflow")] - #[test_case("-9223372036854775809", IdlType::I64; "i64 underflow")] - #[test_case("1.5", IdlType::U64; "fraction")] - fn decimal_integer_override_rejects_invalid_values(digits: &str, ty: IdlType) { - let converted = json_to_txtx_value_for_idl_type(&serde_json::json!(digits), &ty, &[]); - if let Ok(value) = converted { - assert!(borsh_encode_value_to_idl_type(&value, &ty, &Vec::new(), None).is_err()); - } - } - #[test] fn startup_status_subscription_tracks_accepted_transitions() { use surfpool_types::SurfnetStartupPhase; diff --git a/crates/core/src/tests/tessera/mod.rs b/crates/core/src/tests/tessera/mod.rs index 82f7e6c6a..a7142149a 100644 --- a/crates/core/src/tests/tessera/mod.rs +++ b/crates/core/src/tests/tessera/mod.rs @@ -39,11 +39,8 @@ const WSOL_MINT: &str = "So11111111111111111111111111111111111111112"; const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; const DFLOW_PROGRAM: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"; -const CURRENT_DEPLOY_SLOT: u64 = 438_800_691; -const CURRENT_ELF_SHA256: &str = "433f2a857ffe2045310a478b4aca0fd824308d01f283719275baec60e2aecb3b"; -const MAX_PRICE_AGE_SLOTS: u64 = 19; -/// Where the deployed program reads each market's quote rejection age. -const FRESHNESS_LIMIT_OFFSET: usize = 88; +const CURRENT_DEPLOY_SLOT: u64 = 446_053_401; +const CURRENT_ELF_SHA256: &str = "82fd37995fcece47a253b1a00c1dd7e4c3fff706b2e42384110e2cbabf4201b3"; #[derive(Clone, Copy)] struct JupiterMarketSpec { @@ -183,7 +180,7 @@ async fn tessera_fork() -> TesseraFork { let programdata = accounts.remove(0); assert_eq!(programdata.data.len(), 576_977, "ProgramData size changed"); assert_eq!( - u64::from_le_bytes(programdata.data[4..12].try_into().unwrap()), + read_u64(&programdata.data, 4), CURRENT_DEPLOY_SLOT, "Tessera was redeployed; revalidate the raw layout" ); @@ -245,13 +242,72 @@ fn native_token_account(mint: &Pubkey, owner: &Pubkey, amount: u64) -> Vec { } fn token_amount(data: &[u8]) -> u64 { - u64::from_le_bytes(data[64..72].try_into().unwrap()) + read_u64(data, 64) } fn token_owner(data: &[u8]) -> Pubkey { Pubkey::new_from_array(data[32..64].try_into().expect("token owner")) } +fn last_restart_slot_account() -> Account { + Account { + lamports: 1_000_000, + data: 246_464_040u64.to_le_bytes().to_vec(), + owner: Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"), + executable: false, + rent_epoch: 0, + } +} + +fn user_token_account( + mint: &Pubkey, + owner: &Pubkey, + token_program: Pubkey, + amount: u64, +) -> Account { + let is_native = mint == &Pubkey::from_str_const(WSOL_MINT); + Account { + lamports: if is_native { + amount.saturating_add(2_039_280) + } else { + 10_000_000 + }, + data: if is_native { + native_token_account(mint, owner, amount) + } else { + token_account(mint, owner, amount) + }, + owner: token_program, + executable: false, + rent_epoch: 0, + } +} + +fn sign_and_send( + svm: &mut litesvm::LiteSVM, + taker: &solana_keypair::Keypair, + instructions: &[Instruction], + destination_key: Pubkey, +) -> Result { + use solana_signer::Signer; + use solana_transaction::Transaction; + + let mut message = solana_message::Message::new(instructions, Some(&taker.pubkey())); + message.recent_blockhash = svm.latest_blockhash(); + let signature_count = message.header.num_required_signatures as usize; + let mut transaction = Transaction::new_unsigned(message); + transaction.signatures = vec![solana_signature::Signature::default(); signature_count]; + transaction.signatures[0] = taker.sign_message(&transaction.message.serialize()); + + svm.send_transaction(transaction) + .map_err(|error| format!("{error:?}"))?; + Ok(token_amount( + &svm.get_account(&destination_key) + .expect("destination account") + .data, + )) +} + fn tessera_run( fork: &TesseraFork, amount_in: u64, @@ -263,7 +319,6 @@ fn tessera_run( use litesvm::LiteSVM; use solana_keypair::Keypair; use solana_signer::Signer; - use solana_transaction::Transaction; let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); let global_state_key = Pubkey::from_str_const(TESSERA_GLOBAL_STATE); @@ -277,7 +332,7 @@ fn tessera_run( let config_key = Pubkey::from_str_const(TESSERA_V11_CONFIG); let market_record_key = Pubkey::from_str_const(TESSERA_V11_MARKET_RECORD); let mut market = fork.market.data.clone(); - let market_slot = u64::from_le_bytes(market[120..128].try_into().unwrap()); + let market_slot = read_u64(&market, 120); mutate(&mut market); let mut svm = LiteSVM::new() @@ -295,13 +350,7 @@ fn tessera_run( svm.set_sysvar(&clock); svm.set_account( Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), - Account { - lamports: 1_000_000, - data: 246_464_040u64.to_le_bytes().to_vec(), - owner: Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"), - executable: false, - rent_epoch: 0, - }, + last_restart_slot_account(), ) .map_err(|error| format!("set last restart slot: {error:?}"))?; svm.set_account(global_state_key, fork.global_state.clone()) @@ -340,28 +389,16 @@ fn tessera_run( } else { (destination_key, source_key) }; - let user_account = |mint: &Pubkey, amount: u64| { - let is_native = mint == &base_mint_key; - Account { - lamports: if is_native { - amount.saturating_add(2_039_280) - } else { - 10_000_000 - }, - data: if is_native { - native_token_account(mint, &taker.pubkey(), amount) - } else { - token_account(mint, &taker.pubkey(), amount) - }, - owner: token_program, - executable: false, - rent_epoch: 0, - } - }; - svm.set_account(source_key, user_account(&source_mint, amount_in)) - .map_err(|error| format!("set source: {error:?}"))?; - svm.set_account(destination_key, user_account(&destination_mint, 0)) - .map_err(|error| format!("set destination: {error:?}"))?; + svm.set_account( + source_key, + user_token_account(&source_mint, &taker.pubkey(), token_program, amount_in), + ) + .map_err(|error| format!("set source: {error:?}"))?; + svm.set_account( + destination_key, + user_token_account(&destination_mint, &taker.pubkey(), token_program, 0), + ) + .map_err(|error| format!("set destination: {error:?}"))?; let mut data = vec![0x11, direction]; data.extend_from_slice(&amount_in.to_le_bytes()); @@ -402,20 +439,7 @@ fn tessera_run( data, }, ]; - let mut message = solana_message::Message::new(&instructions, Some(&taker.pubkey())); - message.recent_blockhash = svm.latest_blockhash(); - let signature_count = message.header.num_required_signatures as usize; - let mut transaction = Transaction::new_unsigned(message); - transaction.signatures = vec![solana_signature::Signature::default(); signature_count]; - transaction.signatures[0] = taker.sign_message(&transaction.message.serialize()); - - svm.send_transaction(transaction) - .map_err(|error| format!("{error:?}"))?; - Ok(token_amount( - &svm.get_account(&destination_key) - .expect("destination account") - .data, - )) + sign_and_send(&mut svm, &taker, &instructions, destination_key) } fn tessera_run_jupiter( @@ -428,7 +452,6 @@ fn tessera_run_jupiter( use litesvm::LiteSVM; use solana_keypair::Keypair; use solana_signer::Signer; - use solana_transaction::Transaction; let program_id = Pubkey::from_str_const(TESSERA_PROGRAM); let global_state_key = Pubkey::from_str_const(TESSERA_GLOBAL_STATE); @@ -440,6 +463,8 @@ fn tessera_run_jupiter( let base_token_program = market_fork.base_mint.owner; let quote_token_program = market_fork.quote_mint.owner; let instructions_sysvar = Pubkey::from_str_const("Sysvar1nstructions1111111111111111111111111"); + let config_key = Pubkey::from_str_const(TESSERA_V11_CONFIG); + let market_record_key = Pubkey::from_str_const(TESSERA_V11_MARKET_RECORD); let mut market = market_fork.market.data.clone(); let market_slot = read_u64(&market, 120); mutate(&mut market); @@ -459,17 +484,15 @@ fn tessera_run_jupiter( svm.set_sysvar(&clock); svm.set_account( Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), - Account { - lamports: 1_000_000, - data: 246_464_040u64.to_le_bytes().to_vec(), - owner: Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"), - executable: false, - rent_epoch: 0, - }, + last_restart_slot_account(), ) .map_err(|error| format!("set last restart slot: {error:?}"))?; svm.set_account(global_state_key, fork.global_state.clone()) .map_err(|error| format!("set global state: {error:?}"))?; + svm.set_account(config_key, fork.config.clone()) + .map_err(|error| format!("set config: {error:?}"))?; + svm.set_account(market_record_key, fork.market_record.clone()) + .map_err(|error| format!("set market record: {error:?}"))?; let mut market_account = market_fork.market.clone(); market_account.data = market; svm.set_account(market_key, market_account) @@ -508,32 +531,14 @@ fn tessera_run_jupiter( } else { (destination_key, source_key) }; - let user_account = |mint: &Pubkey, token_program: Pubkey, amount: u64| { - let is_native = mint == &Pubkey::from_str_const(WSOL_MINT); - Account { - lamports: if is_native { - amount.saturating_add(2_039_280) - } else { - 10_000_000 - }, - data: if is_native { - native_token_account(mint, &taker.pubkey(), amount) - } else { - token_account(mint, &taker.pubkey(), amount) - }, - owner: token_program, - executable: false, - rent_epoch: 0, - } - }; svm.set_account( source_key, - user_account(&source_mint, source_program, amount_in), + user_token_account(&source_mint, &taker.pubkey(), source_program, amount_in), ) .map_err(|error| format!("set source: {error:?}"))?; svm.set_account( destination_key, - user_account(&destination_mint, destination_program, 0), + user_token_account(&destination_mint, &taker.pubkey(), destination_program, 0), ) .map_err(|error| format!("set destination: {error:?}"))?; @@ -556,23 +561,12 @@ fn tessera_run_jupiter( AccountMeta::new_readonly(base_token_program, false), AccountMeta::new_readonly(quote_token_program, false), AccountMeta::new_readonly(instructions_sysvar, false), + AccountMeta::new_readonly(config_key, false), + AccountMeta::new_readonly(market_record_key, false), ], data, }; - let mut message = solana_message::Message::new(&[instruction], Some(&taker.pubkey())); - message.recent_blockhash = svm.latest_blockhash(); - let signature_count = message.header.num_required_signatures as usize; - let mut transaction = Transaction::new_unsigned(message); - transaction.signatures = vec![solana_signature::Signature::default(); signature_count]; - transaction.signatures[0] = taker.sign_message(&transaction.message.serialize()); - - svm.send_transaction(transaction) - .map_err(|error| format!("{error:?}"))?; - Ok(token_amount( - &svm.get_account(&destination_key) - .expect("destination account") - .data, - )) + sign_and_send(&mut svm, &taker, &[instruction], destination_key) } fn write_u64(data: &mut [u8], offset: usize, value: u64) { @@ -597,39 +591,40 @@ fn expected_first_level_output(market: &[u8], amount_in: u64, direction: u8) -> u64::try_from(output).expect("Tessera first-level quote fits u64") } -/// Scales one side of a ladder the way a caller composing the raw template would. -/// -/// `first_offset` is the field's offset in level 0; records are 24 bytes apart. Returns the -/// template values for both directions, only one of which is actually scaled. -fn scale_ladder( - market: &[u8], - field: &str, - sell_bps: u16, - buy_bps: u16, -) -> HashMap { - const SELL_AMOUNT: usize = 160; - const BUY_AMOUNT: usize = 640; - const FACTOR_IN_RECORD: usize = 8; - - let base = if field == "factor" { - FACTOR_IN_RECORD - } else { - 0 - }; +// A small input can still start beyond level zero because prior flow and the selected +// configuration advance the ladder. Formula probes explicitly isolate an unconsumed start. +/// Puts the local fixture in the state the first-level formula assumes: no consumed depth at +/// 0/8, and all five selectable configurations neutral (no ppm adjustment at 1136, factor scale +/// 1,000,000 at 1140, `skipped_levels` leading levels at 1144). Which configuration the program +/// selects depends on the market record's age, so every one of them is normalized. +fn set_quote_start(data: &mut [u8], skipped_levels: u8) { + write_u64(data, 0, 0); + write_u64(data, 8, 0); + for selector in 0..5 { + let config = 1136 + 12 * selector; + data[config..config + 4].copy_from_slice(&0u32.to_le_bytes()); + data[config + 4..config + 8].copy_from_slice(&1_000_000u32.to_le_bytes()); + data[config + 8] = skipped_levels; + } +} + +fn scale_ladder(market: &[u8], sell_bps: u16, buy_bps: u16) -> HashMap { + const SELL_FACTOR: usize = 168; + const BUY_FACTOR: usize = 648; let mut values = HashMap::with_capacity(LADDER_LEVELS * 2); for (side, first_offset, bps) in [ - ("sell_levels", SELL_AMOUNT + base, sell_bps), - ("buy_levels", BUY_AMOUNT + base, buy_bps), + ("sell_levels", SELL_FACTOR, sell_bps), + ("buy_levels", BUY_FACTOR, buy_bps), ] { for level in 0..LADDER_LEVELS { let live = read_u64(market, first_offset + level * LADDER_RECORD_SIZE); let scaled = (u128::from(live) * u128::from(bps) / 10_000) as u64; assert!( live == 0 || scaled > 0, - "{side}.{level}.{field} rounds a live nonzero value to zero at {bps} bps" + "{side}.{level}.factor rounds a live nonzero value to zero at {bps} bps" ); values.insert( - format!("{side}.{level}.{field}"), + format!("{side}.{level}.factor"), serde_json::json!(scaled.to_string()), ); } @@ -691,42 +686,18 @@ async fn tessera_templates_guard_market_and_preserve_unwritten_bytes() { assert_eq!(market.market.data.len(), 1264); assert!(layout.guard(&market.market.data).is_ok()); } - let mut wrong_layout_tag = fork.market.data.clone(); - wrong_layout_tag[96] ^= 1; - assert!(layout.guard(&wrong_layout_tag).is_err()); + let mut wrong_guard_bytes = fork.market.data.clone(); + wrong_guard_bytes[96] ^= 1; + assert!(layout.guard(&wrong_guard_bytes).is_err()); assert!(layout.guard(&fork.market.data[..1263]).is_err()); - let price_values = HashMap::from([ - ( - "quote_atoms_per_base_atom_x1e15".to_string(), - serde_json::json!(100_000_000_000_000u64), - ), - ( - "base_atoms_per_quote_atom_x1e15".to_string(), - serde_json::json!(10_000_000_000_000_000u64), - ), - ]); - let repriced = layout - .materialize(&fork.market.data, &fair_value.properties, &price_values, 0) - .expect("fair value materializes"); - assert_eq!( - u64::from_le_bytes(repriced[128..136].try_into().unwrap()), - 100_000_000_000_000 - ); - assert_eq!( - u64::from_le_bytes(repriced[144..152].try_into().unwrap()), - 10_000_000_000_000_000 - ); - assert_only_ranges_changed(&fork.market.data, &repriced, &[(128, 136), (144, 152)]); - let template = registry.get("tessera-depth").expect("ladder template"); let values: HashMap = template .properties .iter() .map(|property| { let offset = property.offset.expect("raw property offset"); - let current = - u64::from_le_bytes(fork.market.data[offset..offset + 8].try_into().unwrap()); + let current = read_u64(&fork.market.data, offset); (property.path.clone(), serde_json::json!(current / 2)) }) .collect(); @@ -742,32 +713,11 @@ async fn tessera_templates_guard_market_and_preserve_unwritten_bytes() { .map(|property| { let offset = property.offset.expect("raw property offset"); let expected = values[&property.path].as_u64().expect("u64 value"); - assert_eq!( - u64::from_le_bytes(forged[offset..offset + 8].try_into().unwrap()), - expected - ); + assert_eq!(read_u64(&forged, offset), expected); (offset, offset + 8) }) .collect(); assert_only_ranges_changed(&fork.market.data, &forged, &ranges); - - let freshness = registry.get("tessera-freshness").expect("freshness"); - let refreshed = freshness - .raw_layout - .as_ref() - .expect("raw layout") - .materialize( - &fork.market.data, - &freshness.properties, - &HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), - 987_654, - ) - .expect("freshness materializes"); - assert_eq!( - u64::from_le_bytes(refreshed[120..128].try_into().unwrap()), - 987_654 - ); - assert_only_ranges_changed(&fork.market.data, &refreshed, &[(120, 128)]); } #[tokio::test] @@ -832,40 +782,12 @@ async fn tessera_builders_materialize_and_keep_quotes_fresh() { preparation.base_atoms_per_quote_atom_x1e15 ); assert_only_ranges_changed(&materialized, &next_slot, &[(120, 128)]); - - let local = svm.inner.get_account(&market_key).unwrap().unwrap(); - let depth = build_tessera_depth_scenario(market_key, &local, 1000, 10000).unwrap(); - svm.register_scenario(depth, Some(BASE_SLOT + 2)).unwrap(); - assert_eq!( - svm.inner.get_account(&market_key).unwrap().unwrap().data, - local.data - ); - svm.materialize_overrides_for_slot(&None, BASE_SLOT + 2) - .await - .unwrap(); - let reduced = svm.inner.get_account(&market_key).unwrap().unwrap().data; - let mut expected = local.data.clone(); - for level in 0..20 { - let offset = 160 + level * 24; - if local.data[offset + 16] != 0 { - write_u64(&mut expected, offset, read_u64(&local.data, offset) / 10); - } - } - write_u64(&mut expected, 120, BASE_SLOT + 2); - assert_eq!(reduced, expected); - svm.materialize_overrides_for_slot(&None, BASE_SLOT + 3) - .await - .unwrap(); - write_u64(&mut expected, 120, BASE_SLOT + 3); - assert_eq!( - svm.inner.get_account(&market_key).unwrap().unwrap().data, - expected - ); } #[tokio::test] async fn tessera_depth_builder_prepares_cbb_swaps_in_both_directions() { - let fork = tessera_fork().await; + let mut fork = tessera_fork().await; + set_quote_start(&mut fork.jupiter_markets[0].market.data, 0); let cbb = &fork.jupiter_markets[0]; let address = Pubkey::from_str_const(cbb.spec.address); let slot = read_u64(&cbb.market.data, 120) + 1; @@ -1003,7 +925,8 @@ async fn tessera_stale_quote_template_lands_every_configured_rejection_boundary( #[tokio::test] async fn tessera_current_layout_controls_price_depth_and_freshness() { - let fork = tessera_fork().await; + let mut fork = tessera_fork().await; + set_quote_start(&mut fork.market.data, 0); assert_eq!(fork.market.data.len(), 1264, "market layout size changed"); assert_eq!( &fork.market.data[24..56], @@ -1017,13 +940,13 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { let amount_in = 238_781_608; let baseline = tessera_run(&fork, amount_in, 1, true, false, |_| {}).expect("baseline sell"); let inverse_only = tessera_run(&fork, amount_in, 1, true, false, |market| { - let inverse = u64::from_le_bytes(market[144..152].try_into().unwrap()); + let inverse = read_u64(market, 144); write_u64(market, 144, inverse / 2); }) .expect("sell with buy-side-only mutation"); let doubled = tessera_run(&fork, amount_in, 1, true, false, |market| { - let price = u64::from_le_bytes(market[128..136].try_into().unwrap()); - let inverse = u64::from_le_bytes(market[144..152].try_into().unwrap()); + let price = read_u64(market, 128); + let inverse = read_u64(market, 144); apply_template( market, "tessera-fair-value", @@ -1045,13 +968,13 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { let baseline_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |_| {}).expect("baseline buy"); let direct_only_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { - let price = u64::from_le_bytes(market[128..136].try_into().unwrap()); + let price = read_u64(market, 128); write_u64(market, 128, price * 2); }) .expect("buy with sell-side-only mutation"); let doubled_price_buy = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { - let price = u64::from_le_bytes(market[128..136].try_into().unwrap()); - let inverse = u64::from_le_bytes(market[144..152].try_into().unwrap()); + let price = read_u64(market, 128); + let inverse = read_u64(market, 144); apply_template( market, "tessera-fair-value", @@ -1069,78 +992,8 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { ); }) .expect("doubled-price buy"); - let large_sell = 30_000_000_000; - let depth = |sell, buy| { - build_tessera_depth_scenario( - Pubkey::from_str_const(TESSERA_SOL_USDC_MARKET), - &fork.market, - sell, - buy, - ) - .expect("build depth scenario") - .overrides - .remove(0) - .values - }; - let thin_sell_values = depth(1_000, 10_000); - let thin_buy_values = depth(10_000, 1_000); - let thin_sell_curve_values = scale_ladder(&fork.market.data, "factor", 5_000, 10_000); - let thin_buy_curve_values = scale_ladder(&fork.market.data, "factor", 10_000, 5_000); - let half_sell_curve = tessera_run(&fork, amount_in, 1, true, false, |market| { - apply_template(market, "tessera-curve", thin_sell_curve_values.clone(), 0); - }) - .expect("sell with half sell factors"); - let inactive_sell_curve = tessera_run(&fork, amount_in, 1, true, false, |market| { - apply_template(market, "tessera-curve", thin_buy_curve_values.clone(), 0); - }) - .expect("sell with half buy factors"); - let half_buy_curve = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { - apply_template(market, "tessera-curve", thin_buy_curve_values.clone(), 0); - }) - .expect("buy with half buy factors"); - let inactive_buy_curve = tessera_run(&fork, buy_amount_in, 0, true, false, |market| { - apply_template(market, "tessera-curve", thin_sell_curve_values.clone(), 0); - }) - .expect("buy with half sell factors"); - let large_sell_baseline = - tessera_run(&fork, large_sell, 1, true, false, |_| {}).expect("large sell"); - let thin_sell = tessera_run(&fork, large_sell, 1, true, false, |market| { - apply_template(market, "tessera-depth", thin_sell_values.clone(), 0); - }) - .expect("large sell with thin sell ladder"); - let inactive_sell_depth = tessera_run(&fork, large_sell, 1, true, false, |market| { - apply_template(market, "tessera-depth", thin_buy_values.clone(), 0); - }) - .expect("large sell with buy-side depth mutation"); - let large_buy = 3_000_000_000; - let large_buy_baseline = - tessera_run(&fork, large_buy, 0, true, false, |_| {}).expect("large buy"); - let thin_buy = tessera_run(&fork, large_buy, 0, true, false, |market| { - apply_template(market, "tessera-depth", thin_buy_values.clone(), 0); - }) - .expect("large buy with thin buy ladder"); - let inactive_buy_depth = tessera_run(&fork, large_buy, 0, true, false, |market| { - apply_template(market, "tessera-depth", thin_sell_values.clone(), 0); - }) - .expect("large buy with sell-side depth mutation"); - let market_slot = u64::from_le_bytes(fork.market.data[120..128].try_into().unwrap()); + let market_slot = read_u64(&fork.market.data, 120); let clock_slot = market_slot + 1; - let fresh_at_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { - apply_template( - market, - "tessera-freshness", - HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), - clock_slot.saturating_sub(MAX_PRICE_AGE_SLOTS), - ); - }); - let stale_after_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { - apply_template( - market, - "tessera-freshness", - HashMap::from([("last_update_slot".to_string(), serde_json::json!(0))]), - clock_slot.saturating_sub(MAX_PRICE_AGE_SLOTS + 1), - ); - }); let configured_freshness_boundary = 5; let fresh_at_configured_boundary = tessera_run(&fork, amount_in, 1, true, false, |market| { write_u64(market, 88, configured_freshness_boundary); @@ -1171,7 +1024,7 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { expected_first_level_output(&fork.market.data, buy_amount_in, 0); eprintln!( - "Tessera sell={baseline}, expected_sell={expected_first_level_sell_output}, inverse_only={inverse_only}, doubled={doubled}, buy={baseline_buy}, expected_buy={expected_first_level_buy_output}, direct_only_buy={direct_only_buy}, doubled_price_buy={doubled_price_buy}, large_sell={large_sell_baseline}, thin_sell={thin_sell}, large_buy={large_buy_baseline}, thin_buy={thin_buy}, freshness_boundary={MAX_PRICE_AGE_SLOTS}, fresh={fresh_at_boundary:?}, stale={stale_after_boundary:?}" + "Tessera sell={baseline}, expected_sell={expected_first_level_sell_output}, inverse_only={inverse_only}, doubled={doubled}, buy={baseline_buy}, expected_buy={expected_first_level_buy_output}, direct_only_buy={direct_only_buy}, doubled_price_buy={doubled_price_buy}" ); assert!( baseline > 0, @@ -1197,20 +1050,6 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { ); assert!(unsigned_sentinel.contains("Custom(0)")); assert!(writable_global.contains("Custom(1)")); - assert!(thin_sell < large_sell_baseline); - assert_eq!(inactive_sell_depth, large_sell_baseline); - assert!(thin_buy < large_buy_baseline); - assert_eq!(inactive_buy_depth, large_buy_baseline); - assert!(half_sell_curve > baseline * 49 / 100 && half_sell_curve < baseline * 51 / 100); - assert_eq!(inactive_sell_curve, baseline); - assert!(half_buy_curve > baseline_buy * 49 / 100 && half_buy_curve < baseline_buy * 51 / 100); - assert_eq!(inactive_buy_curve, baseline_buy); - assert!(fresh_at_boundary.is_ok()); - assert!( - stale_after_boundary - .expect_err("age 20 must be rejected") - .contains("Custom(65535)") - ); assert!(fresh_at_configured_boundary.is_ok()); assert!( stale_at_configured_boundary @@ -1221,7 +1060,8 @@ async fn tessera_current_layout_controls_price_depth_and_freshness() { #[tokio::test] async fn tessera_cbb_market_proves_generic_price_and_curve_layout() { - let fork = tessera_fork().await; + let mut fork = tessera_fork().await; + set_quote_start(&mut fork.jupiter_markets[0].market.data, 0); let cbb = &fork.jupiter_markets[0]; let market_key = Pubkey::from_str_const(TESSERA_CBB_USDC_MARKET); let market = TesseraMarket::validate(market_key, &cbb.market, &cbb.base_mint, &cbb.quote_mint) @@ -1231,39 +1071,12 @@ async fn tessera_cbb_market_proves_generic_price_and_curve_layout() { assert_eq!(market.base_decimals, 8); assert_eq!(market.quote_decimals, 6); - let preparation = build_tessera_fair_value_scenario(&market, "78.8477010015472512") - .expect("build decimal-aware CBB fair value"); - assert_eq!(preparation.market, market_key); - assert_eq!( - preparation.quote_atoms_per_base_atom_x1e15, - 788_477_010_015_472 - ); - - let amount_in = cbb.spec.amount_in; - let active_curve_values = scale_ladder(&cbb.market.data, "factor", 5_000, 10_000); - let inactive_curve_values = scale_ladder(&cbb.market.data, "factor", 10_000, 5_000); + let amount_in = cbb.spec.amount_in.min(read_u64(&cbb.market.data, 160) / 2); + assert!(amount_in > 0); + let active_curve_values = scale_ladder(&cbb.market.data, 5_000, 10_000); + let inactive_curve_values = scale_ladder(&cbb.market.data, 10_000, 5_000); let baseline = tessera_run_jupiter(&fork, cbb, amount_in, 1, |_| {}).expect("CBB baseline sell"); - let doubled = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { - let direct = read_u64(market, 128); - let inverse = read_u64(market, 144); - apply_template( - market, - "tessera-fair-value", - HashMap::from([ - ( - "quote_atoms_per_base_atom_x1e15".to_string(), - serde_json::json!((direct * 2).to_string()), - ), - ( - "base_atoms_per_quote_atom_x1e15".to_string(), - serde_json::json!((inverse / 2).to_string()), - ), - ]), - 0, - ); - }) - .expect("CBB doubled-price sell"); let factors_half = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { apply_template(market, "tessera-curve", active_curve_values.clone(), 0); }) @@ -1272,18 +1085,6 @@ async fn tessera_cbb_market_proves_generic_price_and_curve_layout() { apply_template(market, "tessera-curve", inactive_curve_values.clone(), 0); }) .expect("CBB inactive-factor control"); - let trailing_candidates_double = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { - for offset in [1140usize, 1152, 1164, 1176, 1188] { - let value = u32::from_le_bytes(market[offset..offset + 4].try_into().unwrap()); - market[offset..offset + 4].copy_from_slice(&(value * 2).to_le_bytes()); - } - }) - .expect("CBB doubled trailing-candidate sell"); - let trailing_candidates_lower = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { - for offset in [1140usize, 1152, 1164, 1176, 1188] { - market[offset..offset + 4].copy_from_slice(&999_999u32.to_le_bytes()); - } - }); let invalid_single_factor = tessera_run_jupiter(&fork, cbb, amount_in, 1, |market| { write_u64(market, 168, 500_000); }) @@ -1298,22 +1099,23 @@ async fn tessera_cbb_market_proves_generic_price_and_curve_layout() { baseline, expected_first_level_output(&cbb.market.data, amount_in, 1) ); - assert!(doubled > baseline * 19 / 10 && doubled < baseline * 21 / 10); assert!(factors_half > baseline * 49 / 100 && factors_half < baseline * 51 / 100); assert_eq!(inactive_factors_half, baseline); - assert_eq!(trailing_candidates_double, baseline); - assert_eq!(trailing_candidates_lower, Ok(baseline)); assert!(invalid_single_factor.contains("Custom(8)")); assert!(disabled_first_level.contains("Custom(65535)")); } #[tokio::test] async fn tessera_halt_template_rejects_both_quote_directions() { - let fork = tessera_fork().await; - let cbb = &fork.jupiter_markets[0]; - let apply_halt = |market: &mut Vec| { + let mut fork = tessera_fork().await; + for skipped_levels in [None, Some(1)] { + if let Some(skip) = skipped_levels { + set_quote_start(&mut fork.jupiter_markets[0].market.data, skip); + } + let cbb = &fork.jupiter_markets[0]; + let mut halted = cbb.market.data.clone(); apply_template( - market, + &mut halted, "tessera-halt", HashMap::from([ ("sell_level_0_enabled".to_string(), serde_json::json!(0)), @@ -1321,27 +1123,45 @@ async fn tessera_halt_template_rejects_both_quote_directions() { ]), 0, ); - }; - - let sell_amount = cbb.spec.amount_in; - let buy_amount = 4_000_000; - assert!(tessera_run_jupiter(&fork, cbb, sell_amount, 1, |_| {}).is_ok()); - assert!(tessera_run_jupiter(&fork, cbb, buy_amount, 0, |_| {}).is_ok()); - - let halted_sell = tessera_run_jupiter(&fork, cbb, sell_amount, 1, apply_halt) - .expect_err("halted sell direction must fail"); - let halted_buy = tessera_run_jupiter(&fork, cbb, buy_amount, 0, apply_halt) - .expect_err("halted buy direction must fail"); - assert!(halted_sell.contains("Custom(65535)")); - assert!(halted_buy.contains("Custom(65535)")); + let mut expected = cbb.market.data.clone(); + for start in [176, 656] { + for level in 0..LADDER_LEVELS { + expected[start + level * LADDER_RECORD_SIZE] = 0; + } + } + assert_eq!(halted, expected); + for (direction, amount) in [(1, cbb.spec.amount_in), (0, 4_000_000)] { + let baseline = tessera_run_jupiter(&fork, cbb, amount, direction, |_| {}).unwrap(); + assert!(baseline > 0); + if skipped_levels.is_some() { + let first_only = tessera_run_jupiter(&fork, cbb, amount, direction, |data| { + data[176] = 0; + data[656] = 0; + }) + .unwrap(); + assert_eq!( + first_only, baseline, + "disabling skipped levels cannot halt a quote" + ); + } + let error = tessera_run_jupiter(&fork, cbb, amount, direction, |data| { + *data = halted.clone(); + }) + .expect_err("halting every level must reject either direction"); + assert!(error.contains("Custom(65535)"), "{error}"); + } + } } #[tokio::test] async fn tessera_four_additional_markets_prove_price_and_curve_directions() { - let fork = tessera_fork().await; + let mut fork = tessera_fork().await; + for market in &mut fork.jupiter_markets[1..] { + set_quote_start(&mut market.market.data, 0); + } for market_fork in &fork.jupiter_markets[1..] { let market_key = Pubkey::from_str_const(market_fork.spec.address); - // Every one of these markets must clear the same owner, size and layout-tag guard. + // Every one of these markets must clear the same owner, size and byte guard. TesseraMarket::validate( market_key, &market_fork.market, @@ -1351,7 +1171,18 @@ async fn tessera_four_additional_markets_prove_price_and_curve_directions() { .unwrap_or_else(|error| panic!("{} validation failed: {error}", market_fork.spec.address)); let direction = market_fork.spec.direction; - let amount_in = market_fork.spec.amount_in; + // Live ladders move between runs; the first-level formula only holds while the probe + // fits in level 1, so the probe follows the live capacity down. + let first_capacity = read_u64( + &market_fork.market.data, + if direction == 1 { 160 } else { 640 }, + ); + assert!( + first_capacity >= 2, + "{} has insufficient first-level capacity for the probe: {first_capacity}", + market_fork.spec.address + ); + let amount_in = market_fork.spec.amount_in.min(first_capacity / 2); let baseline = tessera_run_jupiter(&fork, market_fork, amount_in, direction, |_| {}) .unwrap_or_else(|error| { panic!("{} baseline failed: {error}", market_fork.spec.address) @@ -1370,18 +1201,10 @@ async fn tessera_four_additional_markets_prove_price_and_curve_directions() { } else { (10_000, 5_000) }; - let active_curve_values = scale_ladder( - &market_fork.market.data, - "factor", - active_sell_bps, - active_buy_bps, - ); - let inactive_curve_values = scale_ladder( - &market_fork.market.data, - "factor", - active_buy_bps, - active_sell_bps, - ); + let active_curve_values = + scale_ladder(&market_fork.market.data, active_sell_bps, active_buy_bps); + let inactive_curve_values = + scale_ladder(&market_fork.market.data, active_buy_bps, active_sell_bps); let active_factors_half = tessera_run_jupiter(&fork, market_fork, amount_in, direction, |market| { apply_template(market, "tessera-curve", active_curve_values.clone(), 0) @@ -1467,20 +1290,8 @@ async fn tessera_discovers_live_markets() { ) .unwrap(); assert_eq!(*market, expected); - assert_eq!( - market.freshness_limit_slots, - read_u64(&account.data, FRESHNESS_LIMIT_OFFSET) - ); assert!(!market.label().is_empty()); } - let registry = TemplateRegistry::new(); - assert!( - !registry - .get("tessera-fair-value") - .unwrap() - .constants - .contains_key("market") - ); eprintln!( "Discovered {} Tessera markets from program accounts", markets.len() diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 37651d608..0ef4ebe29 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -21,8 +21,6 @@ rmcp = { workspace = true, features = ["transport-io", "transport-sse-server", " serde = { workspace = true } serde_json = { workspace = true } serde_yaml = "0.9" -solana-account = { workspace = true } -solana-commitment-config = { workspace = true } solana-keypair = { workspace = true } solana-pubkey = { workspace = true } solana-signer = { workspace = true } diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index f90637059..01732dad3 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -19,12 +19,14 @@ use start_surfnet::StartSurfnetResponse; use surfpool_core::{ scenarios::{ TemplateRegistry, - protocols::pump::v1::graduation_builder::{ - build_pump_graduation_scenario, pump_graduation_addresses, - }, - protocols::tessera::v1::{ - TesseraMarket, build_tessera_depth_scenario, build_tessera_fair_value_scenario, - discover_tessera_markets, + protocols::{ + pump::v1::graduation_builder::{ + build_pump_graduation_scenario, pump_graduation_addresses, + }, + tessera::v1::{ + TESSERA_DEFAULT_MARKET, TesseraMarket, build_tessera_depth_scenario, + build_tessera_fair_value_scenario, discover_tessera_markets, + }, }, }, solana_account::Account, @@ -41,13 +43,6 @@ use crate::helpers::find_next_available_surfnet_port; mod set_token_account; mod start_surfnet; -fn scenario_tool_error(message: String) -> CallToolResult { - let response = RegisterScenarioResponse::error(message); - CallToolResult::success(vec![Content::text( - serde_json::to_string(&response).unwrap_or_default(), - )]) -} - #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct CreateTesseraFairValueScenarioParams { @@ -384,6 +379,13 @@ impl RegisterScenarioResponse { } } +fn scenario_tool_error(message: String) -> CallToolResult { + let response = RegisterScenarioResponse::error(message); + CallToolResult::success(vec![Content::text( + serde_json::to_string(&response).unwrap_or_default(), + )]) +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct GetTokenAddressParams { #[schemars(description = "The token symbol to look up (e.g., 'USDC', 'SOL', 'JUP')")] @@ -423,7 +425,23 @@ impl TokenAddressResponse { } } +fn parse_market(address: &str) -> Result { + Pubkey::from_str(address.trim()) + .map_err(|error| format!("Invalid Tessera market pubkey: {error}")) +} + impl Surfpool { + async fn tessera_market_account( + &self, + surfnet_port: Option, + market: Pubkey, + ) -> Result { + self.fetch_surfnet_accounts(surfnet_port, &[market]) + .await? + .remove(0) + .ok_or_else(|| format!("Tessera market account {market} was not found")) + } + /// Reads through the surfnet's own RPC: local state wins, only missing /// accounts fall back to its remote source. async fn fetch_surfnet_accounts( @@ -444,10 +462,6 @@ impl Surfpool { .collect()) } - /// Posts a scenario to this process's own scenarios API and returns the editor link. - /// - /// The self-call uses the bind address; the advertised Studio URL is a different axis and - /// may not be reachable from here. async fn stage_scenario(&self, scenario: Scenario) -> Result { let endpoint = format!( "http://127.0.0.1:{}/v1/scenarios", @@ -462,42 +476,49 @@ impl Surfpool { { Ok(response) => response, Err(error) => { - return Ok(scenario_tool_error(format!( + let response = RegisterScenarioResponse::error(format!( "Failed to load scenarios at {endpoint}: {error}" - ))); + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); } }; let status = response.status(); let body = match response.text().await { Ok(body) => body, Err(error) => { - return Ok(scenario_tool_error(format!( + let response = RegisterScenarioResponse::error(format!( "Failed to read response text: {error}" - ))); + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); } }; - let parsed: serde_json::Value = match serde_json::from_str(&body) { - Ok(parsed) => parsed, + let response: serde_json::Value = match serde_json::from_str(&body) { + Ok(response) => response, Err(error) => { - return Ok(scenario_tool_error(format!( + let response = RegisterScenarioResponse::error(format!( "Failed to parse JSON response: {error}. Response: {body}" - ))); + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); } }; - - // A different scenario already occupies this id: say so instead of - // reporting a success the model would trust if status == reqwest::StatusCode::CONFLICT { - return Ok(scenario_tool_error(format!( + let response = RegisterScenarioResponse::error(format!( "A different scenario is already stored under id {:?}. Pick another id, or delete the existing one first.", scenario.id - ))); + )); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); } - if let Some(error) = parsed.get("error") { - return Ok(scenario_tool_error(format!("RPC error: {error}"))); + if let Some(error) = response.get("error") { + let response = RegisterScenarioResponse::error(format!("RPC error: {error}")); + let json = serde_json::to_string(&response).unwrap_or_default(); + return Ok(CallToolResult::success(vec![Content::text(json)])); } - let scenario_id = parsed + let scenario_id = response .get("id") .and_then(|value| value.as_str()) .unwrap_or(&scenario.id); @@ -506,9 +527,8 @@ impl Surfpool { CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED ); let response = RegisterScenarioResponse::success(url); - Ok(CallToolResult::success(vec![Content::text( - serde_json::to_string(&response).unwrap_or_default(), - )])) + let json = serde_json::to_string(&response).unwrap_or_default(); + Ok(CallToolResult::success(vec![Content::text(json)])) } } @@ -1083,66 +1103,41 @@ impl Surfpool { &self, Parameters(params): Parameters, ) -> Result { - let market_address = match params.market.as_deref().map(str::trim) { - None | Some("") => { - surfpool_core::scenarios::protocols::tessera::v1::TESSERA_DEFAULT_MARKET - } - Some(address) => match Pubkey::from_str(address) { - Ok(market) => market, - Err(error) => { - return Ok(scenario_tool_error(format!( - "Invalid Tessera market pubkey: {error}" - ))); - } - }, - }; - - let accounts = match self - .fetch_surfnet_accounts(params.surfnet_port, &[market_address]) - .await - { - Ok(accounts) => accounts, - Err(error) => return Ok(scenario_tool_error(error)), - }; - let Some(market_account) = accounts[0].as_ref() else { - return Ok(scenario_tool_error(format!( - "Tessera market account {market_address} was not found" - ))); - }; - let (base_mint, quote_mint) = match TesseraMarket::mint_addresses(market_account) { - Ok(mints) => mints, - Err(error) => return Ok(scenario_tool_error(error.to_string())), - }; - - let mints = match self - .fetch_surfnet_accounts(params.surfnet_port, &[base_mint, quote_mint]) - .await - { - Ok(mints) => mints, - Err(error) => return Ok(scenario_tool_error(error)), - }; - let (Some(base_account), Some(quote_account)) = (mints[0].as_ref(), mints[1].as_ref()) - else { - return Ok(scenario_tool_error(format!( - "Tessera market {market_address} references a mint that was not found" - ))); - }; - - let market = match TesseraMarket::validate( - market_address, - market_account, - base_account, - quote_account, - ) { - Ok(market) => market, - Err(error) => return Ok(scenario_tool_error(error.to_string())), - }; - let preparation = match build_tessera_fair_value_scenario(&market, ¶ms.price) { - Ok(preparation) => preparation, - Err(error) => return Ok(scenario_tool_error(error.to_string())), - }; - - self.stage_scenario(preparation.scenario).await + let scenario: Result = async { + let market_address = match params.market.as_deref().map(str::trim) { + None | Some("") => TESSERA_DEFAULT_MARKET, + Some(address) => parse_market(address)?, + }; + let market_account = self + .tessera_market_account(params.surfnet_port, market_address) + .await?; + let (base_mint, quote_mint) = TesseraMarket::mint_addresses(&market_account) + .map_err(|error| error.to_string())?; + let mints = self + .fetch_surfnet_accounts(params.surfnet_port, &[base_mint, quote_mint]) + .await?; + let (Some(base_account), Some(quote_account)) = (mints[0].as_ref(), mints[1].as_ref()) + else { + return Err(format!( + "Tessera market {market_address} references a mint that was not found" + )); + }; + let market = TesseraMarket::validate( + market_address, + &market_account, + base_account, + quote_account, + ) + .map_err(|error| error.to_string())?; + build_tessera_fair_value_scenario(&market, ¶ms.price) + .map(|preparation| preparation.scenario) + .map_err(|error| error.to_string()) + } + .await; + match scenario { + Ok(scenario) => self.stage_scenario(scenario).await, + Err(error) => Ok(scenario_tool_error(error)), + } } #[tool( @@ -1152,36 +1147,24 @@ impl Surfpool { &self, Parameters(params): Parameters, ) -> Result { - let market = match Pubkey::from_str(params.market.trim()) { - Ok(market) => market, - Err(error) => { - return Ok(scenario_tool_error(format!( - "Invalid Tessera market pubkey: {error}" - ))); - } - }; - let accounts = match self - .fetch_surfnet_accounts(params.surfnet_port, &[market]) - .await - { - Ok(accounts) => accounts, - Err(error) => return Ok(scenario_tool_error(error)), - }; - let Some(account) = accounts[0].as_ref() else { - return Ok(scenario_tool_error(format!( - "Tessera market account {market} was not found" - ))); - }; - let scenario = match build_tessera_depth_scenario( - market, - account, - params.sell_remaining_bps, - params.buy_remaining_bps, - ) { - Ok(scenario) => scenario, - Err(error) => return Ok(scenario_tool_error(error.to_string())), - }; - self.stage_scenario(scenario).await + let scenario: Result = async { + let market = parse_market(¶ms.market)?; + let account = self + .tessera_market_account(params.surfnet_port, market) + .await?; + build_tessera_depth_scenario( + market, + &account, + params.sell_remaining_bps, + params.buy_remaining_bps, + ) + .map_err(|error| error.to_string()) + } + .await; + match scenario { + Ok(scenario) => self.stage_scenario(scenario).await, + Err(error) => Ok(scenario_tool_error(error)), + } } #[tool( @@ -1499,40 +1482,37 @@ mod tests { } #[tokio::test] - async fn tessera_fair_value_rejects_a_bad_market_before_any_rpc() { + async fn tessera_tools_reject_a_bad_market_before_any_rpc() { let surfpool = Surfpool::new(); - let result = surfpool - .create_tessera_fair_value_scenario(Parameters(CreateTesseraFairValueScenarioParams { - surfnet_port: None, - market: Some("not-a-pubkey".to_string()), - price: "100.25".to_string(), - })) - .await - .expect("the tool reports input errors in its payload, not as a protocol error"); - let text = format!("{:?}", result.content); - assert!( - text.contains("Invalid Tessera market pubkey"), - "unexpected payload: {text}" - ); - } - - #[tokio::test] - async fn tessera_depth_rejects_a_bad_market_before_any_rpc() { - let result = Surfpool::new() - .create_tessera_depth_scenario(Parameters(CreateTesseraDepthScenarioParams { - market: "not-a-pubkey".to_string(), - sell_remaining_bps: 1000, - buy_remaining_bps: 10000, - surfnet_port: None, - })) - .await - .unwrap(); - assert!( - json_of(&result)["error"] - .as_str() - .unwrap() - .contains("Invalid Tessera market pubkey") - ); + let results = [ + surfpool + .create_tessera_fair_value_scenario(Parameters( + CreateTesseraFairValueScenarioParams { + surfnet_port: None, + market: Some("not-a-pubkey".to_string()), + price: "100.25".to_string(), + }, + )) + .await, + surfpool + .create_tessera_depth_scenario(Parameters(CreateTesseraDepthScenarioParams { + market: "not-a-pubkey".to_string(), + sell_remaining_bps: 1000, + buy_remaining_bps: 10000, + surfnet_port: None, + })) + .await, + ]; + for result in results { + let result = result + .expect("the tool reports input errors in its payload, not as a protocol error"); + assert!( + json_of(&result)["error"] + .as_str() + .unwrap() + .contains("Invalid Tessera market pubkey") + ); + } } #[tokio::test] diff --git a/crates/types/src/scenarios.rs b/crates/types/src/scenarios.rs index fab64d27b..2229988ed 100644 --- a/crates/types/src/scenarios.rs +++ b/crates/types/src/scenarios.rs @@ -1029,6 +1029,10 @@ pub enum RawEncoding { count: usize, stride: usize, }, + U8Strided { + count: usize, + stride: usize, + }, /// A base58 pubkey, written as 32 bytes. Bytes32, /// The slot the override materializes at, plus `lead` (may be negative). @@ -1041,7 +1045,7 @@ impl RawEncoding { /// Byte width of this encoding. pub fn width(&self) -> usize { match self { - RawEncoding::U8 => 1, + RawEncoding::U8 | RawEncoding::U8Strided { .. } => 1, RawEncoding::U16 => 2, RawEncoding::U32 | RawEncoding::I32 | RawEncoding::I32Strided { .. } => 4, RawEncoding::U64 | RawEncoding::I64 | RawEncoding::Slot { .. } => 8, @@ -1056,7 +1060,8 @@ impl RawEncoding { /// encodings with the same loop instead of special-casing one of them. pub fn placements(&self) -> (usize, usize) { match self { - RawEncoding::I32Strided { count, stride } => (*count, *stride), + RawEncoding::I32Strided { count, stride } + | RawEncoding::U8Strided { count, stride } => (*count, *stride), other => (1, other.width()), } } @@ -1090,7 +1095,7 @@ impl RawEncoding { }}; } Ok(match self { - RawEncoding::U8 => int!(u8, "u8"), + RawEncoding::U8 | RawEncoding::U8Strided { .. } => int!(u8, "u8"), RawEncoding::U16 => int!(u16, "u16"), RawEncoding::U32 => int!(u32, "u32"), RawEncoding::U64 => int!(u64, "u64"), @@ -1765,6 +1770,83 @@ mod tests { assert!(err.contains("exceeds"), "unexpected error: {err}"); } + #[test] + fn u8_strided_round_trips_and_rejects_out_of_range_values() { + use super::RawEncoding; + + let encoding = RawEncoding::U8Strided { + count: 3, + stride: 8, + }; + let serialized = json!({"u8_strided": {"count": 3, "stride": 8}}); + assert_eq!(serde_json::to_value(&encoding).unwrap(), serialized); + assert_eq!( + serde_json::from_value::(serialized).unwrap(), + encoding + ); + for value in [json!(-1), json!(256), json!("256")] { + let err = encoding.encode(&value, 0).expect_err("outside u8 range"); + assert!(err.contains("invalid u8"), "unexpected error: {err}"); + } + } + + #[test] + fn u8_strided_writes_single_bytes_and_preserves_padding() { + use super::{Property, RawEncoding, RawLayout}; + + let layout = RawLayout { + account_size: 19, + magic: None, + }; + let mut property = Property::field("flags".to_string()); + property.offset = Some(2); + property.encoding = Some(RawEncoding::U8Strided { + count: 3, + stride: 8, + }); + let properties = [property]; + layout.validate_properties(&properties).unwrap(); + for value in [0, 255] { + let out = layout + .materialize( + &[0xa5; 19], + &properties, + &HashMap::from([("flags".to_string(), json!(value))]), + 0, + ) + .unwrap(); + let mut expected = [0xa5; 19]; + for offset in [2, 10, 18] { + expected[offset] = value; + } + assert_eq!(out, expected); + } + } + + #[test] + fn u8_strided_rejects_invalid_run_bounds_count_and_stride() { + use super::{Property, RawEncoding, RawLayout}; + + let layout = RawLayout { + account_size: 19, + magic: None, + }; + for (offset, count, stride, message) in [ + (3, 3, 8, "beyond"), + (2, 0, 8, "zero placements"), + (2, 3, usize::MAX, "stride overflow"), + (usize::MAX, 1, 1, "offset overflow"), + ] { + let mut property = Property::field("flags".to_string()); + property.offset = Some(offset); + property.encoding = Some(RawEncoding::U8Strided { count, stride }); + let err = layout + .validate_properties(&[property]) + .expect_err("invalid strided run"); + assert!(err.contains(message), "unexpected error: {err}"); + } + } + #[test] fn u16_be_ref_rejects_out_of_range_values() { let seed = PdaSeed::U16BeRef("index".to_string()); From 586c141092e80fbeec327b35f2f5552f4da8f5b2 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Thu, 17 Sep 2026 13:05:16 +0300 Subject: [PATCH 6/8] fix(tessera): apply freshness once instead of persisting it --- .../src/scenarios/protocols/tessera/README.md | 9 +++++---- .../src/scenarios/protocols/tessera/v1/depth.rs | 2 +- .../protocols/tessera/v1/fair_value.rs | 7 ++++--- .../protocols/tessera/v1/overrides.yaml | 2 +- crates/core/src/tests/tessera/mod.rs | 17 ++++++++++++++--- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/core/src/scenarios/protocols/tessera/README.md b/crates/core/src/scenarios/protocols/tessera/README.md index fc7d499cf..e5a3369de 100644 --- a/crates/core/src/scenarios/protocols/tessera/README.md +++ b/crates/core/src/scenarios/protocols/tessera/README.md @@ -97,14 +97,15 @@ stages the scenario through the shared path. Builder-created overrides keep `fetchBeforeUse: false`: creation has already read and hydrated the target account, and the scenario must use that prepared local snapshot. When composing a direct template scenario, set `fetchBeforeUse: true` on the first override for each account not -yet in local state. Freshness overrides use `persist: true` and `last_update_slot: null` to write -the current materialization slot on every application. +yet in local state. Freshness overrides pass `last_update_slot: null` to write the materialization +slot itself. They are applied once; a scenario that runs past the market's freshness window sets +`persist` itself. The `create_tessera_depth_scenario` tool reads current Surfnet state and takes remaining basis points per direction: 1000 retains 10%, 10000 leaves that direction unchanged. It scales only enabled capacities, with integer-floor rounding, and rejects zero capacities or increases. Prices, factors -and disabled levels are preserved. The scenario combines `tessera-depth` with persisted freshness; -depth itself is applied once. Creating another scenario reads the then-current state again. +and disabled levels are preserved. The scenario combines `tessera-depth` with a freshness override, +both applied once. Creating another scenario reads the then-current state again. Curve changes remain available through the raw template. ## Behavioral evidence diff --git a/crates/core/src/scenarios/protocols/tessera/v1/depth.rs b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs index 7e19fd152..27cd90b93 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/depth.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/depth.rs @@ -137,7 +137,7 @@ mod tests { } assert_eq!(actual, expected); assert!(!scenario.overrides[0].persist); - assert!(scenario.overrides[1].persist); + assert!(!scenario.overrides[1].persist); assert!( scenario.overrides[0] .values diff --git a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs index ea655da23..17095da1e 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs @@ -195,15 +195,16 @@ pub fn build_tessera_fair_value_scenario( }) } +/// Applied once, like every other override: nothing on a fork republishes the quote, but nothing +/// overwrites it either. A scenario that spans enough slots to age past the market's window sets +/// persist itself. pub(super) fn freshness_override(target: AccountAddress) -> OverrideInstance { - // Persisted, so the quote stays fresh however long the scenario runs. OverrideInstance::new(FRESHNESS_TEMPLATE.to_string(), PREPARATION_SLOT, target) .with_values(HashMap::from([( "last_update_slot".to_string(), serde_json::Value::Null, )])) .with_label("Keep Tessera quote fresh".to_string()) - .with_persist(true) } fn read_pubkey(data: &[u8], offset: usize) -> SurfpoolResult { @@ -372,7 +373,7 @@ mod tests { assert!(!price.fetch_before_use); assert!(!price.persist); assert!(!freshness.fetch_before_use); - assert!(freshness.persist); + assert!(!freshness.persist); assert_eq!( freshness.values.get("last_update_slot"), Some(&serde_json::Value::Null) diff --git a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml index b8750dc3d..d89811d1d 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/tessera/v1/overrides.yaml @@ -229,7 +229,7 @@ templates: Use list_tessera_markets to select the override account. For percentage reductions, use create_tessera_depth_scenario, which reads the current Surfnet account and scales enabled levels exactly. Pass remaining basis points: 1000 keeps 10%, - 10000 leaves a direction unchanged. It also adds persistent freshness. + 10000 leaves a direction unchanged. It also refreshes the quote once. These are directional capacities, not token vault balances. Do not assume they are monotonic cumulative breakpoints. To make one direction shallower, scale the currently enabled amount fields for that direction by the same ratio. Lower sell_levels for a large diff --git a/crates/core/src/tests/tessera/mod.rs b/crates/core/src/tests/tessera/mod.rs index a7142149a..a4342e4e2 100644 --- a/crates/core/src/tests/tessera/mod.rs +++ b/crates/core/src/tests/tessera/mod.rs @@ -763,16 +763,24 @@ async fn tessera_builders_materialize_and_keep_quotes_fresh() { &[(120, 128), (128, 136), (144, 152)], ); + // The builder queues nothing past its preparation slot: the prepared bytes stay as written. + assert!( + svm.scheduled_overrides + .get(&(BASE_SLOT + 1)) + .expect("read the next slot's queue") + .is_none(), + "the builder must not queue an override past its preparation slot" + ); svm.materialize_overrides_for_slot(&None, BASE_SLOT + 1) .await - .expect("materialize persistent Tessera freshness"); + .expect("materialize the next Tessera slot"); let next_slot = svm .inner .get_account(&market_key) .expect("get Tessera market") .expect("Tessera market present") .data; - assert_eq!(read_u64(&next_slot, 120), BASE_SLOT + 1); + assert_eq!(read_u64(&next_slot, 120), BASE_SLOT); assert_eq!( read_u64(&next_slot, 128), preparation.quote_atoms_per_base_atom_x1e15 @@ -781,7 +789,10 @@ async fn tessera_builders_materialize_and_keep_quotes_fresh() { read_u64(&next_slot, 144), preparation.base_atoms_per_quote_atom_x1e15 ); - assert_only_ranges_changed(&materialized, &next_slot, &[(120, 128)]); + assert_eq!( + next_slot, materialized, + "an unpersisted override leaves the account alone" + ); } #[tokio::test] From 9273e3043177cae971185770fa9659bc70f4e523 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Thu, 17 Sep 2026 16:24:31 +0300 Subject: [PATCH 7/8] fix(tessera): skip a bad market instead of failing the catalog --- .../scenarios/protocols/tessera/v1/markets.rs | 120 ++++++++++++++---- 1 file changed, 96 insertions(+), 24 deletions(-) diff --git a/crates/core/src/scenarios/protocols/tessera/v1/markets.rs b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs index ebc7c2c74..d42a537f0 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/markets.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/markets.rs @@ -7,6 +7,7 @@ use solana_client::{ rpc_filter::{Memcmp, RpcFilterType}, }; use solana_commitment_config::CommitmentConfig; +use solana_pubkey::Pubkey; use super::{TESSERA_DEFAULT_MARKET, TESSERA_PROGRAM_ID, TesseraMarket, fair_value::MARKET_LAYOUT}; use crate::{ @@ -37,16 +38,22 @@ pub async fn discover_tessera_markets( .await? .into_result()?; - let accounts = accounts - .into_iter() - .map(|(address, encoded)| { - let account: Account = encoded.to_account().ok_or_else(|| { - SurfpoolError::internal(format!("Could not decode Tessera market {address}")) - })?; - let mints = TesseraMarket::mint_addresses(&account)?; - Ok((address, account, mints)) - }) - .collect::>>()?; + // One obsolete or malformed market must not hide every valid one, so a market that fails to + // decode or validate is skipped with a warning and the rest of the catalog is still returned. + // This is the same warn-and-continue rule the materializer applies per override. + let candidates = accounts.len(); + let mut retained = Vec::new(); + for (address, encoded) in accounts { + let Some(account) = encoded.to_account() else { + warn!("Skipping Tessera market {address}: its account data could not be decoded"); + continue; + }; + match TesseraMarket::mint_addresses(&account) { + Ok(mints) => retained.push((address, account, mints)), + Err(error) => warn!("Skipping Tessera market {address}: {error}"), + } + } + let accounts = retained; let mut mints = accounts .iter() @@ -56,25 +63,49 @@ pub async fn discover_tessera_markets( mints.dedup(); let mut mint_accounts = HashMap::new(); for batch in mints.chunks(100) { - let fetched = client + // A datasource failure or an unreadable mint disqualifies only the markets that point at + // it, which the validation below reports per market. + let fetched = match client .get_multiple_accounts(batch, CommitmentConfig::confirmed()) - .await?; + .await + { + Ok(fetched) => fetched, + Err(error) => { + warn!("Skipping {} Tessera mints: {error}", batch.len()); + continue; + } + }; for (address, account) in batch.iter().zip(fetched) { - mint_accounts.insert(*address, account.map_account()?); + match account.map_account() { + Ok(account) => { + mint_accounts.insert(*address, account); + } + Err(error) => warn!("Skipping Tessera mint {address}: {error}"), + } } } - let mut markets = accounts - .iter() - .map(|(address, account, (base, quote))| { - let mint = |address| { - mint_accounts.get(address).ok_or_else(|| { - SurfpoolError::internal(format!("Tessera mint {address} was not found")) - }) - }; - TesseraMarket::validate(*address, account, mint(base)?, mint(quote)?) - }) - .collect::>>()?; + let mut markets = Vec::new(); + for (address, account, (base, quote)) in &accounts { + let mint = |address| { + mint_accounts.get(address).ok_or_else(|| { + SurfpoolError::internal(format!("Tessera mint {address} was not found")) + }) + }; + match mint(base) + .and_then(|base| Ok((base, mint(quote)?))) + .and_then(|(base, quote)| TesseraMarket::validate(*address, account, base, quote)) + { + Ok(market) => markets.push(market), + Err(error) => warn!("Skipping Tessera market {address}: {error}"), + } + } + // An empty catalog from a program that does own markets is a failure, not a partial result. + if markets.is_empty() && candidates > 0 { + return Err(SurfpoolError::internal(format!( + "none of the {candidates} discovered Tessera markets validated; the integration needs a refresh" + ))); + } markets.sort_by_cached_key(|market| { ( market.address != TESSERA_DEFAULT_MARKET, @@ -84,3 +115,44 @@ pub async fn discover_tessera_markets( }); Ok(markets) } +/// Validates one discovered market against the mint accounts fetched for the whole catalog. A +/// market whose mints are missing or unreadable fails here alone, so the rest of the catalog +/// still resolves. +fn resolve_market( + address: Pubkey, + account: &Account, + mints: (Pubkey, Pubkey), + mint_accounts: &HashMap, +) -> SurfpoolResult { + let mint = |address: &Pubkey| { + mint_accounts + .get(address) + .ok_or_else(|| SurfpoolError::internal(format!("Tessera mint {address} was not found"))) + }; + let (base, quote) = mints; + TesseraMarket::validate(address, account, mint(&base)?, mint("e)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_market_whose_mints_are_missing_fails_alone() { + let market = Pubkey::new_unique(); + let base = Pubkey::new_unique(); + let quote = Pubkey::new_unique(); + let account = Account { + data: vec![0; MARKET_LAYOUT.account_size], + owner: TESSERA_PROGRAM_ID, + ..Account::default() + }; + + let error = resolve_market(market, &account, (base, quote), &HashMap::new()) + .expect_err("a market with no mint accounts must not resolve"); + assert!( + error.to_string().contains(&base.to_string()), + "the error must name the missing mint: {error}" + ); + } +} From 4263e5811eac8aa05bb5f1864e05f67c8fc12cd4 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Fri, 18 Sep 2026 08:45:08 +0300 Subject: [PATCH 8/8] docs(tessera): say that the freshness override is written once --- crates/core/src/scenarios/protocols/tessera/README.md | 4 ++-- crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/core/src/scenarios/protocols/tessera/README.md b/crates/core/src/scenarios/protocols/tessera/README.md index 3e33c0626..01a2cd57e 100644 --- a/crates/core/src/scenarios/protocols/tessera/README.md +++ b/crates/core/src/scenarios/protocols/tessera/README.md @@ -98,8 +98,8 @@ Builder-created overrides keep `fetchBeforeUse: false`: creation has already rea the target account, and the scenario must use that prepared local snapshot. When composing a direct template scenario, set `fetchBeforeUse: true` on the first override for each account not yet in local state. Freshness overrides pass `last_update_slot: null` to write the materialization -slot itself. They are applied once; a scenario that runs past the market's freshness window -schedules another refresh at a later slot. +slot itself. They are applied once; a scenario extended past the market's freshness window needs +another freshness override of its own. The `create_tessera_depth_scenario` tool reads current Surfnet state and takes remaining basis points per direction: 1000 retains 10%, 10000 leaves that direction unchanged. It scales only enabled diff --git a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs index acef3c1bb..933f42c13 100644 --- a/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs +++ b/crates/core/src/scenarios/protocols/tessera/v1/fair_value.rs @@ -196,8 +196,8 @@ pub fn build_tessera_fair_value_scenario( } /// Applied once, at the scenario's own slot: nothing on a fork republishes the quote, and nothing -/// overwrites it either. A scenario that spans enough slots to age past the market's window -/// refreshes it again at a later slot. +/// overwrites it either. Play pauses the clock there, so a swap in that slot reads a fresh quote. +/// Running past the market's window takes a second freshness override; the builder adds only one. pub(super) fn freshness_override(target: AccountAddress) -> OverrideInstance { OverrideInstance::new(FRESHNESS_TEMPLATE.to_string(), PREPARATION_SLOT, target) .with_values(HashMap::from([(