Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 17 additions & 5 deletions crates/cli/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use rmcp_actix_web::transport::StreamableHttpService;
#[cfg(feature = "explorer")]
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use surfpool_core::scenarios::TemplateRegistry;
use surfpool_core::scenarios::{TemplateRegistry, resolve_live_constants};
use surfpool_mcp::Surfpool;
use surfpool_studio_ui::serve_studio_static_files;
use surfpool_types::{
Expand Down Expand Up @@ -147,12 +147,24 @@ async fn get_config(
#[actix_web::get("/v1/scenarios/templates")]
async fn get_scenario_templates(
template_registry: Data<RwLock<TemplateRegistry>>,
config: Data<RwLock<SanitizedConfig>>,
) -> Result<HttpResponse, Error> {
let registry = template_registry.read().map_err(|_| {
actix_web::error::ErrorInternalServerError("Failed to read template registry")
})?;
let templates: Vec<OverrideTemplate> = template_registry
.read()
.map_err(|_| {
actix_web::error::ErrorInternalServerError("Failed to read template registry")
})?
.all()
.into_iter()
.cloned()
.collect();
let rpc_url = config
.read()
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to read context"))?
.rpc_url
.clone();

let templates: Vec<&OverrideTemplate> = registry.all();
let templates = resolve_live_constants(&rpc_url, templates).await;
let response = serde_json::to_string(&templates)
.map_err(|_| actix_web::error::ErrorInternalServerError("Failed to serialize templates"))?;

Expand Down
1 change: 1 addition & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ axum = { version = "0.8", default-features = false, features = ["tokio", "http1"

[dev-dependencies]
ed25519-dalek = "1.0.1"
solana-program-runtime = "4.2.1"
libsecp256k1 = "0.7.2"
p256 = { version = "0.13", default-features = false, features = ["ecdsa"] }
test-case = { workspace = true }
Expand Down
37 changes: 37 additions & 0 deletions crates/core/src/scenarios/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Protocols that are natively supported by Surfpool will have their IDLs included
- **Drift v2** - Perp and spot markets, user state, and global state
- **Pump v1** - Bonding curve launchpad with curve reserve and global config override templates
- **PumpSwap v1** - Constant-product AMM with pool state and global config override templates, including canonical pool derivation for migrated pump.fun coins
- **Tessera V** - Proprietary market maker (no published IDL), with price, freshness, ladder depth, ladder curve and halt templates over the markets its maker currently quotes, read from the program. See [protocols/tessera/README.md](./protocols/tessera/README.md)

For custom protocols, an IDL can be registered at runtime using the [`surfnet_registerIdl`](https://docs.surfpool.run/rpc/cheatcodes#surfnet-registeridl) RPC cheatcode.

Expand Down Expand Up @@ -78,6 +79,42 @@ Each collection uses exactly one write model: an IDL-backed collection cannot se
compiled into Surfpool's built-in template registry; there is no runtime raw-layout registration
endpoint.

### Constants read from program accounts

A constant can list the program's own accounts instead of a hardcoded catalog. The options are read
through the surfnet RPC (local state first, then its datasource) when templates are served to Studio
and MCP, cached for 60 seconds, and never at startup:

```yaml
constants:
market:
label: Example market
source:
program_accounts:
program: <program id>
size: 1264
filters: [{ offset: 96, bytes: [5, 0, 0, 0, 0, 0, 0, 0] }]
fields:
base_mint: { offset: 24, encoding: pubkey }
quote_mint: { offset: 56, encoding: pubkey, mask: [1, 2, 3, 4] }
base_vault: { offset: 88, encoding: pubkey }
limit: { offset: 120, encoding: u64 }
pair: { base: base_mint, quote: quote_mint }
value: base_vault
default: { base: <base mint>, quote: <quote mint> }
fresh: { field: limit, within_slots: 5000 }
```

Each matching account becomes one option labelled with its pair's token symbols, sorted by label.
Its value is the account address, or the pubkey field named by `value`. Its metadata holds every
field, `account`, `pair`, `base_decimals` and `quote_decimals`. `mask` holds one XOR key per 8-byte
word of a field. `expand: [{ value: <field>, suffix: " base vault", metadata: { side: base } }]`
turns each account into one option per entry instead. `fresh` drops accounts whose u64 slot field
is more than `within_slots` behind the surfnet's current slot, and `default` moves the option with
those two mints to the front. A template whose address is
`{ type: pubkey }` with no value targets the first option. If the read fails the constant is
served with no options.

### Override Templates
Directly using the `surfnet_registerScenario` endpoint requires building out a map of account keys that are specific to the schema of the account that is being written to.
This is a cumbersome process in most cases.
Expand Down
Loading
Loading