Repository navigation
feat(scenarios): add Jupiter Perps support - state templates - #49
92Infinitus92 wants to merge 14 commits into
Conversation
There was a problem hiding this comment.
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
timestampis required but not exposed: the README andllm_contexttell users to writetimestampon Doves feeds for deposits, swaps, quotes and keeper fills. Thejupiter-perps-doves-pricetemplate only declaresprice. It works through the rawvaluesmap, and the tests depend on that. Any UI that builds forms fromproperties(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.rsrewrites theparamsdescription ofCallSurfnetRpcParams. 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.yamldeclaresaccount_type: Custody, but two of its five templates targetPool. If anything usesaccount_typeto 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 (thecustody/feed/positionmodules) 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 |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
fetchBeforeUseguidance. Thellm_contextof every custody and pool template tells the LLM to "Set fetchBeforeUse: true". The README's troubleshooting table namesfetchBeforeUsereloading only the custody as a cause of the 6014InvalidCustodyBalanceerror. Its last row also says later overrides of the same account should usefetchBeforeUse: 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_contexttoo. -
Unrelated changes in the PR.
.github/workflows/claude-review.ymladds a CI review bot.- The
CallSurfnetRpcParams.paramsschema description incrates/mcpis 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
timestampproperty says quotes refuse a feed 5 seconds old. Thellm_contextand README say 15 seconds for quotes. -
Minor, in the tests:
liquidation_candidateruns a baselineliquidatefor 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. |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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. | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(commitci: claude code review) and rewrites the MCPCallSurfnetRpcParams.paramsdescription. 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_SLOTare asserted against live ProgramData, andKEEPERis 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_contexttext. 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 | |||
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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!( |
There was a problem hiding this comment.
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. | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 needsANTHROPIC_API_KEYandpull-requests: write) and the MCPCallSurfnetRpcParams.paramsdescription 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
getProgramAccountsagainst 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. | |||
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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!( |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
timestampahead 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.ymlis a CI workflow for this fork and has nothing to do with Perps.- The
CallSurfnetRpcParams.paramsschema description incrates/mcpis 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_programasserts the exact ProgramData deploy slot, so any Jupiter redeploy breaks the whole suite. 3bcc64d already had to follow one.open_positionsruns up to 16getProgramAccountsscans on Perps. By default it uses the publicapi.mainnet-betaendpoint, which often rate-limits or rejects these calls.
The suite is behind
integration-tests, so this is acceptable. Still, consider making a missingSURFPOOL_TEST_RPC_URLskip the tests, or documenting that a private RPC is required, so CI doesn't flake. -
Minor:
open_positionssays "newest updates first", but inside each 2^16-second window the results are sorted by pubkey, not byupdate_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 |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
SummaryThis PR adds Jupiter Perps and Doves as bundled protocols:
The templates and Notes
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
Minor: this IDL gets parsed twice, here and again inside
|
There was a problem hiding this comment.
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
timestampahead 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
CallSurfnetRpcParamsdescription change incrates/mcphas nothing to do with Jupiter Perps. It is accurate (it matches the externally taggedJsonValueenum), 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
getProgramAccountsscans of open positions on the public RPC by default. They are gated behindintegration-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 requireSURFPOOL_TEST_RPC_URLrather 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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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.
…live weights cannot refuse its swaps
There was a problem hiding this comment.
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.yamlplus thekeep_alive:import (commits 7a2bad6, 26cc905). Please update the body, including the "stale-price limits" test description: the test is nowprice_overrides_keep_their_feeds_published_at_the_clock. - Unrelated change. The MCP
CallSurfnetRpcParams.paramsschema 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
getProgramAccountsagainst the public mainnet RPC. They are gated behindintegration-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 |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-priceturns it on. Update the "Prices on a fork" paragraph so reviewers and release notes aren't misled. - Unrelated change. The MCP
CallSurfnetRpcParams.paramsdescription 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
getProgramAccountsscans against the public mainnet RPC by default, which is often rate-limited or refused for large programs. This is acceptable since CI doesn't enableintegration-tests, but the module doc should say that a dedicatedSURFPOOL_TEST_RPC_URLis effectively required. - Version mismatch on Doves. The Doves collection is versioned
v1, but it lives underjupiter-perps/doves/v1with protocoljupiter-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 |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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>\"}}}}}]}}}]" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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-pricemoves the price Perps reads;jupiter-perps-custody-risk,-custody-borrow-rateand-custody-feeschange one market;jupiter-perps-pool-limitsand-pool-feeschange the JLP pool.anchor idl convert, as the README's provenance section records.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-priceimports a keep-alive template over every DovesAgPriceFeed(theownedkind from #53, which this PR is stacked on; merge #53 first). Once an override withkeepAlive: trueplays, every Doves feed the surfnet has loaded getstimestampfrom the Clock every slot, so those checks find fresh prices. To keep one feed stale while the others stay fresh, thellm_contextgives the recipe: one override withkeepAlive: falseand one withkeepAlive: 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
fetchBeforeUseon 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_perpsruns 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_clockshows that one price override keeps every pool feed published through produced slots and a time travel, and that a feed withkeepAlive: falseages 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
paramsdescription.Studio: LimeChain/surfpool-web-ui#36