From d18d14f3674193557a91ce28a3c04bc714446ff6 Mon Sep 17 00:00:00 2001 From: 92Infinitus92 <92georgepetroff92@gmail.com> Date: Mon, 31 Aug 2026 14:23:37 +0300 Subject: [PATCH] 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 | 13 +- 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, 3013 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 2809381f..a65eed11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12015,6 +12015,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", @@ -12076,6 +12077,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 4e38b1ab..7b103657 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -112,6 +112,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 99f0b096..b6af45e2 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 00000000..33ba18c6 --- /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 00000000..a3a6d96c --- /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 00000000..f3d78287 --- /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 00000000..37db50bb --- /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 00000000..2f62f745 --- /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 4f95a059..65f38ac3 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -24,6 +24,8 @@ pub const KAMINO_V1_IDL_CONTENT: &str = include_str!("./protocols/kamino/v1/idl. pub const KAMINO_V1_OVERRIDES_CONTENT: &str = include_str!("./protocols/kamino/v1/overrides.yaml"); pub const BISONFI_OVERRIDES_CONTENT: &str = include_str!("./protocols/bisonfi/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 = @@ -79,6 +81,7 @@ impl TemplateRegistry { default.load_meteora_overrides(); default.load_kamino_overrides(); default.load_bisonfi_overrides(); + default.load_tessera_overrides(); default.load_drift_overrides(); default.load_whirlpool_overrides(); default.load_spl_token_overrides(); @@ -123,6 +126,10 @@ impl TemplateRegistry { self.load_protocol_overrides_without_idl(BISONFI_OVERRIDES_CONTENT, "bisonfi"); } + pub fn load_tessera_overrides(&mut self) { + self.load_protocol_overrides_without_idl(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"); @@ -505,11 +512,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) + BisonFi (4) = 66 + // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + BisonFi (4) + Tessera (6) = 72 assert_eq!( registry.count(), - 66, - "Registry should load 66 templates total" + 72, + "Registry should load 72 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 00000000..853cb2b7 --- /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 eeb64407..43d062e3 100644 --- a/crates/core/src/tests/mod.rs +++ b/crates/core/src/tests/mod.rs @@ -4,7 +4,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 00000000..f1c4d081 --- /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 0ef4ebe2..37651d60 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 7ce4c3e0..83115816 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();