Skip to content

feat(scenarios): add Jupiter Perps support - state templates - #49

Open
92Infinitus92 wants to merge 14 commits into
feat/scenarios/override/jupiter-perps-supportfrom
feat/jupiter-perps-support
Open

92Infinitus92 wants to merge 14 commits into
feat/scenarios/override/jupiter-perps-supportfrom
feat/jupiter-perps-support

Conversation

@92Infinitus92

@92Infinitus92 92Infinitus92 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Jupiter Perps (PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu) and its Doves price feeds as state templates, so a scenario can move a market's price and change the custody and pool settings that decide liquidations, fees and deposits.

  • jupiter-perps-doves-price moves the price Perps reads; jupiter-perps-custody-risk, -custody-borrow-rate and -custody-fees change one market; jupiter-perps-pool-limits and -pool-fees change the JLP pool.
  • The Doves IDL is the one published on chain, bundled verbatim. The Perps IDL is the on-chain legacy IDL after anchor idl convert, as the README's provenance section records.
  • Markets are found live from the JLP pool's custody list; nothing is hardcoded.

Prices on a fork: JLP deposits, swaps and quotes refuse a feed older than 5 seconds, and a keeper fill needs a price published after its request. jupiter-perps-doves-price imports a keep-alive template over every Doves AgPriceFeed (the owned kind from #53, which this PR is stacked on; merge #53 first). Once an override with keepAlive: true plays, every Doves feed the surfnet has loaded gets timestamp from the Clock every slot, so those checks find fresh prices. To keep one feed stale while the others stay fresh, the llm_context gives the recipe: one override with keepAlive: false and one with keepAlive: true. Liquidations do not check a price's age.

Copies on a fork: Perps refuses a transfer through a custody whose vault holds less than the custody records (6014). A transaction loads its missing accounts in one request, so the pair agrees, but a custody read earlier on its own (Studio's market list, a lookup, or fetchBeforeUse on a custody template) can drift from its vault. The README's troubleshooting row gives the fix: reset both, and the next transaction loads them together.

Tests: tests/jupiter_perps runs the deployed Perps and Doves programs (pinned to their deploy slots) against live accounts: IDL round trips, the lookup recipe, a price shock and each lever liquidating a position, opening fees, deposit limits and switches, pool fees, and keep-alive: price_overrides_keep_their_feeds_published_at_the_clock shows that one price override keeps every pool feed published through produced slots and a time travel, and that a feed with keepAlive: false ages until a deposit refuses it (6003). fmt, clippy and the core suite pass.

Shared with the other open protocol PRs, byte for byte: the MCP params description.

Studio: LimeChain/surfpool-web-ui#36

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds Jupiter Perps (Custody and Pool) and Doves (AgPriceFeed) state templates. It bundles the IDLs published on chain, adds a README and registers both collections. It also adds a large integration suite that runs the deployed programs against live mainnet accounts. The registry wiring and count updates are correct, and the docs are detailed. The tests cover each lever end to end, not just the encoding.

Issues

  • timestamp is required but not exposed: the README and llm_context tell users to write timestamp on Doves feeds for deposits, swaps, quotes and keeper fills. The jupiter-perps-doves-price template only declares price. It works through the raw values map, and the tests depend on that. Any UI that builds forms from properties (Studio) can't set it, though, so those flows only work through raw RPC or the MCP.
  • Unrelated MCP change: crates/mcp/src/surfpool/mod.rs rewrites the params description of CallSurfnetRpcParams. It is generic and has nothing to do with Jupiter Perps, so it should go in its own PR or be called out in the description.
  • Misleading account_type: jupiter-perps/v1/overrides.yaml declares account_type: Custody, but two of its five templates target Pool. If anything uses account_type to filter or label, the pool templates will show up under the wrong type.
  • Large hand-maintained test file: tests/jupiter_perps/mod.rs (1.3k lines) relies on hardcoded byte offsets (the custody/feed/position modules) and live mainnet state. It also defaults to the public mainnet RPC, so it will be flaky and rate-limited. Pinning the deploy slots helps, but decoding through the bundled IDL instead of fixed offsets would be less brittle when the program is upgraded.

Overall the change looks good once the doves template exposes timestamp and the MCP change is split out or explained.

description: Override the Doves price Jupiter Perps reads for one asset
idl_account_name: AgPriceFeed
properties:
- path: price

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs here (line 41) and in the README (timestamp: <Surfnet clock ...>) tell users to override timestamp on this template, but only price is declared as a property. It works through raw values (the tests depend on it), but form-driven clients like Studio won't offer it. Consider adding:

      - path: timestamp
        label: Publish time
        description: "Unix seconds the price was published. Write it ahead of the Surfnet clock so deposits, swaps, quotes and keeper fills accept the copied feed"

@@ -0,0 +1,239 @@
protocol: jupiter-perps
version: v1
account_type: Custody

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

account_type: Custody is declared for the whole file, but jupiter-perps-pool-limits and jupiter-perps-pool-fees target Pool (lines 164, 199). If anything groups or labels by account_type, the pool templates will be misattributed. Consider splitting the Pool templates into their own file or checking that account_type is only informational.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rewrite of the generic call_surfnet_rpc param description is unrelated to Jupiter Perps, and the PR description doesn't mention it. Please split it out or call it out. Also check that the tagged {"String":...} encoding it documents is actually the format JsonValue deserializes. If JsonValue takes plain JSON, this instruction will make every LLM-issued RPC call fail.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The PR adds six Jupiter Perps scenario templates: one Doves price feed template, three custody templates and two pool templates. Both on-chain IDLs are bundled unchanged, the registry counts and tests are updated, and there's a thorough tests/jupiter_perps integration suite that runs the pinned deployed programs against live accounts. The override paths match the bundled IDL, including pricing.max_leverage, limit.max_aum_usd (u128), max_lp_token_price_change_bps and the Doves price/timestamp. The README is detailed and useful.

Overall the PR looks solid. I found a few issues:

  • Contradictory fetchBeforeUse guidance. The llm_context of every custody and pool template tells the LLM to "Set fetchBeforeUse: true". The README's troubleshooting table names fetchBeforeUse reloading only the custody as a cause of the 6014 InvalidCustodyBalance error. Its last row also says later overrides of the same account should use fetchBeforeUse: false, because a reload drops earlier local changes. An agent following the template text will hit both problems, so the warning needs to be in the templates' llm_context too.

  • Unrelated changes in the PR.

    • .github/workflows/claude-review.yml adds a CI review bot.
    • The CallSurfnetRpcParams.params schema description in crates/mcp is rewritten.

    Neither is related to Jupiter Perps support. They should be split out, or at least mentioned in the PR body. The workflow in particular looks like fork-specific tooling.

  • Feed age limits disagree. The Doves timestamp property says quotes refuse a feed 5 seconds old. The llm_context and README say 15 seconds for quotes.

  • Minor, in the tests: liquidation_candidate runs a baseline liquidate for each candidate on one shared VM. If a candidate's baseline liquidation succeeds, that VM state is changed for the candidates checked after it. The returned graph is unaffected because later tests build a fresh VM, so this only matters for how candidates are picked.

FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool
5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its
custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint
is the token's mint. Set fetchBeforeUse: true.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tells the LLM to always set fetchBeforeUse: true on custody overrides. The README troubleshooting row for 6014 names a fetchBeforeUse that reloads only the custody as a cause of vault/custody drift. The last row also says a later override with fetchBeforeUse: true drops earlier local changes. Please add both caveats here, and at lines 110 and 144 and in the pool templates. Something like: "use fetchBeforeUse: true only on the first override of an account. If a later transfer fails with 6014, surfnet_resetAccount both the custody and its token_account." Otherwise an agent that follows this text will end up in the failure modes the README describes.

description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8"
- path: timestamp
label: Publish time
description: "Unix seconds the price was published. Deposits, swaps and quotes refuse a feed 5 seconds older than the clock, and a keeper fill needs a time after its request; Perps accepts a time ahead of the clock"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says quotes refuse a feed 5 seconds older than the clock. The llm_context (line 43) and the README say quotes allow 3× max_price_age_sec, up to 15 seconds. Please make the description match, e.g. "Deposits and swaps refuse a feed 5 seconds older than the clock (quotes 15)".

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rewrite of the MCP params description isn't related to Jupiter Perps. Please move it to its own PR, or at least mention it in the PR description, so the change to the MCP-facing schema gets its own review.

@@ -0,0 +1,141 @@
name: Perform a code review when a pull request is created.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a CI code-review workflow, which has nothing to do with the Jupiter Perps templates and looks like fork-specific tooling. It also needs an ANTHROPIC_API_KEY secret upstream. Please drop it from this PR or split it into a separate one.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds six Jupiter Perps and Doves state templates, bundles both IDLs and adds a thorough integration suite (tests/jupiter_perps) that runs the deployed programs against live accounts. The registry wiring is small and matches the existing protocols. The template counts and tag counts are updated (81→87, oracle 4→5), and the property paths are validated through the IDL by the existing registry tests. The docs are detailed and line up with the tests. Overall this looks good. The remaining points are about scope and maintenance.

Issues

  • Changes outside the PR's scope. The PR adds .github/workflows/claude-review.yml (commit ci: claude code review) and rewrites the MCP CallSurfnetRpcParams.params description. Neither is part of Jupiter Perps support, so both should go in separate PRs that can be reviewed and reverted on their own. On the workflow itself: the PR title and body are put into the prompt unescaped. The Claude job is read-only and fork PRs are skipped, so the risk is limited, but it is still a prompt-injection surface worth a deliberate decision.
  • The PR body says "Both IDLs are … bundled verbatim". The README (IDL provenance) says the Perps IDL is the on-chain legacy IDL after anchor idl convert. The README is right; the PR body should say the same.
  • The tests break whenever Jupiter redeploys. PERPS_DEPLOYED_SLOT / DOVES_DEPLOYED_SLOT are asserted against live ProgramData, and KEEPER is hardcoded. Any upgrade or keeper rotation turns the whole suite red even when nothing in surfpool changed. That is by design, but there are two cheaper options:
    • turn the slot check into a clearly labelled skip or warning;
    • document that these tests are expected to need re-pinning, so CI owners don't treat the failure as a regression.
  • Repeated llm_context text. The "FIND THE CUSTODY … fetchBeforeUse … 6014" paragraph appears word for word in all three custody templates, and the pool paragraph in both pool templates. The lines that mix wrapped and unwrapped text (overrides.yaml L61, L112, L149, L187, L230) show it was copy-pasted. That's fine if the loader has no way to share text, but expect them to drift apart over time.

@@ -0,0 +1,249 @@
protocol: jupiter-perps
version: v1
account_type: Custody

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The collection-level account_type: Custody doesn't apply to the two pool templates, which set idl_account_name: Pool. That works, but a short comment here (or Multiple, as Drift uses) would tell readers the collection covers both account types.

FIND THE CUSTODY (read it live, nothing is hardcoded): getAccountInfo on the JLP Pool
5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq with encoding jsonParsed lists its
custodies; read them with getMultipleAccounts and jsonParsed, and take the one whose mint
is the token's mint. Set fetchBeforeUse: true on the first override of an account and false on later ones in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is copied word for word into all three custody templates (L61, L112, L149), and the line wrapping is uneven. Please rewrap it at least. If the loader can share text between templates (for example with YAML anchors), use that so the three copies can't drift apart.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unrelated to Jupiter Perps and changes how every MCP client is told to build RPC params, so it should be its own PR. Also, if {"String":...} wrapping is really required, the input format looks like a serde enum leaking into the tool. Accepting plain serde_json::Value would be friendlier than documenting the wrapping in a long description.

}
let data = fetch(&[programdata]).await.remove(0).data;
let slot = u64_at(&data, 4);
assert_eq!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any Perps or Doves upgrade makes this assert fail and takes the whole suite down, even when surfpool hasn't changed. Consider making the suite skip with a clear message instead, or at least note in the module docs that the slots need re-pinning after upgrades, so CI failures aren't mistaken for regressions.

@@ -0,0 +1,141 @@
name: Perform a code review when a pull request is created.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CI workflow isn't part of the Jupiter Perps feature; please split it into its own PR. Note also that pull_request.title/body are inserted into the prompt as-is. The job is read-only, so the risk is limited, but PR authors can steer the review output.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds six state templates for Jupiter Perps: five on the Perps program (custody risk, borrow rate and fees; pool limits and fees) and one Doves price-feed template. It bundles the on-chain IDLs, adds registry wiring and count assertions, a protocol README, and a large set of live-mainnet integration tests behind integration-tests.

Assessment

The templates are well scoped. Markets are found live from the pool's custody list rather than hardcoded. The llm_context and README explain fork-specific problems that would otherwise be hard to figure out: stale Doves prices, fetchBeforeUse reload semantics, and custody/vault drift (6014). The registry changes are minimal and match the existing protocols, and the count and tag assertions are updated.

Issues

  • Unrelated changes are bundled in. .github/workflows/claude-review.yml (CI infra that needs ANTHROPIC_API_KEY and pull-requests: write) and the MCP CallSurfnetRpcParams.params description rewrite have nothing to do with Jupiter Perps. Please split them into their own PRs so each can be reviewed and reverted on its own.
  • The integration tests are brittle by design. They hard-fail whenever Jupiter redeploys Perps or Doves, because of the pinned ProgramData slot. They also depend on getProgramAccounts against public mainnet RPC. If they run in any scheduled or CI job, expect sporadic failures that have nothing to do with surfpool. Consider gating them more narrowly or documenting that they are manual-only.
  • Small fact drift risk. The docs hardcode live parameter values, such as the 5 s max_price_age_sec, the 500 BPS JLP guard and the "single hardcoded keeper". These are correct today but will go stale silently. Pointing readers to the field to read (as is already done in some places) is more durable than quoting the number.

@@ -0,0 +1,141 @@
name: Perform a code review when a pull request is created.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CI workflow is unrelated to Jupiter Perps support. Please move it into its own PR: it adds a secret dependency and a job with pull-requests: write, and that deserves a separate review and an easy revert.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MCP schema change has nothing to do with the Perps templates. It looks useful, but it belongs in a separate PR. Also, JsonValue::Number is i64, so a u64 above i64::MAX or a float can't be expressed. If agents are now being told to wrap every value, it may be worth saying that large integers should be sent as String.

}
let data = fetch(&[programdata]).await.remove(0).data;
let slot = u64_at(&data, 4);
assert_eq!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any Jupiter redeploy turns this assert into a hard test failure, which has nothing to do with surfpool changes. If these tests ever run in CI or on a schedule, consider skipping with a clear message instead of panicking, or document clearly that the suite is manual-only.

.client
.get_program_ui_accounts_with_config(&PERPS, config)
.await
.unwrap_or_else(|e| panic!("{} scan: {e}", market.label));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getProgramAccounts against Perps on the default api.mainnet-beta.solana.com is often rate-limited or rejected for large programs, and this can run up to 16 scans per market. Consider requiring SURFPOOL_TEST_RPC_URL for this path, or at least mention in the module doc that a private RPC is effectively required.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

Adds Jupiter Perps and its Doves oracle as scenario template collections: 5 Perps templates covering custody risk, borrow rate and fees plus pool limits and pool fees, and 1 Doves price template. The Doves template has a keep-alive import that republishes timestamp each slot. The PR also adds the bundled IDLs, a README and an integration suite that runs the deployed programs against live mainnet accounts.

The templates are well documented. The registry wiring is consistent with the other protocols, and the tests exercise each lever end to end. Overall this looks good. Most of the points below are about scope and keeping the description accurate.

Issues

  • The PR description is out of date. The body says keep-alive is a follow-up and that the README tells scenarios to write timestamp ahead of the clock. Commit 7a2bad6 already adds keep-alive (doves/v1/keep-alive.yaml), and the README now says Surfpool republishes the feed. Please update the body so reviewers and the changelog match what ships.

  • Unrelated changes are mixed in:

    • .github/workflows/claude-review.yml is a CI workflow for this fork and has nothing to do with Perps.
    • The CallSurfnetRpcParams.params schema description in crates/mcp is a general MCP change.

    Consider splitting both out, especially if this PR is meant to go upstream to solana-foundation/surfpool.

  • The tests depend on mainnet and on a pinned deploy:

    • deployed_program asserts the exact ProgramData deploy slot, so any Jupiter redeploy breaks the whole suite. 3bcc64d already had to follow one.
    • open_positions runs up to 16 getProgramAccounts scans on Perps. By default it uses the public api.mainnet-beta endpoint, which often rate-limits or rejects these calls.

    The suite is behind integration-tests, so this is acceptable. Still, consider making a missing SURFPOOL_TEST_RPC_URL skip the tests, or documenting that a private RPC is required, so CI doesn't flake.

  • Minor: open_positions says "newest updates first", but inside each 2^16-second window the results are sorted by pubkey, not by update_time.

label: Price
description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8"
- path: timestamp
label: Publish time

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp is exposed as a user property, but the keep-alive import (timestamp: $unix_s) overwrites it on every slot from the one this override plays. Any value a user sets is effectively ignored, which contradicts the "write timestamp ahead of the clock" advice in the PR body. Consider removing the property, or saying explicitly that only the first slot uses it.

JUPITER_PERPS_DOVES_OVERRIDES_CONTENT,
"jupiter-perps-doves",
);
let idl = serde_json::from_str(JUPITER_PERPS_DOVES_IDL_CONTENT).expect("doves idl");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Doves IDL is parsed a second time here, after load_protocol_overrides has already parsed it. The expect("doves idl") message is also inconsistent with the unable to load {protocol} idl: {e} panic used elsewhere. You could parse it once and pass it to both load_collection and load_keep_alive.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MCP schema change is unrelated to Jupiter Perps. Please move it to its own PR so it can be reviewed and reverted on its own.

};

const RPC_URL_ENV: &str = "SURFPOOL_TEST_RPC_URL";
const DEFAULT_RPC_URL: &str = "https://api.mainnet-beta.solana.com";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is the public mainnet RPC, but the suite runs many getProgramAccounts scans on Perps (see open_positions), which the public endpoint commonly throttles or rejects. Consider requiring SURFPOOL_TEST_RPC_URL, or skipping with a clear message when it is unset.

side: position::SHORT,
};

/// Open positions of the market, newest updates first. A position account outlives its trade with

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the doc says "newest updates first", but each window's results are sorted by pubkey (open.sort_unstable()), so ordering within a window isn't by recency.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

Summary

This PR adds Jupiter Perps and Doves as bundled protocols:

  • Five Perps templates: custody risk, borrow rate, custody fees, pool limits and pool fees.
  • One Doves price template.
  • A Doves keep-alive.yaml that republishes the timestamp of every Doves-owned AgPriceFeed the surfnet has loaded, every slot, at the Surfnet clock's $unix_s, once a jupiter-perps-doves-price override plays.
  • A README and the registry wiring.
  • A large integration-tests suite that runs the deployed programs against live accounts.

The templates and llm_context are careful and consistent with each other. The registry counts and tests are updated. I checked that $unix_s is a supported keep-alive placeholder (crates/types/src/scenarios.rs). Overall this looks good. My notes are mostly about documentation and scope.

Notes

  • The PR description is out of date. It says keep-alive (feat(core/scenarios): introduce keep alive feature solana-foundation/surfpool#906) is a later follow-up and that the README shows how to write timestamp ahead of the clock. The branch now merges feat(core/scenarios): introduce keep alive feature solana-foundation/surfpool#906 and ships Doves keep-alive itself. Please update the body so reviewers and the Studio PR are working from the right behaviour.

  • Unrelated change. The crates/mcp schema description for CallSurfnetRpcParams.params has nothing to do with Perps. It is accurate (JsonValue is externally tagged and Number is i64), but it would be cleaner as its own PR, or at least mentioned in the description.

  • Integration tests are brittle by design:

    • deployed_program fails whenever Jupiter redeploys Perps or Doves. This already happened once on this branch (3bcc64d).
    • open_positions runs filtered getProgramAccounts scans against Perps on api.mainnet-beta.solana.com by default, and public RPC often rate-limits or refuses these.

    This is fine behind integration-tests. Consider saying in the module docs that SURFPOOL_TEST_RPC_URL should point at a non-public RPC.

  • Keep-alive scope. The owned import republishes every Doves AgPriceFeed the surfnet has loaded, not only the overridden one. The llm_context and the README's Keep-alive section say this correctly. The README intro does not (see the inline comment).


crates/core/src/scenarios/protocols/jupiter-perps/v1/README.md:11

This says keep-alive applies to the overridden feed only ("keeps a feed published ... once a scenario overrides it"). The Keep-alive section below, and the owned import in doves/v1/overrides.yaml, republish every loaded Doves feed, including ones loaded later. Suggest: "Once a scenario overrides a Doves price, Surfpool keeps every loaded Doves feed published."


crates/core/src/scenarios/registry.rs:364

Minor: this IDL gets parsed twice, here and again inside load_protocol_overrides just above. It also panics with a terse message. The content is bundled and the registry tests cover it, so this is low risk. A clearer expect("bundled Doves IDL must parse") would help, or reuse the parse result if load_protocol_overrides can return it.


crates/core/src/scenarios/protocols/jupiter-perps/doves/v1/overrides.yaml:86

fetchBeforeUse: true reloads the feed from mainnet. With keep-alive running, that is harmless for timestamp. But a later override of the same feed in another slot that forgets false silently reverts the scenario's earlier price, and keep-alive then keeps republishing the mainnet price. It might be worth saying that explicitly here, since price is the field users will most often re-override.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds Jupiter Perps support: 5 Perps templates (three custody, two pool) and a Doves AgPriceFeed price template. The Doves template brings in a keep-alive template that republishes every loaded Doves feed at the clock time. The PR also adds bundled IDLs, a README, registry wiring with updated counts, and a live-mainnet integration suite (tests/jupiter_perps). It also widens the description of the MCP call_surfnet_rpc params.

The template YAML and llm_context are thorough and agree with the README. Both IDL sha256 hashes in the README match the bundled files (8d9860…, 4945d1…). Registry wiring and test counts are consistent: 87 templates in total, 5 oracle templates.

Issues

  • The PR description is stale. It says "nothing publishes Doves prices on a surfnet… a follow-up will keep the feeds published" and tells scenarios to write timestamp ahead of the clock. The branch now merges keep-alive (solana-foundation#906) and makes the Doves template publish every loaded feed. The README and llm_context describe the new behaviour. Please update the description so reviewers and Studio (surfpool-web-ui#36) don't follow the old workaround.
  • The diff is noisy. The range includes an upstream merge and unrelated commits (solana-foundation#907, solana-foundation#926, solana-foundation#932, solana-foundation#933, CI review workflow). Rebasing would keep the PR focused.
  • The MCP change is out of scope. The CallSurfnetRpcParams description change in crates/mcp has nothing to do with Jupiter Perps. It is accurate (it matches the externally tagged JsonValue enum), but it belongs in its own PR or should be called out in the description.
  • The integration tests are fragile by design. They depend on live mainnet state: deploy slots pinned to the ProgramData header, a hardcoded keeper key, and getProgramAccounts scans of open positions on the public RPC by default. They are gated behind integration-tests, but any Perps redeploy fails them. The history already shows one "follow the Perps redeploy" commit. If they run in CI, consider an opt-in marker or #[ignore] so CI doesn't break on events outside this repo, and require SURFPOOL_TEST_RPC_URL rather than defaulting to the public RPC.

JUPITER_PERPS_DOVES_OVERRIDES_CONTENT,
"jupiter-perps-doves",
);
let idl = serde_json::from_str(JUPITER_PERPS_DOVES_IDL_CONTENT).expect("doves idl");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the Doves IDL is parsed twice, once inside load_protocol_overrides and again here. This call also uses .expect("doves idl"), unlike the panic!("unable to load {} idl: {}") style used elsewhere. Consider a small helper that parses once and passes the result to both load_collection and load_keep_alive, or at least use the same error message.

- path: price
label: Price
description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8"
- path: timestamp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timestamp is exposed as a user property, but the imported keep-alive overwrites it with $unix_s every slot from the slot this override plays. A value set here only sticks when the same override sets keepAlive: false. Say that in the property description, e.g. "ignored unless keepAlive: false", so Studio users and LLMs don't set a future timestamp expecting it to hold (the workaround the PR description still suggests).

const PERPS: Pubkey = Pubkey::from_str_const("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu");
const PERPS_PROGRAMDATA: Pubkey =
Pubkey::from_str_const("C258LjtN8Q928yz2nJe5kwQycckGfWBSrhZBP2mTpLQt");
const PERPS_DEPLOYED_SLOT: u64 = 465131236;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pinned slot, plus the hardcoded KEEPER, makes the suite fail whenever Jupiter redeploys Perps or rotates the keeper. That is outside this repo's control, and it has already happened once on this branch. If integration-tests runs in CI, consider gating these tests behind an explicit env var such as SURFPOOL_TEST_RPC_URL (and not defaulting to the public mainnet RPC) so upstream redeploys don't break the build.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is unrelated to Jupiter Perps. It is accurate for the externally tagged JsonValue, but consider splitting it out or at least mentioning it in the PR description.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds Jupiter Perps and its Doves oracle as two template collections: five Perps templates (custody risk, borrow rate and fees; pool limits and fees) and one Doves price template. The Doves price template imports an owned keep-alive template, so once a price override plays, every loaded Doves feed is republished each slot. It also bundles both IDLs, registers them in TemplateRegistry, adds registry unit tests, and adds a tests/jupiter_perps integration suite that runs the deployed programs against live accounts.

The YAML is well documented and the registry wiring follows the existing pattern. The llm_context and README guidance on lookup, error codes and fetch/reload is detailed. Overall this looks good. Mostly nits and housekeeping:

  • The PR description is out of date. It says nothing publishes Doves prices on a surfnet and that keep-alive will come in a follow-up. The branch now merges solana-foundation#906 and ships keep-alive.yaml plus the keep_alive: import (commits 7a2bad6, 26cc905). Please update the body, including the "stale-price limits" test description: the test is now price_overrides_keep_their_feeds_published_at_the_clock.
  • Unrelated change. The MCP CallSurfnetRpcParams.params schema description (crates/mcp/src/surfpool/mod.rs) has nothing to do with Jupiter Perps and would fit better in its own PR.
  • The integration tests depend on mainnet. They pin the programs' deploy slots, and the commit history already shows one forced update after a Perps redeploy. They also depend on hardcoded keeper bytecode, live open positions, and getProgramAccounts against the public mainnet RPC. They are gated behind integration-tests, but expect them to break regularly. Consider stating in the module docs that a redeploy failure is expected maintenance, not a regression.

- path: price
label: Price
description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8"
- path: timestamp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep-alive rewrites timestamp to the Clock time every slot. Setting this property has no lasting effect unless the override also sets keepAlive: false, which is easy to miss when someone tries to make a feed stale on purpose. Either mention keepAlive: false here ("set together with keepAlive: false to pin a stale time"), or drop the property from the user-facing template.

JUPITER_PERPS_DOVES_OVERRIDES_CONTENT,
"jupiter-perps-doves",
);
let idl = serde_json::from_str(JUPITER_PERPS_DOVES_IDL_CONTENT).expect("doves idl");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the Doves IDL is parsed twice, once inside load_protocol_overrides and again here. The .expect("doves idl") message also differs from the unable to load {protocol} idl panic used elsewhere. You could parse it once and pass it to both, or add a small helper that reuses the existing error message.

side: position::SHORT,
};

/// Open positions of the market, newest updates first. A position account outlives its trade with

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc says "newest updates first", but within each 2^16-second window the results are sorted by pubkey (open.sort_unstable()), so positions are only newest-first at window granularity. Either reword the doc, or sort by update_time if order matters to the candidate selection.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds Jupiter Perps and Doves state templates: 5 Perps templates (custody risk, borrow rate and fees; pool limits and fees) and 1 Doves price template. The Doves price template also brings in a keep-alive that keeps every Doves AgPriceFeed republished. It also adds a README, the bundled IDLs, registry wiring with updated counts, and a set of integration tests against mainnet.

I checked the bundled IDL sha256 values against the README's provenance table, and they match. The registry wiring follows the existing pattern. The keep-alive import resolves: the YAML's owner: deserializes into the owners: vec![..] the test asserts. The llm_context text is detailed and lines up with the README.

Issues and suggestions

  • The PR description is out of date. It still says nothing publishes Doves prices and that keep-alive (solana-foundation#906) will come in a follow-up. That keep-alive work is now merged into this branch, and jupiter-perps-doves-price turns it on. Update the "Prices on a fork" paragraph so reviewers and release notes aren't misled.
  • Unrelated change. The MCP CallSurfnetRpcParams.params description change has nothing to do with Jupiter Perps. Consider splitting it into its own PR, or at least mentioning it in the description.
  • Brittle tests. The integration tests pin the Perps and Doves deploy slots and assert on them. Any mainnet redeploy fails them, and commit 3bcc64d shows one has already happened. They also run getProgramAccounts scans against the public mainnet RPC by default, which is often rate-limited or refused for large programs. This is acceptable since CI doesn't enable integration-tests, but the module doc should say that a dedicated SURFPOOL_TEST_RPC_URL is effectively required.
  • Version mismatch on Doves. The Doves collection is versioned v1, but it lives under jupiter-perps/doves/v1 with protocol jupiter-perps-doves. That's fine, but if the Doves program gets its own IDL revision, its version will be tied to the Perps folder layout.

withdrawal, swap or quote, or a position, finds every custody's feed fresh, and a keeper fill finds
a price published after its request. Keep-alive runs until the network is reset.

To keep one feed stale on purpose, override it with `keepAlive: false`; a later override of it with

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves out a step that doves/v1/overrides.yaml spells out. Keep-alive only starts once an override with keepAlive: true writes a feed. Overriding one feed with keepAlive: false on its own doesn't make that feed specifically stale: nothing is republished, so every feed goes stale. Please document the two-override recipe here too, as in the template's llm_context: the stale feed with keepAlive: false, plus another feed at its live price with keepAlive: true.

JUPITER_PERPS_DOVES_OVERRIDES_CONTENT,
"jupiter-perps-doves",
);
let idl = serde_json::from_str(JUPITER_PERPS_DOVES_IDL_CONTENT).expect("doves idl");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this parses the Doves IDL a second time (load_protocol_overrides already parsed it a few lines up). The panic message is also less descriptive than the unable to load {} idl: {} format used there. Consider parsing it once and passing a clone to both load_collection and load_keep_alive, or at least matching the existing error message.

pub method: String,
#[schemars(description = "The parameters to pass to the RPC method")]
#[schemars(
description = "The parameters to pass to the RPC method. Wrap every value at every depth by its type: a string as {\"String\":\"...\"}, a whole number as {\"Number\":5}, a boolean as {\"Bool\":true}, a list as {\"Array\":[...]} and an object as {\"Object\":{\"key\":<wrapped value>}}; object keys stay plain. Example for getProgramAccounts with a filter: [{\"String\":\"<program id>\"},{\"Object\":{\"encoding\":{\"String\":\"base64\"},\"filters\":{\"Array\":[{\"Object\":{\"memcmp\":{\"Object\":{\"offset\":{\"Number\":0},\"bytes\":{\"String\":\"<base58>\"}}}}}]}}}]"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change has nothing to do with Jupiter Perps support and isn't mentioned in the PR description. Consider moving it to a separate PR, or calling it out. Also, the long escaped example is hard to maintain inline; a const or a raw string literal (r#"..."#) would read better.

label: Price
description: "Price mantissa; the USD price is price x 10^expo. Example: 8000000000 is $80 at expo -8"
- path: timestamp
label: Publish time

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting timestamp here does nothing unless the override sets keepAlive: false, because keep-alive rewrites it every slot. Consider rejecting or warning on a timestamp value when keepAlive is true. At minimum, the README's "Number formats"/"Keep-alive" sections should mention it, so users don't expect a future-dated timestamp to stick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants