diff --git a/Cargo.lock b/Cargo.lock index 17eb85624..461841b86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6064,9 +6064,9 @@ dependencies = [ [[package]] name = "phoenix-rise-accounts" -version = "0.6.12" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5935fbffbb20cc6f168a11bfca4519797f65ad34fa027fd80c3e2ee00bc7866e" +checksum = "01fb3deafb2b89cde7c9d93b00276694a5c2fb039662d37b0074266c67f18584" dependencies = [ "borsh 1.8.1", "bytemuck", @@ -6078,9 +6078,9 @@ dependencies = [ [[package]] name = "phoenix-rise-math" -version = "0.6.12" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c26875c5a22a98b4b085ee9cb0d045dc60f0ad0f2bbd1f9fbd2228c2e0d760" +checksum = "3177f28a026f24d38b2d78ab607b4f614cd0208e0b8f050272edb935f1632a6c" dependencies = [ "borsh 1.8.1", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 7c084949c..b5afc86aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ mustache = "0.9.0" notify = { version = "8.0.0", default-features = false } npm_rs = "1.0.0" once_cell = { version = "1.19.0", default-features = false } -phoenix-rise-accounts = "0.6.12" +phoenix-rise-accounts = "0.6.15" ratatui = { version = "0.29.0", features = [ "crossterm", ], default-features = false } diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/README.md b/crates/core/src/scenarios/protocols/phoenix-eternal/README.md index f9bbe81b7..8d9a1d57d 100644 --- a/crates/core/src/scenarios/protocols/phoenix-eternal/README.md +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/README.md @@ -1,96 +1,206 @@ # Phoenix Eternal -Surfpool bundles the Phoenix Eternal IDL and eight override templates, so a scenario can put -Phoenix perpetuals into the state your code needs to see: a trader close to liquidation, a moved -mark price, throttled withdrawals. The templates only prepare state. Trades and liquidations are -sent by your own code. +Surfpool bundles the Phoenix Eternal IDL and 19 override templates, so a scenario can put +Phoenix perpetuals into the state your code needs to see: a market moved to a new price, traders +set a planned distance from liquidation, a paused market, throttled withdrawals. The templates only +prepare state. Liquidations and trades are sent by your own code. Every field's meaning and units are on the template itself, visible in Studio and through `get_override_templates`. This page covers what the templates cannot say on their own. For how Phoenix itself works, see the [Phoenix docs](https://docs.phoenix.trade/). +## How the templates change state + +Phoenix keeps one market's state across several accounts that must agree: the oracle readings and +mark in the PerpAssetMap, the makers' liquidity in the spline collection, resting orders in the +orderbook, and each trader's collateral and positions in its Trader account, the +GlobalTraderIndex and the ActiveTraderBuffer. Writing one of them alone leaves the others behind; +a mark moved without the liquidity, for example, lets a liquidation fill at the old price. + +The instruction templates run Phoenix's own instructions on a copy of the local VM. They name the +same signer accounts as on mainnet: the oracle keys, the makers, the trader, or the GlobalConfig +role that owns a setting. Their private keys are not used: signature checks are disabled in the +copy, and only a temporary local fee payer signs. Phoenix still checks the instructions' account +and state constraints: a maintenance factor above the cancel order factor, a withdrawal beyond +the free margin, or an order the margin cannot carry is refused. A refused template is skipped +with a warning naming the program's reason. + +After the instructions succeed, Play writes back the accounts they changed, all or none: when a +write fails, the accounts written before it are put back. That holds for one template. The +templates of a scenario run one after another in their slot and are not one unit: a refused +template is skipped and the ones before it stay applied. + +The collateral, stop-loss and permission templates patch one field of one account. The maintenance +described under "Keeping markets usable" also patches the PerpAssetMap and the spline collections +directly. + ## Terms -| Term | Meaning | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Trader | A trader's account, with its USDC collateral and capability flags in `traderState`. | -| GlobalTraderIndex | Phoenix keeps the state of some traders here instead of in their Trader account: a 16-byte record per listed trader, with its positions in the ActiveTraderBuffer. Phoenix reads that record instead of the Trader. A listed trader has the HOT bit (value 1) of `traderState.flags` set. | -| PerpAssetMap | The single account that holds every market's mark price and risk factors. | -| Hawkeye | A Phoenix program that returns margin and risk views, such as `view_margin`. | -| Effective collateral | What Phoenix measures margin against: USDC collateral, plus the discounted value of SOL collateral and unrealized gains, minus unrealized losses, plus unsettled funding. `view_margin` returns it as `effective_collateral_quote_lots`. | -| Ticks | Phoenix's price unit. USD per base unit = `markTicks * tickSize * 10^(baseLotDecimals - 6)`, all three from `list_phoenix_markets`. | -| Quote lots | Units of Phoenix USDC (mint `PhUsd11YkbjSaWjFncfAAmatntsjx3MgDR9B6g1ks3A`), which has 6 decimals. `1000000` is 1 USDC. | -| Backstop factor | Each market's risk factor for the BackstopLiquidatable tier (`backstopRiskFactorBps`). | +| Term | Meaning | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Trader | A trader's account, with its USDC collateral and capability flags in `traderState`. | +| GlobalTraderIndex | Phoenix keeps the state of active traders here instead of in their Trader account, with their positions in the ActiveTraderBuffer. Phoenix reads those, so the templates check traders through Hawkeye, not the account copy. | +| Hot / cold trader | A trader the GlobalTraderIndex lists, such as one with resting orders, is hot in Phoenix's SDK; any other trader is cold, with its whole state in its Trader account. | +| PerpAssetMap | The single account that holds every market's oracle readings, mark price and risk parameters. | +| Spline | A market maker's liquidity curve around its mid price, in the market's spline collection. Liquidations mostly fill against splines. | +| Resting order | A limit order waiting in a market's book. Resting orders hold margin and keep their trader hot, and a keeper cannot liquidate a trader while any of them add risk. | +| Crossed book | Resting orders left on the wrong side of the makers' quotes after a move, such as bids above the new ask. Phoenix's uncross crank matches them, and anyone can send it. | +| Hawkeye | A Phoenix program that returns margin and risk views, such as `view_margin`. | +| Effective collateral | What Phoenix measures margin against: USDC collateral, plus the discounted value of SOL collateral and unrealized gains, minus unrealized losses, plus unsettled funding. | +| Ticks | Phoenix's price unit. USD per base unit = `markTicks * tickSize * 10^(baseLotDecimals - 6)`, all three from `list_phoenix_markets`. | +| Quote lots | Units of Phoenix USDC (mint `PhUsd11YkbjSaWjFncfAAmatntsjx3MgDR9B6g1ks3A`), which has 6 decimals. `1000000` is 1 USDC. | ## Number formats | Value | Unit | Example | | ---------------------------------------------- | ---------------------------------- | ------------------------ | -| Mark, take-profit and stop-loss prices | Ticks | | +| Prices | Ticks | | | Collateral, withdraw budgets and withdraw fees | Quote lots | `1000000` = 1 USDC | +| Position and liquidation sizes | Base lots | | | Risk factors | Basis points of the initial margin | `5000` = 50% | | Trading fees | Millionths of the filled notional | `350` = 0.035% (3.5 bps) | -| Permission expiry | Unix seconds | `0` = never expires | - -The collateral, mark and maintenance templates take their numbers as decimal strings, so large -values stay exact in JavaScript. The other templates take JSON numbers. - -Set `fetchBeforeUse: true` on every override, so the account is fetched from the upstream -datasource before its bytes are changed. Use `false` only for a later override that builds on -state an earlier override of the same scenario prepared. - -Every Phoenix override also keeps the markets usable for the rest of the session. Oracle updates -keep each market's readings fresh, and Phoenix refuses a market once its readings are older than -its stale threshold times its hard-stale multiplier, or than the threshold alone when the -multiplier is 0. Nothing refreshes them locally, so the override raises every market's -threshold in the local PerpAssetMap instead, fetching the map first when it is not local yet. -Prices and reading slots stay as they were. - -## Traders in the GlobalTraderIndex - -Traders join and leave the GlobalTraderIndex all the time. The `GlobalTraderIndex` will be pulled -from the upstream datasource once, so it can potentially disagree with a Trader that is fetched -later. Hawkeye and the program read a trader from its index record whenever the local -GlobalTraderIndex lists it, whatever the HOT bit says. The templates follow the program: - -- If the local GlobalTraderIndex lists the Trader, its collateral is written to both its record - there and its Trader account. Other TraderState fields are refused for it, since only - collateral is mirrored into the record. -- If the Trader's HOT bit is set but the local GlobalTraderIndex does not list it, the Trader - joined the index after it was pulled. Phoenix rejects every transaction for that Trader with - `TradersViewError::TraderNotFound`, so the collateral override is refused. Restart surfnet to - pull the current GlobalTraderIndex. - -## What the templates enforce - -- **Collateral stress** sets the Trader's USDC collateral (`quoteLotCollateral`) and only lowers - it, since raising it needs a real deposit. The ceiling is the amount on mainnet with - `fetchBeforeUse`, otherwise the amount in the local VM, and for a listed trader it is its index - record. A target above the ceiling is skipped at Play with a warning. -- Lowering `quoteLotCollateral` by N quote lots lowers effective collateral by N. SOL collateral - is separate and still counts toward effective collateral. -- **Mark shock** sets the market's mark and its oracle readings to the target ticks, stamped with - the current slot. The orderbook does not move, so a trade can pull the mark back toward the - book. Compute a relative move from `markTicks` just before Play. -- **Maintenance margin stress** needs a factor above the market's backstop factor and at most - 10000. The maintenance margin scales linearly with it. -- **Trader capabilities** only applies to a Trader the GlobalTraderIndex does not list. The HOT - bit cannot be set: Phoenix would look the trader up in the GlobalTraderIndex, which does not - list it, and reject its transactions. `62` grants every capability except HOT, `54` is - reduce-only and `6` is frozen. -- **Stop-loss trigger** edits the two legs of a StopLosses account placed with `PlaceStopLoss`. - `stopLosses.0` is the greater-than leg (the take-profit of a long, the stop loss of a short) and - `stopLosses.1` the less-than leg. It does not place, resize or execute a leg, and it does not - touch position conditional orders (`PlacePositionConditionalOrder`). Only an active leg fires: - bit 0 of its flags byte, at account byte 49 for `stopLosses.0` and 129 for `stopLosses.1`. - Leave `positionSequenceNumber` as it is. -- **Withdraw limits**: a USDC withdrawal is paid at once only when the queue is empty and it fits - the remaining budget. Otherwise it is queued, never partially filled, until someone calls the - permissionless `ConsumeWithdrawQueue` instruction. SOL withdrawals never queue. -- **Delegated permission**: a PermissionAccount lets a delegated key sign certain Phoenix actions - for an authority. An `expiresAtTimestamp` in the past expires it now, and - `numSignerActionsRemaining` of `0` leaves no actions. Trading delegation through a Trader's - `position_authority` is separate. + +The instruction templates take numbers as decimal strings, so large values stay exact in +JavaScript; the stop-loss and permission templates take JSON numbers. A configuration field left +out or left empty keeps its current value, unless its template says the field is required. + +## fetchBeforeUse + +`fetchBeforeUse: true` makes Play fetch the override's own account from the upstream datasource +first, replacing the local copy. Without it, an override whose account is not in the local VM yet +is skipped. + +Leave it `false` on the templates that change the PerpAssetMap or a Trader. Phoenix keeps one +market across several accounts, and a refetch replaces only the override's own account: +refetching the PerpAssetMap puts every market's prices and factors back to mainnet's, while the +order books and traders keep what earlier scenarios prepared, and the two no longer agree. +`list_phoenix_markets` and `list_phoenix_trader_positions` load those accounts into the surfnet. +The GlobalConfig, WithdrawQueue, StopLosses and permission accounts stand alone, so use `true` the +first time a scenario changes one of them. + +Surfnet copies each account from mainnet the first time it is read, while mainnet keeps trading. +Order books and the ActiveTraderBuffer name traders by their node in the GlobalTraderIndex, so the +three must come from the same moment. The first Phoenix template or tool that needs the index on a +surfnet loads it with the buffer and every market's book in one request. Splines, Trader accounts +and the PerpAssetMap are still copied when first needed, which can leave a collateral or a price +slightly behind mainnet. A Trader account copied at another moment than the index can also +disagree with it on whether the trader is hot: `phoenix-trader-collateral` marks a listed trader's +cold account hot again, so every Phoenix program reads the trader from the index, and a hot trader +the index does not list is refused. + +**Known limitation:** that does not help when your own code read Phoenix accounts on a fresh +surfnet before any Phoenix template or tool ran, or once Phoenix lists more than 98 markets and the +request splits. It shows up as the uncross-crank message, the refusal of a hot trader the +GlobalTraderIndex does not list, or a skipped override failing with `InvalidAccountData` in Hawkeye +before any program log. Reset the Phoenix accounts (`surfnet_resetAccount` on the Phoenix program +with `includeOwnedAccounts: true`) or restart the surfnet, and play again. + +## Templates + +| Template | What it does | Signer accounts | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------- | +| `phoenix-market-move` | Moves a market's oracle readings and its makers' splines to a price; crossed orders stay crossed. | oracle keys, makers | +| `phoenix-market-uncross` | Matches the resting orders a move left crossed with Phoenix's uncross crank, which anyone can send. | none, a permissionless crank | +| `phoenix-oracle-price` | Moves only a market's oracle readings, in ticks or by a percentage; the book and makers stay. | oracle keys | +| `phoenix-trader-cancel-orders` | Cancels a trader's resting orders on every market, taking it out of the index, or on chosen ones. | trader | +| `phoenix-trader-collateral` | Brings a trader's effective collateral to a USDC amount, its distance from liquidation. | field write | +| `phoenix-market-risk-factors` | Maintenance, backstop and high-risk factors. | risk authority | +| `phoenix-market-cancel-risk-factor` | The resting-order factor, the ceiling for the maintenance factor. | risk authority | +| `phoenix-market-max-liquidation-size` | The most base lots one liquidation may close. | risk authority | +| `phoenix-market-open-interest-cap` | The most open interest the market accepts. | risk authority | +| `phoenix-market-funding` | Funding interval, period and maximum rate. | market authority | +| `phoenix-market-fees` | Default taker and maker fees. | market authority | +| `phoenix-market-status` | Active, PostOnly, Paused or Closed. | market authority | +| `phoenix-exchange-status` | The exchange-wide active, gated and maintenance flags. | root authority | +| `phoenix-withdraw-limits` | The withdrawal budget and its refill per slot. | root authority | +| `phoenix-withdraw-parameters` | Deposit cooldown and withdrawal and queueing fees. | risk authority | +| `phoenix-trader-fees` | A trader's fee override multipliers. | market authority | +| `phoenix-trader-capabilities` | Allows or blocks a trader's orders, risk-increasing trades, deposits and withdrawals. | risk authority | +| `phoenix-stop-loss-trigger` | Edits the two legs of a StopLosses account placed with `PlaceStopLoss`. | IDL write | +| `phoenix-permission-limits` | A delegated permission's expiry and remaining actions. | IDL write | + +These templates cover Phoenix's perpetual markets. Templates for spot trading are planned. + +## Recipes + +`phoenix-market-move` and `phoenix-market-uncross` go together, in the same slot and in this order, +and Studio's Phoenix state presets build them that way. Without `phoenix-market-uncross`, resting +orders on the other side of a moved market's new price stay crossed, and trades and liquidations +fill against them at their old prices. + +`phoenix-trader-collateral` takes the effective collateral wanted, in USDC, and lands on it when it +plays: it reads Hawkeye's effective collateral then and moves the collateral field by the +difference, where Phoenix reads it. That is the Trader account or, for a hot trader, which +`list_phoenix_trader_positions` reports as `hot: true`, its record in the GlobalTraderIndex, which +Phoenix copies back to the account when the trader leaves the index. Effective collateral moves one +for one with that field, so pending funding, resting orders and changes since the scenario was +created do not change where it lands. The value can be negative. + +`liquidate_via_market_order` refuses a trader that still has risk-increasing resting orders on any +market, and one whose effective collateral is already below zero. A keeper force-cancels those +orders first with `ForceCancelRiskIncreasing`, which Phoenix allows any signer while the trader is +liquidatable. A scenario can cancel them instead with `phoenix-trader-cancel-orders`, given the +trader's account in its `trader` value and no `symbol`, before `phoenix-trader-collateral` in the +same slot. The overrides of a slot play one after another, each on its own, so a refused cancel +leaves the collateral to play alone. `symbol` picks the markets: empty for every market, which +takes a hot trader out of the index, one symbol such as `SOL`, or several separated by commas such +as `SOL, BTC, ETH`. An unknown symbol refuses the whole override. Positions stay open, and a cold +trader has no orders to cancel. + +Pick the target from the tool's `margin`, the trader with its orders canceled, by what the keeper +should do: + +- **One liquidation:** between `backstopMargin` and `maintenanceMargin`. The keeper's first + liquidation releases enough margin that the account is healthy again. +- **Every position:** between `highRiskMargin` and `backstopMargin`, with the keeper liquidating the + smallest maintenance margin first. A trader with many positions can recover before its last + one. Below `highRiskMargin` a liquidation can leave the trader underwater and fail, and Play + logs a warning. +- **Watch it cross:** pass `moves` to the tool and pick the target between the backstop and + maintenance margin of `marginAfterMoves`. Play `phoenix-trader-cancel-orders`, so the move cannot + fill the trader's orders, and `phoenix-trader-collateral` with + `margin.effectiveCollateral - marginAfterMoves.effectiveCollateral + target`, which must stay + above `margin.maintenanceMargin` so the trader is healthy until the move. Then play those + `phoenix-market-move` overrides, each followed by its `phoenix-market-uncross`, at a later slot, + since a cancel freezes the mark for risk in its own slot. + +A target is worked out from the margins when the scenario is created, and they move with the mark: +after another scenario moves the trader's markets, create it again. + +The tool reports quote lots; divide them by `1000000` for USDC. It fails with the reason when it +cannot read the trader, and reports in `refusal` why either template would refuse it, such as a +spline market maker. Nothing stops a collateral above the current one, which the global vault does +not back; a "healthy now, liquidatable after a move" target can need one. + +Keep `fetchBeforeUse` false on the template. A refused override is skipped and only logs +`Skipping override ...`, for example when Phoenix keeps a trader in its index after the cancel; +check the trader with the tool again after Play. + +## Keeping markets usable + +Oracle updates keep each market's readings fresh on mainnet, and Phoenix refuses a market whose +readings are older than its stale threshold. Nothing updates them in the local VM, so every +Phoenix override also raises every market's stale thresholds in the local PerpAssetMap. Prices +stay as they were. + +This maintenance runs before template validation, including for the two generic IDL templates, +and writes the map only when its data changes. A refused template can therefore leave these +maintenance changes applied. + +Every Phoenix override also moves readings older than the local clock up to its slot. An oracle +report folds the gap between book and oracle into the price, weighted by the slots since the last +reading, so on a map fetched long before the clock a deep move would leave no positive price. + +A running surfnet's clock trails mainnet, so a PerpAssetMap fetched from the upstream datasource +records funding updates from after the local time. Phoenix refuses to update funding, and with it +every price, before the last update, so every Phoenix override also moves those funding +timestamps back to the local clock. Funding then accrues from the local time. + +Makers on mainnet re-quote every few slots, and a spline region stops quoting a set number of slots +after its maker's last update (6 to 60 on SOL). Nobody re-quotes on a surfnet, so every Phoenix +override also loads every market's spline collection and sets each region's lifespan to Phoenix's +good-till-canceled value, writing only collections that change. The books keep their liquidity +while the surfnet's clock runs, so a keeper finds the same quotes long after Play. ## Finding accounts @@ -100,51 +210,38 @@ GlobalTraderIndex lists it, whatever the HOT bit says. The templates follow the | Market orderbook | The `orderbook` of the market in `list_phoenix_markets`. | | StopLosses | PDA `["stoploss", trader account, asset id as u64 little-endian]`, or `getProgramAccounts` with `dataSize: 328` and a memcmp of the trader account at offset 224. | | PermissionAccount | PDA `["permission", authority, delegated key]`, or `getProgramAccounts` with `dataSize: 168` and a memcmp of the authority at offset 8. | -| PerpAssetMap | Fixed address, carried by its templates. | -| WithdrawQueue | Fixed address, carried by its template. | - -## Recipe: liquidation cascade - -1. Pick a trader whose Hawkeye `view_margin` shows a position and `is_liquidatable` 0. -2. At slot 0, use `phoenix-trader-collateral-stress` with this target, all three values from - `view_margin`: - ``` - collateral_quote_lots - effective_collateral_quote_lots + maintenance_margin_quote_lots / 2 - ``` - Effective collateral then sits at half the maintenance margin. -3. At slot 1, use `phoenix-direct-mark-risk-shock` on the trader's market, moving the mark - against the position. -4. Send the liquidation. - -Phoenix ranks a trader by its effective collateral against the margins `view_margin` returns: - -| Effective collateral | Tier | -| -------------------------------------------------------------------- | -------------------- | -| At or above `initial_margin_quote_lots` | Safe | -| Below the initial margin, above `cancel_margin_quote_lots` | AtRisk | -| At or below the cancel margin, above `maintenance_margin_quote_lots` | Cancellable | -| Below the maintenance margin | Liquidatable | -| Below `backstop_margin_quote_lots` | BackstopLiquidatable | -| Below `high_risk_margin_quote_lots` | HighRisk | - -## Studio and MCP - -- In Studio, **Scenario presets → Phoenix state** builds the collateral, mark and maintenance - scenarios with USD and percent inputs. The other templates are in the scenario editor. +| PerpAssetMap | `2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC`: every market's prices and risk factors, which the market templates change. | +| WithdrawQueue | `3c3NTwpg7yW91FxijkHBXwVH1xUifun3Z8TC5eW5Si3K`: the exchange's withdrawal queue and limits, which the withdraw templates change. | +| GlobalConfig | `2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ`: the exchange-wide status flags, which `phoenix-exchange-status` changes. | + +## MCP + - `list_phoenix_markets` returns every market with its symbol, orderbook, `markTicks`, `tickSize`, `baseLotDecimals` and risk factors, read from the local VM. -- `create_phoenix_collateral_scenario` takes `trader` and `targetQuoteLots` and returns a Studio - editor URL. +- `list_phoenix_trader_positions` returns the markets where a trader holds a position, with its + side, size in base lots and maintenance margin, whether it is `hot`, and its `margin` as + `phoenix-trader-cancel-orders` leaves it: a hot trader's orders canceled. Given `moves`, it also + returns `marginAfterMoves`, with each market moved and uncrossed. + Hawkeye's views run on a copy of the surfnet's state, with its markets kept usable the way Play + keeps them, and write nothing back. The reads go through the surfnet, which loads any account + it is missing. - `create_scenario` accepts any template and refuses a market symbol `list_phoenix_markets` does not list. ## Troubleshooting -| Error | Meaning | -| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Phoenix PerpAssetMap ... was not found` or `Phoenix dependency ... is missing locally` | Neither the local VM nor the upstream datasource holds the Phoenix accounts. Start surfnet with a datasource that has the Phoenix deployment. | -| `Hot Phoenix Trader has no reachable GlobalTraderIndex entry` | The trader joined the GlobalTraderIndex after it was pulled; Phoenix rejects its transactions too. Restart surfnet. | -| `Cannot get mark price, staleness or validity check failed` | No Phoenix scenario was played on this surfnet, so the PerpAssetMap kept its fetched stale thresholds and its oracle readings aged past them. Play any Phoenix scenario. | +| Warning or error | Meaning | +| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `Account ... not found in SVM for override ...` | The override's own account is not in the local VM. Load it with the listing tools, or set `fetchBeforeUse: true` on a standalone one. | +| `... accounts are missing locally and there is no datasource` | The template needs Phoenix accounts that are not in the local VM, and surfnet has no upstream datasource to fetch them from. | +| `... is offline and missing locally` | The account is marked offline, so surfnet will not fetch it. | +| `instruction N failed: ... logs: [...]` | Phoenix refused the template's instruction; the logs carry its reason, such as a factor out of range. | +| `... is in Phoenix's active trader index and ...` | `phoenix-trader-cancel-orders` canceled the trader's orders and Phoenix still lists it, or it has none to cancel. | +| `... quotes splines on ... markets; ...` | The trader is a spline market maker, which `phoenix-trader-collateral` does not support. | +| `... is below its high-risk margin ...` | The collateral target is below the trader's high-risk margin; a liquidation can leave it underwater and fail. | +| `the ... book stayed crossed after ... uncross cranks` | `phoenix-market-uncross` left the book crossed after its cranks. Try a smaller move. | +| `InvalidAccountData` before any program log | The order book and the trader index were copied from mainnet at different moments; see the known limitation. | +| `Cannot get mark price, staleness or validity check failed` | No Phoenix scenario was played on this surfnet, so the oracle readings aged past the stale thresholds. Play any Phoenix scenario. | ## Tests against mainnet @@ -152,4 +249,10 @@ Phoenix ranks a trader by its effective collateral against the margins `view_mar cargo test -p surfpool-core --features integration-tests tests::phoenix ``` -Set `SURFPOOL_TEST_RPC_URL` to use a private endpoint if the public one rate-limits. +Set `SURFPOOL_TEST_RPC_URL` to use a private endpoint if the public one rate-limits. CI does not +build this feature, so run them before a push (about half a minute). + +- Each run copies mainnet's exchange accounts and every book once. +- Each test runs Phoenix's mainnet programs on a fresh local copy and plays templates as Play does. +- Liquidations are real keeper transactions. +- Each test opens its own trader locally, with the positions and orders it needs. diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/collateral.rs b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/collateral.rs deleted file mode 100644 index bc6837f35..000000000 --- a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/collateral.rs +++ /dev/null @@ -1,914 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - ops::Range, -}; - -use phoenix_rise_accounts::{ - PhoenixAccount, PhoenixAccountDecodeError, multi_arena::MultiArenaHeader, trader::TraderHeader, -}; -use solana_account::Account; -use solana_pubkey::Pubkey; - -use super::state_builder::PHOENIX_ETERNAL_PROGRAM_ID; -use crate::error::{SurfpoolError, SurfpoolResult}; - -pub fn trader_header(trader: &Pubkey, account: &Account) -> SurfpoolResult { - if account.owner != PHOENIX_ETERNAL_PROGRAM_ID { - return Err(SurfpoolError::invalid_account_owner( - *trader, - None::, - )); - } - let header = TraderHeader::try_read_from_account_bytes(&account.data).map_err(|error| { - SurfpoolError::invalid_account_data( - trader, - "Expected a valid Phoenix Eternal Trader account", - Some(error), - ) - })?; - if Pubkey::new_from_array(header.key) != *trader { - return Err(SurfpoolError::invalid_account_data( - trader, - "Phoenix Trader header key does not match its account address", - None::, - )); - } - Ok(header) -} - -/// A hot Trader's TraderState is also stored in its GlobalTraderIndex record, and Phoenix's -/// margin view (Hawkeye) reads collateral from that record, not from the Trader. Only collateral -/// is written to the record here, so any other TraderState field is refused rather than set on -/// the Trader alone. -pub fn validate_hot_trader_fields( - values: &HashMap, -) -> SurfpoolResult<()> { - if let Some(field) = values.keys().find(|field| { - field.as_str() == "traderState" - || (field.starts_with("traderState.") - && field.as_str() != "traderState.quoteLotCollateral") - }) { - return Err(SurfpoolError::internal(format!( - "Phoenix TraderState field '{field}' is unsupported; only traderState.quoteLotCollateral is mirrored into the GlobalTraderIndex for hot Traders" - ))); - } - Ok(()) -} - -/// Every hot Trader the GlobalTraderIndex tree reaches, paired with the byte range of its -/// TraderState record. The walk starts at the root, so freed nodes, which keep stale keys, flags -/// and collateral, are skipped. A key reached twice makes the tree invalid. -pub fn index_trader_state_ranges(index: &Account) -> SurfpoolResult)>> { - let invalid = || SurfpoolError::internal("Invalid Phoenix GlobalTraderIndex tree"); - if index.owner != PHOENIX_ETERNAL_PROGRAM_ID { - return Err(SurfpoolError::internal( - "Expected a Phoenix-owned GlobalTraderIndex account", - )); - } - let header = MultiArenaHeader::try_from_account_bytes( - "GlobalTraderIndex", - &index.data, - PhoenixAccount::GlobalTraderIndexHeader.discriminant(), - ) - .map_err(|error| { - SurfpoolError::internal(format!("Invalid Phoenix GlobalTraderIndex: {error}")) - })?; - if header.num_arenas() != 1 || header.superblock().num_active_arenas() != 1 { - return Err(SurfpoolError::internal( - "Phoenix collateral overrides currently require a single-arena GlobalTraderIndex", - )); - } - // MultiArenaHeader (48), superblock (32), tree root and padding (16), then - // 1-based Sokoban nodes: four u32 registers, a 32-byte key, and IDL TraderState. - const NODES_START: usize = 96; - const NODE_LEN: usize = 64; - let data = &index.data; - if data.len() < NODES_START || !(data.len() - NODES_START).is_multiple_of(NODE_LEN) { - return Err(invalid()); - } - let capacity = (data.len() - NODES_START) / NODE_LEN; - let read_u32 = |offset| u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); - let mut pending = vec![read_u32(80)]; - let mut visited = HashSet::new(); - let mut keys = HashSet::new(); - let mut ranges = Vec::new(); - while let Some(node) = pending.pop() { - if node == 0 { - continue; - } - if node >= header.superblock().bump_index() - || node as usize > capacity - || !visited.insert(node) - { - return Err(invalid()); - } - let start = NODES_START + (node as usize - 1) * NODE_LEN; - pending.extend([read_u32(start), read_u32(start + 4)]); - let key = Pubkey::new_from_array(data[start + 16..start + 48].try_into().unwrap()); - if !keys.insert(key) { - return Err(invalid()); - } - ranges.push((key, start + 48..start + 64)); - } - if visited.len() != header.superblock().size() as usize { - return Err(invalid()); - } - Ok(ranges) -} - -pub fn index_trader_state_range( - index: &Account, - trader_key: &[u8; 32], -) -> SurfpoolResult> { - let trader_key = Pubkey::new_from_array(*trader_key); - index_trader_state_ranges(index)? - .into_iter() - .find_map(|(key, range)| (key == trader_key).then_some(range)) - .ok_or_else(|| { - SurfpoolError::internal("Hot Phoenix Trader has no reachable GlobalTraderIndex entry") - }) -} - -pub fn current_quote_lot_collateral( - header: &TraderHeader, - index: Option<&Account>, -) -> SurfpoolResult { - // The index is passed whenever Phoenix reads this Trader from its record, even a cold-flagged - // Trader the local index still lists. - if index.is_none() && !header.trader_state.is_hot() { - return Ok(header.trader_state.quote_lot_collateral.as_inner()); - } - let index = index.ok_or_else(|| { - SurfpoolError::internal("Hot Phoenix Trader requires its GlobalTraderIndex account") - })?; - let range = index_trader_state_range(index, &header.key)?; - Ok(i64::from_le_bytes( - index.data[range.start..range.start + 8].try_into().unwrap(), - )) -} - -/// A target near i64::MIN overflows Phoenix's checked margin math instead of making the trader -/// liquidatable. -pub const MIN_QUOTE_LOT_COLLATERAL: i64 = i64::MIN / 2; - -pub fn parse_quote_lot_collateral(value: &serde_json::Value) -> SurfpoolResult { - let collateral = value - .as_i64() - .or_else(|| value.as_str().and_then(|value| value.parse().ok())) - .ok_or_else(|| { - SurfpoolError::internal("Phoenix collateral must be a signed 64-bit integer") - })?; - ensure_collateral_floor(collateral)?; - Ok(collateral) -} - -pub fn ensure_collateral_floor(collateral: i64) -> SurfpoolResult<()> { - if collateral < MIN_QUOTE_LOT_COLLATERAL { - return Err(SurfpoolError::internal(format!( - "Phoenix collateral must be at least {MIN_QUOTE_LOT_COLLATERAL} quote lots" - ))); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use phoenix_rise_accounts::trader::TRADER_CAPABILITY_HOT; - - use super::*; - use crate::scenarios::protocols::phoenix_eternal::v1::state_builder::build_phoenix_collateral_scenario; - - const FIRST_KEY: [u8; 32] = [11; 32]; - const SECOND_KEY: [u8; 32] = [22; 32]; - - #[test] - fn collateral_values_preserve_signed_integer_precision() { - for value in [ - MIN_QUOTE_LOT_COLLATERAL, - -9_007_199_254_740_993, - 0, - i64::MAX, - ] { - assert_eq!( - parse_quote_lot_collateral(&serde_json::json!(value.to_string())).unwrap(), - value - ); - assert_eq!( - parse_quote_lot_collateral(&serde_json::json!(value)).unwrap(), - value - ); - } - for value in [ - serde_json::json!("9223372036854775808"), - serde_json::json!(-1.5), - serde_json::json!(null), - serde_json::json!(i64::MIN), - serde_json::json!((MIN_QUOTE_LOT_COLLATERAL - 1).to_string()), - ] { - assert!(parse_quote_lot_collateral(&value).is_err()); - } - } - - fn write_u32(data: &mut [u8], offset: usize, value: u32) { - data[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); - } - - fn index_account() -> Account { - let mut data = vec![0; 96 + 3 * 64]; - data[..8].copy_from_slice(&PhoenixAccount::GlobalTraderIndexHeader.discriminant()); - write_u32(&mut data, 48, 2); - data[52..54].copy_from_slice(&1_u16.to_le_bytes()); - data[54..56].copy_from_slice(&1_u16.to_le_bytes()); - write_u32(&mut data, 56, 3); - write_u32(&mut data, 60, 4); - write_u32(&mut data, 64, 3); - write_u32(&mut data, 80, 2); - write_u32(&mut data, 160, 1); - write_u32(&mut data, 104, 2); - for (slot, key, collateral) in [ - (0, FIRST_KEY, 111_i64), - (1, SECOND_KEY, 222_i64), - (2, FIRST_KEY, 999_i64), - ] { - let start = 96 + slot * 64; - data[start + 16..start + 48].copy_from_slice(&key); - data[start + 48..start + 56].copy_from_slice(&collateral.to_le_bytes()); - write_u32(&mut data, start + 56, TRADER_CAPABILITY_HOT); - } - Account { - data, - owner: PHOENIX_ETERNAL_PROGRAM_ID, - ..Account::default() - } - } - - fn trader_account(key: [u8; 32], collateral: i64, hot: bool) -> Account { - let mut data = vec![0; core::mem::size_of::() + 16]; - data[..8].copy_from_slice(&PhoenixAccount::Trader.discriminant()); - data[24..56].copy_from_slice(&key); - data[88..96].copy_from_slice(&collateral.to_le_bytes()); - if hot { - write_u32(&mut data, 96, TRADER_CAPABILITY_HOT); - } - Account { - data, - owner: PHOENIX_ETERNAL_PROGRAM_ID, - ..Account::default() - } - } - - #[test] - fn index_lookup_selects_reachable_keys_and_rejects_malformed_trees() { - let index = index_account(); - assert_eq!( - index_trader_state_range(&index, &FIRST_KEY).unwrap(), - 144..160 - ); - assert_eq!( - index_trader_state_range(&index, &SECOND_KEY).unwrap(), - 208..224 - ); - assert!(index_trader_state_range(&index, &[44; 32]).is_err()); - - type Corrupt = fn(&mut Account); - let corruptions: [(&str, Corrupt); 8] = [ - ("only a freed duplicate matches", |index| { - index.data[112..144].copy_from_slice(&[33; 32]) - }), - ("cycle", |index| write_u32(&mut index.data, 96, 2)), - ("size disagrees with the reachable nodes", |index| { - write_u32(&mut index.data, 48, 3) - }), - ("child beyond capacity", |index| { - write_u32(&mut index.data, 60, 100); - write_u32(&mut index.data, 164, 4); - }), - ("child above the bump index", |index| { - write_u32(&mut index.data, 60, 2) - }), - ("duplicate reachable key", |index| { - index.data[176..208].copy_from_slice(&FIRST_KEY) - }), - ("wrong owner", |index| index.owner = Pubkey::new_unique()), - ("wrong discriminator", |index| index.data[..8].fill(0)), - ]; - for (case, corrupt) in corruptions { - let mut index = index_account(); - corrupt(&mut index); - assert!( - index_trader_state_range(&index, &FIRST_KEY).is_err(), - "{case}" - ); - } - for (arenas, active) in [(0_u16, 1_u16), (2, 1), (1, 0), (1, 2)] { - let mut index = index_account(); - index.data[52..54].copy_from_slice(&arenas.to_le_bytes()); - index.data[54..56].copy_from_slice(&active.to_le_bytes()); - assert!(index_trader_state_range(&index, &FIRST_KEY).is_err()); - } - for len in [0, 79, 95, 96 + 3 * 64 - 1] { - let mut index = index_account(); - index.data.truncate(len); - assert!(index_trader_state_range(&index, &FIRST_KEY).is_err()); - } - } - - #[test] - fn index_listing_returns_only_reachable_records() { - let index = index_account(); - let mut listed = index_trader_state_ranges(&index).unwrap(); - listed.sort_by_key(|(_, range)| range.start); - assert_eq!( - listed, - vec![ - (Pubkey::new_from_array(FIRST_KEY), 144..160), - (Pubkey::new_from_array(SECOND_KEY), 208..224), - ], - "slot 2 is a freed duplicate of FIRST_KEY, unreachable from the root and skipped" - ); - - let mut duplicated = index_account(); - duplicated.data[112..144].copy_from_slice(&SECOND_KEY); - assert!( - index_trader_state_ranges(&duplicated).is_err(), - "a key reached twice" - ); - } - - #[tokio::test] - async fn materialize_patches_selected_hot_trader_and_index_record_only() { - use super::super::state_builder::PHOENIX_GLOBAL_TRADER_INDEX; - use crate::surfnet::svm::SurfnetSvm; - - // A Trader the index lists is read from its record even when its own flag says cold, and - // its record caps the target even when its account copy holds less. - for (key, other_key, collateral_offset, target, flagged_hot, account_collateral) in [ - (FIRST_KEY, SECOND_KEY, 144, 1_i64, true, 9_999), - ( - SECOND_KEY, - FIRST_KEY, - 208, - -9_007_199_254_740_993, - true, - 9_999, - ), - (FIRST_KEY, SECOND_KEY, 144, 1_i64, false, 9_999), - (FIRST_KEY, SECOND_KEY, 144, 100_i64, false, 50), - ] { - let trader = Pubkey::new_from_array(key); - let other_trader = Pubkey::new_from_array(other_key); - let index_key = PHOENIX_GLOBAL_TRADER_INDEX; - let mut before_trader = trader_account(key, account_collateral, flagged_hot); - before_trader.lamports = 1; - let mut before_other = trader_account(other_key, 8_888, true); - before_other.lamports = 1; - let mut before_index = index_account(); - before_index.lamports = 1; - let scenario = - build_phoenix_collateral_scenario(trader, &before_trader, &target.to_string()) - .unwrap(); - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&trader, before_trader.clone()).unwrap(); - svm.set_account(&other_trader, before_other.clone()) - .unwrap(); - svm.set_account(&index_key, before_index.clone()).unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); - - assert_eq!(svm.get_account(&trader).unwrap().unwrap(), before_trader); - assert_eq!(svm.get_account(&index_key).unwrap().unwrap(), before_index); - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); - - let mut expected_trader = before_trader; - expected_trader.data[88..96].copy_from_slice(&target.to_le_bytes()); - let mut expected_index = before_index; - expected_index.data[collateral_offset..collateral_offset + 8] - .copy_from_slice(&target.to_le_bytes()); - assert_eq!(svm.get_account(&trader).unwrap().unwrap(), expected_trader); - assert_eq!( - svm.get_account(&index_key).unwrap().unwrap(), - expected_index - ); - assert_eq!( - svm.get_account(&other_trader).unwrap().unwrap(), - before_other - ); - } - } - - #[tokio::test] - async fn materialize_skips_invalid_or_missing_index_without_partial_trader_write() { - use super::super::state_builder::PHOENIX_GLOBAL_TRADER_INDEX; - use crate::surfnet::svm::SurfnetSvm; - - for failure in [ - "cycle", - "missing key", - "missing account", - "mismatched key", - "unsupported field", - "raised collateral", - "raised record of a cold-flagged Trader", - "capabilities of a cold-flagged Trader", - ] { - let trader = Pubkey::new_from_array(FIRST_KEY); - let index_key = PHOENIX_GLOBAL_TRADER_INDEX; - let mut before_trader = trader_account(FIRST_KEY, 9_999, true); - before_trader.lamports = 1; - let mut before_index = index_account(); - before_index.lamports = 1; - let mut scenario = - build_phoenix_collateral_scenario(trader, &before_trader, "1").unwrap(); - match failure { - "mismatched key" => before_trader.data[24..56].copy_from_slice(&SECOND_KEY), - "unsupported field" => { - scenario.overrides[0] - .values - .insert("traderState.flags".to_string(), serde_json::json!(0)); - } - "raised collateral" => { - scenario.overrides[0].values.insert( - "traderState.quoteLotCollateral".to_string(), - serde_json::json!("112"), - ); - } - // Below the account's 9999 but above the 111 Phoenix reads from the record. - "raised record of a cold-flagged Trader" => { - write_u32(&mut before_trader.data, 96, 0); - scenario.overrides[0].values.insert( - "traderState.quoteLotCollateral".to_string(), - serde_json::json!("500"), - ); - } - // What the capabilities template writes; Phoenix reads the bits from the record. - "capabilities of a cold-flagged Trader" => { - write_u32(&mut before_trader.data, 96, 0); - scenario.overrides[0].values = - HashMap::from([("traderState.flags".to_string(), serde_json::json!(54))]); - } - "cycle" => write_u32(&mut before_index.data, 96, 2), - "missing key" => before_index.data[112..144].copy_from_slice(&[33; 32]), - _ => {} - } - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&trader, before_trader.clone()).unwrap(); - if failure != "missing account" { - svm.set_account(&index_key, before_index.clone()).unwrap(); - } - svm.register_scenario(scenario, Some(100)).unwrap(); - - // A rejected override is skipped, never an error out of the batch: that error - // would abort block production. - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap_or_else(|error| panic!("{failure}: {error}")); - assert_eq!( - svm.get_account(&trader).unwrap().unwrap(), - before_trader, - "{failure}" - ); - let after_index = svm.get_account(&index_key).unwrap(); - if failure == "missing account" { - assert!(after_index.is_none()); - } else { - assert_eq!(after_index.unwrap(), before_index, "{failure}"); - } - } - } - - #[tokio::test] - async fn materialize_patches_cold_trader_and_skips_mismatched_header_key() { - use crate::surfnet::svm::SurfnetSvm; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let matching = trader_account(FIRST_KEY, 9_999, false); - for (on_chain_key, target, patched) in [ - (FIRST_KEY, "1", true), - (SECOND_KEY, "1", false), - (FIRST_KEY, "10000", false), - ] { - let mut scenario = build_phoenix_collateral_scenario(trader, &matching, "1").unwrap(); - scenario.overrides[0].values.insert( - "traderState.quoteLotCollateral".to_string(), - serde_json::json!(target), - ); - let mut on_chain = trader_account(on_chain_key, 9_999, false); - on_chain.lamports = 1; - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&trader, on_chain.clone()).unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); - - let mut expected = on_chain; - if patched { - expected.data[88..96].copy_from_slice(&1_i64.to_le_bytes()); - } - assert_eq!(svm.get_account(&trader).unwrap().unwrap(), expected); - } - } - - #[tokio::test] - async fn materialize_indexes_a_stressed_trader_by_owner() { - use crate::surfnet::svm::SurfnetSvm; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let mut on_chain = trader_account(FIRST_KEY, 9_999, false); - on_chain.lamports = 1; - let scenario = build_phoenix_collateral_scenario(trader, &on_chain, "1").unwrap(); - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - // A fetchBeforeUse refetch stores the Trader without indexing it by owner. - svm.inner.set_account(trader, on_chain.clone()).unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); - - // getProgramAccounts serves the local copy of an account only when it is indexed by owner. - let owned = svm - .get_account_owned_by(&PHOENIX_ETERNAL_PROGRAM_ID) - .unwrap(); - let (_, stressed) = owned - .iter() - .find(|(pubkey, _)| *pubkey == trader) - .expect("the stressed Trader must be indexed by owner"); - let mut expected = on_chain; - expected.data[88..96].copy_from_slice(&1_i64.to_le_bytes()); - assert_eq!(stressed, &expected); - } - - #[tokio::test(flavor = "multi_thread")] - async fn fetch_before_use_refreshes_a_hot_traders_index_record() { - use std::collections::HashMap; - - use base64::{Engine, prelude::BASE64_STANDARD}; - use solana_commitment_config::CommitmentConfig; - - use super::super::state_builder::{PHOENIX_GLOBAL_TRADER_INDEX, prepare_phoenix_override}; - use crate::{ - surfnet::{remote::SurfnetRemoteClient, svm::SurfnetSvm}, - tests::helpers::canned_rpc, - }; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let mut account = trader_account(FIRST_KEY, 9_999, true); - account.lamports = 1; - let mut upstream_index = index_account(); - upstream_index.lamports = 1; - let range = index_trader_state_range(&upstream_index, &FIRST_KEY).unwrap(); - let upstream = i64::from_le_bytes( - upstream_index.data[range.start..range.start + 8] - .try_into() - .unwrap(), - ); - // The local VM holds a stressed record, the datasource the unstressed one. The local - // record's flags and fee multipliers differ too, and only its collateral is refreshed. - let mut stressed_index = upstream_index.clone(); - stressed_index.data[range.start..range.start + 8].copy_from_slice(&1_i64.to_le_bytes()); - stressed_index.data[range.start + 14..range.end].copy_from_slice(&[5, 6]); - let url = canned_rpc(format!( - r#"{{"context":{{"apiVersion":"2.1.0","slot":1}},"value":{{"data":["{}","base64"],"executable":false,"lamports":1,"owner":"{}","rentEpoch":0,"space":{}}}}}"#, - BASE64_STANDARD.encode(&upstream_index.data), - PHOENIX_ETERNAL_PROGRAM_ID, - upstream_index.data.len() - )) - .await; - let remote = Some((SurfnetRemoteClient::new(url), CommitmentConfig::confirmed())); - let values = HashMap::from([( - "traderState.quoteLotCollateral".to_string(), - serde_json::json!((upstream / 2).to_string()), - )]); - - for fetch_before_use in [false, true] { - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&PHOENIX_GLOBAL_TRADER_INDEX, stressed_index.clone()) - .unwrap(); - let result = prepare_phoenix_override( - &mut svm, - &trader, - &account, - &values, - &remote, - fetch_before_use, - 100, - ) - .await; - if !fetch_before_use { - assert!( - result.is_err(), - "without fetchBeforeUse the stressed record stays the ceiling" - ); - continue; - } - let writes = result.unwrap().expect("a Trader override"); - let (_, index) = writes - .iter() - .find(|(pubkey, _)| *pubkey == PHOENIX_GLOBAL_TRADER_INDEX) - .expect("the index record is written"); - assert_eq!( - i64::from_le_bytes(index.data[range.start..range.start + 8].try_into().unwrap()), - upstream / 2 - ); - assert_eq!( - index.data[range.start + 8..range.end], - stressed_index.data[range.start + 8..range.end], - "the rest of the record stays local" - ); - } - } - - /// Core refetches the Trader only for the first override in a slot, so only that one refreshes - /// the record. The refreshed collateral stays even when that override is refused, and it caps - /// the later ones until one of them is applied. - #[tokio::test(flavor = "multi_thread")] - async fn a_refused_override_keeps_the_refreshed_record() { - use std::collections::HashMap; - - use base64::{Engine, prelude::BASE64_STANDARD}; - use solana_commitment_config::CommitmentConfig; - - use super::super::state_builder::{PHOENIX_GLOBAL_TRADER_INDEX, prepare_phoenix_override}; - use crate::{ - surfnet::{remote::SurfnetRemoteClient, svm::SurfnetSvm}, - tests::helpers::canned_rpc, - }; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let mut account = trader_account(FIRST_KEY, 9_999, true); - account.lamports = 1; - let mut upstream_index = index_account(); - upstream_index.lamports = 1; - let range = index_trader_state_range(&upstream_index, &FIRST_KEY).unwrap(); - upstream_index.data[range.start..range.start + 8].copy_from_slice(&100_i64.to_le_bytes()); - // The local record predates a drop of the trader's collateral upstream. Its fee multipliers - // and the other trader's record differ locally too, and only the collateral is refreshed. - let mut local_index = upstream_index.clone(); - local_index.data[range.start..range.start + 8].copy_from_slice(&1_000_i64.to_le_bytes()); - local_index.data[range.start + 14..range.end].copy_from_slice(&[5, 6]); - let other = index_trader_state_range(&local_index, &SECOND_KEY).unwrap(); - local_index.data[other.start..other.start + 8].copy_from_slice(&2_000_i64.to_le_bytes()); - let mut refreshed = local_index.data.clone(); - refreshed[range.start..range.start + 8].copy_from_slice(&100_i64.to_le_bytes()); - let url = canned_rpc(format!( - r#"{{"context":{{"apiVersion":"2.1.0","slot":1}},"value":{{"data":["{}","base64"],"executable":false,"lamports":1,"owner":"{}","rentEpoch":0,"space":{}}}}}"#, - BASE64_STANDARD.encode(&upstream_index.data), - PHOENIX_ETERNAL_PROGRAM_ID, - upstream_index.data.len() - )) - .await; - let remote = Some((SurfnetRemoteClient::new(url), CommitmentConfig::confirmed())); - let collateral = - |target: &str| ("traderState.quoteLotCollateral", serde_json::json!(target)); - - // The first override is refused either for its target or for a field a hot Trader refuses. - for first in [ - collateral("200"), - ("traderState.flags", serde_json::json!(62)), - ] { - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&PHOENIX_GLOBAL_TRADER_INDEX, local_index.clone()) - .unwrap(); - for (fetch_before_use, (field, value), applied) in [ - (true, first, false), - (false, collateral("500"), false), - (false, collateral("50"), true), - (false, collateral("80"), false), - ] { - let result = prepare_phoenix_override( - &mut svm, - &trader, - &account, - &HashMap::from([(field.to_string(), value.clone())]), - &remote, - fetch_before_use, - 100, - ) - .await; - assert_eq!(result.is_ok(), applied, "{field} = {value}"); - if fetch_before_use { - let index = svm - .get_account(&PHOENIX_GLOBAL_TRADER_INDEX) - .unwrap() - .unwrap(); - assert_eq!(index.data, refreshed, "{field} = {value}"); - } - if let Ok(Some(writes)) = result { - for (pubkey, written) in writes { - svm.set_account(&pubkey, written).unwrap(); - } - } - } - } - } - - /// A Trader the local index still lists is read from its record even with the HOT bit clear, so - /// fetchBeforeUse refreshes that record from upstream: from the upstream record while upstream - /// lists the Trader, otherwise from the refetched cold Trader account. A hot Trader's account - /// copy is stale, so a hot Trader missing upstream keeps its record. - #[tokio::test(flavor = "multi_thread")] - async fn fetch_before_use_refreshes_the_record_of_a_listed_trader() { - use std::collections::HashMap; - - use base64::{Engine, prelude::BASE64_STANDARD}; - use solana_commitment_config::CommitmentConfig; - - use super::super::state_builder::{PHOENIX_GLOBAL_TRADER_INDEX, prepare_phoenix_override}; - use crate::{ - surfnet::{remote::SurfnetRemoteClient, svm::SurfnetSvm}, - tests::helpers::canned_rpc, - }; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let mut local_index = index_account(); - local_index.lamports = 1; - let range = index_trader_state_range(&local_index, &FIRST_KEY).unwrap(); - local_index.data[range.start..range.start + 8].copy_from_slice(&1_000_i64.to_le_bytes()); - let mut listing = local_index.clone(); - listing.data[range.start..range.start + 8].copy_from_slice(&100_i64.to_le_bytes()); - // The trader has left the upstream index: its reachable node now holds another key. - let mut not_listing = local_index.clone(); - not_listing.data[96 + 16..96 + 48].copy_from_slice(&[33; 32]); - assert!(index_trader_state_ranges(¬_listing).is_ok()); - assert!(index_trader_state_range(¬_listing, &FIRST_KEY).is_err()); - - for (case, upstream, hot, target, applied, record) in [ - ( - "listed upstream, too high", - &listing, - false, - 500_i64, - false, - 100_i64, - ), - ( - "left upstream, too high", - ¬_listing, - false, - 500, - false, - 100, - ), - ("listed upstream, lowered", &listing, false, 50, true, 50), - ("left upstream, lowered", ¬_listing, false, 50, true, 50), - ( - "hot, left upstream", - ¬_listing, - true, - 2_000, - false, - 1_000, - ), - ] { - let url = canned_rpc(format!( - r#"{{"context":{{"apiVersion":"2.1.0","slot":1}},"value":{{"data":["{}","base64"],"executable":false,"lamports":1,"owner":"{}","rentEpoch":0,"space":{}}}}}"#, - BASE64_STANDARD.encode(&upstream.data), - PHOENIX_ETERNAL_PROGRAM_ID, - upstream.data.len() - )) - .await; - let remote = Some((SurfnetRemoteClient::new(url), CommitmentConfig::confirmed())); - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&PHOENIX_GLOBAL_TRADER_INDEX, local_index.clone()) - .unwrap(); - // The Trader account as core just refetched it. - let mut account = trader_account(FIRST_KEY, 100, hot); - account.lamports = 1; - let values = HashMap::from([( - "traderState.quoteLotCollateral".to_string(), - serde_json::json!(target.to_string()), - )]); - - let result = - prepare_phoenix_override(&mut svm, &trader, &account, &values, &remote, true, 100) - .await; - - assert_eq!(result.is_ok(), applied, "{case}"); - if let Ok(Some(writes)) = result { - for (pubkey, written) in writes { - if pubkey == trader { - assert_eq!(written.data[88..96], target.to_le_bytes(), "{case}"); - } - svm.set_account(&pubkey, written).unwrap(); - } - } - let index = svm - .get_account(&PHOENIX_GLOBAL_TRADER_INDEX) - .unwrap() - .unwrap(); - assert_eq!( - index.data[range.start..range.start + 8], - record.to_le_bytes(), - "{case}" - ); - } - } - - #[tokio::test] - async fn materialize_refuses_cold_trader_writes_that_raise_collateral() { - use crate::surfnet::svm::SurfnetSvm; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let matching = trader_account(FIRST_KEY, 9_999, false); - // The whole TraderState is written too, so the collateral it ends with is checked. - for (case, collateral, flags, patched) in [ - ("raise through traderState", 20_000_i64, 0, false), - ("lower through traderState", 1, 0, true), - ( - "lower to the floor through traderState", - MIN_QUOTE_LOT_COLLATERAL, - 0, - true, - ), - ( - "lower past the floor through traderState", - MIN_QUOTE_LOT_COLLATERAL - 1, - 0, - false, - ), - // The index does not list this Trader, so Phoenix could not find it as hot. - ("set the HOT bit through traderState", 1, 63, false), - ] { - let mut scenario = build_phoenix_collateral_scenario(trader, &matching, "1").unwrap(); - scenario.overrides[0].values = HashMap::from([( - "traderState".to_string(), - serde_json::json!({ - "quoteLotCollateral": collateral, - "flags": flags, - "padding": [0], - "globalPositionSequenceNumber": 0, - "makerFeeOverrideMultiplier": 0, - "takerFeeOverrideMultiplier": 0, - }), - )]); - let mut on_chain = matching.clone(); - on_chain.lamports = 1; - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&trader, on_chain.clone()).unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap_or_else(|error| panic!("{case}: {error}")); - - let mut expected = on_chain; - if patched { - expected.data[88..96].copy_from_slice(&collateral.to_le_bytes()); - } - assert_eq!( - svm.get_account(&trader).unwrap().unwrap(), - expected, - "{case}" - ); - } - } - - #[tokio::test(flavor = "multi_thread")] - async fn materialize_indexes_fetched_global_trader_index_by_owner() { - use base64::{Engine, prelude::BASE64_STANDARD}; - use solana_commitment_config::CommitmentConfig; - - use super::super::state_builder::PHOENIX_GLOBAL_TRADER_INDEX; - use crate::{ - surfnet::{remote::SurfnetRemoteClient, svm::SurfnetSvm}, - tests::helpers::canned_rpc, - }; - - let trader = Pubkey::new_from_array(FIRST_KEY); - let mut before_trader = trader_account(FIRST_KEY, 9_999, true); - before_trader.lamports = 1; - let mut remote_index = index_account(); - remote_index.lamports = 1; - let mut scenario = build_phoenix_collateral_scenario(trader, &before_trader, "1").unwrap(); - // The canned RPC answers every request with the index, so the Trader must not be refetched. - scenario.overrides[0].fetch_before_use = false; - // The local VM holds the Trader but has never read the index, so the override fetches it. - let url = canned_rpc(format!( - r#"{{"context":{{"apiVersion":"2.1.0","slot":1}},"value":{{"data":["{}","base64"],"executable":false,"lamports":1,"owner":"{}","rentEpoch":0,"space":{}}}}}"#, - BASE64_STANDARD.encode(&remote_index.data), - PHOENIX_ETERNAL_PROGRAM_ID, - remote_index.data.len() - )) - .await; - let remote = Some((SurfnetRemoteClient::new(url), CommitmentConfig::confirmed())); - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&trader, before_trader).unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); - - svm.materialize_overrides_for_slot(&remote, 100) - .await - .unwrap(); - - // getProgramAccounts serves the local copy of an account only when it is indexed by owner. - let owned = svm - .get_account_owned_by(&PHOENIX_ETERNAL_PROGRAM_ID) - .unwrap(); - let (_, index) = owned - .iter() - .find(|(pubkey, _)| *pubkey == PHOENIX_GLOBAL_TRADER_INDEX) - .expect("the fetched index must be indexed by owner"); - let mut expected = remote_index; - expected.data[144..152].copy_from_slice(&1_i64.to_le_bytes()); - assert_eq!(index, &expected); - } -} diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/idl.json b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/idl.json index 99e5b8fbd..ebc2fb29d 100644 --- a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/idl.json +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/idl.json @@ -16,5853 +16,23677 @@ }, "docs": [ "Anchor-compatible IDL for the Phoenix Eternal program.", + "Instructions marked optionalTrailingAdditionalFeeConfig accept legacy args followed by a Borsh AdditionalFeeConfig, without a Borsh Option tag. No suffix means zero fees. Rates are u8 tenths of a basis point (0..255 = 0..25.5 bps). MakerAndTaker encodes [0, maker, taker]; MakerOnly encodes [1, maker] and sets taker to zero. MakerTakerAndConditionalTaker encodes [2, maker, taker, conditional_taker] and on PlaceLimitOrderWithConditionals the parent uses taker, supplied triggers use conditional_taker, and all resting fills use maker. Other placements ignore conditional_taker and use the ordinary maker/taker pair. Other variants apply the same pair to the parent and its triggers. Standalone conditional placements retain both rates for execution.", + "The additional fee uses the existing trader/position placement authorization and account list; no extra accounts or fee permission are required. The fee applies in addition to the adjusted base trading fee and accrues only on fills.", "This IDL is maintained from the native Rust program surface and includes explicit instruction, account, and event discriminators for transaction and account decoding.", "PDA metadata is included where the program defines a canonical derivation; accounts without PDA metadata are supplied as explicit accounts or are stored in GlobalConfiguration." ], - "instructions": [], - "accounts": [ + "instructions": [ { - "name": "GlobalConfiguration", + "name": "PlaceMarketOrder", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places an immediate-or-cancel order that consumes resting liquidity on the specified market." + ], "discriminator": [ - 37, - 146, - 212, - 210, - 47, - 136, - 111, - 20 + 90, + 118, + 192, + 252, + 192, + 99, + 39, + 145 ], - "docs": [ - "Global exchange configuration account derived from the \"global\" PDA seed.", - "Stores configured authorities, canonical collateral mint, global vault, PerpAssetMap, GlobalTraderIndex, ActiveTraderBuffer, and WithdrawQueue addresses." + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority placing the market order." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] + "args": [ + { + "name": "orderPacket", + "type": { + "defined": { + "name": "OrderPacket" + } } - ] - } + } + ] }, { - "name": "OrderbookHeader", + "name": "PlaceLimitOrder", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places a limit order that may rest on the orderbook or take liquidity depending on matching conditions." + ], "discriminator": [ - 65, + 108, + 176, + 33, + 186, + 146, + 229, 1, - 40, - 241, - 166, - 14, - 86, - 188 + 197 ], - "docs": [ - "Market orderbook account header for a perpetual market.", - "Market orderbooks are explicit market accounts created during AddMarket and recorded in PerpAssetMap metadata; they are not PDAs." + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority placing the limit order." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [ + { + "name": "orderPacket", + "type": { + "defined": { + "name": "OrderPacket" + } + } + } ] }, { - "name": "SplineCollectionHeader", + "name": "PlaceMultiLimitOrder", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places multiple post-only limit orders in a single transaction, allowing batch order placement for market making." + ], "discriminator": [ - 192, - 245, - 182, - 191, - 88, - 37, - 79, - 48 + 236, + 208, + 221, + 172, + 141, + 226, + 129, + 84 ], - "docs": [ - "Spline collection account header derived from [\"spline\", orderbook].", - "Stores spline market-making state for one market." + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority placing the batch of limit orders." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 112, - 108, - 105, - 110, - 101 - ] - }, - { - "kind": "account", - "path": "marketAccount", - "account": "OrderbookHeader" + "args": [ + { + "name": "orderPacket", + "type": { + "defined": { + "name": "MultipleOrderPacket" + } } - ] - } + } + ] }, { - "name": "Trader", + "name": "PlaceMultiLimitOrderV2", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places multiple post-only limit orders with scale-set and reduce-only metadata." + ], "discriminator": [ - 41, - 97, - 73, - 105, - 110, - 214, - 112, - 9 + 64, + 111, + 40, + 210, + 2, + 177, + 38, + 178 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority placing the batch of limit orders." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } ], + "args": [ + { + "name": "multipleOrderPacketV2", + "type": { + "defined": { + "name": "MultipleOrderPacketV2" + } + } + } + ] + }, + { + "name": "CancelOrdersById", "docs": [ - "Trader account derived from [\"trader\", authority, trader_pda_index, trader_subaccount_index].", - "Stores collateral, positions, open orders, and per-trader risk state." + "Cancels a specific set of resting orders identified by their FIFO order ids." ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 116, - 114, - 97, - 100, - 101, - 114 - ] - }, - { - "kind": "account", - "path": "authority" - }, - { - "kind": "arg", - "path": "traderPdaIndex" - }, - { - "kind": "arg", - "path": "traderSubaccountIndex" + "discriminator": [ + 234, + 204, + 126, + 94, + 222, + 22, + 141, + 24 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority requesting the cancellation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account containing the resting orders to cancel." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [ + { + "name": "orderIds", + "type": { + "defined": { + "name": "OrderIds" + } + } + } + ] }, { - "name": "ActiveTraderBufferHeader", + "name": "CancelUpTo", + "docs": [ + "Cancels resting orders on the specified side up to an optional order or price threshold." + ], "discriminator": [ - 192, - 255, - 205, - 165, - 80, - 154, - 131, - 5 + 26, + 209, + 244, + 253, + 59, + 175, + 227, + 54 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority requesting the cancellation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account containing the resting orders to cancel." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CancelUpToInstruction" + } + } + } + ] + }, + { + "name": "CancelAll", "docs": [ - "Header arena for the active trader buffer derived from [\"active_trader_buffer\", 0u16]." + "Cancels every resting order owned by the connected trader on the specified market." ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 97, - 99, - 116, - 105, - 118, - 101, - 95, - 116, - 114, - 97, - 100, - 101, - 114, - 95, - 98, - 117, - 102, - 102, - 101, - 114 - ] - }, - { - "kind": "const", - "value": [ - 0, - 0 - ] - } - ] - } - }, - { - "name": "ActiveTraderBufferArenaHeader", "discriminator": [ - 6, - 249, - 57, - 137, - 82, - 121, + 98, 191, - 107 - ], - "docs": [ - "Additional active trader buffer arena derived from [\"active_trader_buffer\", arena_index]." + 75, + 220, + 115, + 40, + 71, + 237 ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 97, - 99, - 116, - 105, - 118, - 101, - 95, - 116, - 114, - 97, - 100, - 101, - 114, - 95, - 98, - 117, - 102, - 102, - 101, - 114 - ] - }, - { - "kind": "arg", - "path": "arenaIndex" + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority requesting the cancellation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account containing the resting orders to cancel." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [] }, { - "name": "GlobalTraderIndexHeader", + "name": "UpdateOraclePricesWithOrdering", + "docs": [ + "Updates oracle-provided spot and perp prices for one or more perpetual markets with monotonic timestamp ordering." + ], "discriminator": [ - 145, - 92, - 169, - 6, - 5, - 144, - 1, - 205 + 164, + 0, + 14, + 250, + 58, + 198, + 25, + 24 ], - "docs": [ - "Header arena for the global trader index derived from [\"global_trader_index\", 0u16]." + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA; provides oracle authority and quote decimals for conversions." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Wallet authorised to submit oracle price updates (root oracle or delegated user)." + ] + }, + { + "name": "permissionAccount", + "writable": true, + "docs": [ + "Permission account recording oracle capabilities and remaining allowances." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perpetual asset map storing mark price state and oracle metadata." + ] + } ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 116, - 114, - 97, - 100, - 101, - 114, - 95, - 105, - 110, - 100, - 101, - 120 - ] - }, - { - "kind": "const", - "value": [ - 0, - 0 - ] + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateOraclePricesWithOrderingInstruction" + } } - ] - } + } + ] }, { - "name": "GlobalTraderIndexArenaHeader", + "name": "UpdateSplinePrice", + "docs": [ + "Updates the mid-price for a spline market-making program on the specified market." + ], "discriminator": [ - 98, - 110, - 179, - 71, - 203, - 120, + 45, + 155, 200, - 235 - ], - "docs": [ - "Additional global trader index arena derived from [\"global_trader_index\", arena_index]." + 52, + 37, + 214, + 187, + 140 ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108, - 95, - 116, - 114, - 97, - 100, - 101, - 114, - 95, - 105, - 110, - 100, - 101, - 120 - ] - }, - { - "kind": "arg", - "path": "arenaIndex" + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "PerpAssetMap", - "discriminator": [ - 142, - 54, - 115, - 223, - 124, - 239, - 108, - 38 + }, + { + "name": "signerAccount", + "signer": true, + "docs": [ + "Authority of the spline trader account authorised to update the spline price." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Spline trader account whose price is being updated." + ] + }, + { + "name": "splineAccount", + "writable": true, + "docs": [ + "Spline collection PDA containing the spline configuration and pricing data." + ] + }, + { + "name": "orderbook", + "optional": true, + "docs": [ + "Optional market orderbook account included by some clients as trailing metadata." + ] + } ], - "docs": [ - "Perpetual asset map account storing market metadata, risk parameters, oracle metadata, and funding state.", - "The account pubkey is stored in GlobalConfiguration rather than derived from a fixed PDA seed; it is not a PDA." + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateSplinePriceParamsWithOrdering" + } + } + } ] }, { - "name": "WithdrawQueueHeader", + "name": "UpdateSplinePriceWithMetadata", + "docs": [ + "Fixed 32-byte little-endian params followed by a 16-byte client order ID. Flags: bit 0 refreshes regions, bit 1 overrides the sequence number, bit 2 sets updateSlot, bit 3 sets bidWidth, and bit 4 sets askWidth. Unset optional fields are ignored; an explicit zero width resets that side. The processor reads only the fixed params; the client order ID is used off-chain." + ], "discriminator": [ - 125, - 59, - 94, - 108, - 171, - 16, - 43, - 228 + 153, + 162, + 192, + 201, + 188, + 88, + 30, + 224 ], - "docs": [ - "Withdraw queue account storing pending withdrawals and withdrawal throttle state.", - "The account pubkey is stored in GlobalConfiguration rather than derived from a fixed PDA seed; it is not a PDA." + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "signerAccount", + "signer": true, + "docs": [ + "Authority of the spline trader account authorised to update the spline price." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Spline trader account whose price is being updated." + ] + }, + { + "name": "splineAccount", + "writable": true, + "docs": [ + "Spline collection PDA containing the spline configuration and pricing data." + ] + }, + { + "name": "orderbook", + "optional": true, + "docs": [ + "Optional market orderbook account included by some clients as trailing metadata." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateSplinePriceWithMetadataParams" + } + } + } ] }, { - "name": "PermissionAccount", + "name": "DepositFunds", + "docs": [ + "Deposits collateral tokens from a trader's token account into the global vault, crediting the trader's account." + ], "discriminator": [ - 180, - 172, - 109, - 215, - 129, - 165, - 97, - 38 + 202, + 39, + 52, + 211, + 53, + 20, + 250, + 88 ], - "docs": [ - "Permission delegation account derived from [\"permission\", authority, user].", - "Stores delegated capability bits and optional expiry/usage constraints." + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Authority of the trader account making the deposit." + ] + }, + { + "name": "traderTokenAccount", + "writable": true, + "docs": [ + "Trader's token account from which funds are transferred." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA that receives the collateral credit." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Global vault token account that receives the deposited tokens." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for executing the transfer." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 112, - 101, - 114, - 109, - 105, - 115, - 115, - 105, - 111, - 110 - ] - }, - { - "kind": "account", - "path": "authority" - }, - { - "kind": "account", - "path": "user" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DepositFundsInstruction" + } } - ] - } + } + ] }, { - "name": "StopLosses", + "name": "RegisterTrader", + "docs": [ + "Registers a new trader account with the specified configuration and position capacity." + ], "discriminator": [ - 127, - 64, - 139, - 143, - 70, - 43, + 75, + 243, + 224, 167, - 152 - ], - "docs": [ - "Stop-loss account derived from [\"stoploss\", trader_account, asset_id].", - "Stores trigger orders for one trader and market." + 1, + 5, + 51, + 32 ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 115, - 116, - 111, - 112, - 108, - 111, - 115, - 115 + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA that validates the trader registration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Account paying for the trader account creation and rent exemption." + ] + }, + { + "name": "traderWallet", + "docs": [ + "Authority that will control the trader account; used to derive the trader PDA." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA to be created; derived from [\"trader\", trader_wallet, [trader_pda_index, trader_subaccount_index]]." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 114, + 97, + 100, + 101, + 114 + ] + }, + { + "kind": "account", + "path": "traderWallet" + }, + { + "kind": "arg", + "path": "params.traderPdaIndex" + }, + { + "kind": "arg", + "path": "params.traderSubaccountIndex" + } ] - }, - { - "kind": "account", - "path": "traderAccount", - "account": "Trader" - }, - { - "kind": "arg", - "path": "assetId" } - ] - } + }, + { + "name": "systemProgram", + "docs": [ + "System program for creating the trader account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RegisterTraderParams" + } + } + } + ] }, { - "name": "EscrowHeader", + "name": "ReallocTrader", + "docs": [ + "Grows a trader account by one position-map entry when non-position extensions require additional capacity." + ], "discriminator": [ - 50, - 4, - 48, - 59, - 209, - 138, - 251, + 174, + 248, + 63, + 35, + 225, + 236, + 19, 204 ], - "docs": [ - "Escrow inbox account derived from [\"escrow\", authority].", - "Stores pending escrow requests for the authority." - ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 101, - 115, - 99, - 114, - 111, - 119 - ] - }, - { - "kind": "account", - "path": "authority" + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to require the exchange be active." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Account paying the additional rent required for trader account growth." + ] + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet authority stored on the trader account." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA to grow by one map entry when extension capacity lags." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for transferring rent to the trader account." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] }, { - "name": "ConditionalOrderHeader", - "discriminator": [ - 147, - 116, - 229, - 177, - 249, - 200, - 146, - 49 - ], + "name": "LiquidateViaMarketOrder", "docs": [ - "Conditional orders account derived from [\"conditional_orders\", trader_account].", - "Stores attached and position-scoped conditional orders for a trader account." + "Liquidates a trader's position by executing an immediate-or-cancel market order to reduce their exposure." ], - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 99, - 111, - 110, - 100, - 105, - 116, - 105, - 111, - 110, - 97, - 108, - 95, - 111, - 114, - 100, - 101, - 114, - 115 - ] - }, - { - "kind": "account", - "path": "traderAccount", - "account": "Trader" - } - ] - } - } - ], - "types": [ - { - "name": "OrderPacket", - "docs": [ - "Serialized parameters for Phoenix order placement instructions." + "discriminator": [ + 251, + 241, + 184, + 108, + 70, + 70, + 127, + 198 ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "kind", - "type": { - "defined": { - "name": "OrderPacketKind" + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and liquidation is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "liquidatorWallet", + "signer": true, + "docs": [ + "Liquidator authority executing the liquidation." + ] + }, + { + "name": "liquidatedTrader", + "writable": true, + "docs": [ + "Trader account being liquidated (must be in liquidatable state)." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidationParams" + } + } + } + ] }, { - "name": "OrderPacketKind", + "name": "LiquidationTransfer", "docs": [ - "Supported order variants accepted by the Phoenix matching engine." + "Transfers positions directly between a liquidator and liquidatee once the liquidatee is flagged as unhealthy." ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "PostOnly", - "fields": [ - { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } - }, - { - "name": "priceInTicks", - "type": { - "defined": { - "name": "Ticks" - } - } - }, + "discriminator": [ + 57, + 218, + 215, + 68, + 174, + 23, + 204, + 127 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "numBaseLots", - "type": { - "defined": { - "name": "BaseLots" - } - } - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that owns the global trader index, active trader buffer, and collateral controls." + ], + "pda": { + "seeds": [ { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 - ] - } - }, + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits, pricing, and funding parameters for every listed market." + ] + }, + { + "name": "liquidator", + "signer": true, + "docs": [ + "Authority of the liquidator who receives transferred positions." + ] + }, + { + "name": "liquidatorAccount", + "writable": true, + "docs": [ + "Liquidator trader account whose balances are credited as transfers are applied." + ] + }, + { + "name": "liquidateeAccount", + "writable": true, + "docs": [ + "Liquidatee trader account whose unhealthy positions are reduced." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidationTransferParams" + } + } + } + ] + }, + { + "name": "CloseMatchedPositions", + "docs": [ + "Closes offsetting positions between an at-loss trader and an in-profit trader during ADL." + ], + "discriminator": [ + 112, + 174, + 32, + 120, + 93, + 113, + 10, + 161 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "slide", - "type": "bool" - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA that authorizes ADL operations and tracks trader index PDAs." + ], + "pda": { + "seeds": [ { - "name": "lastValidSlot", - "type": { - "option": "u64" - } - }, + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account containing risk parameters used to validate the ADL transfer." + ] + }, + { + "name": "caller", + "signer": true, + "docs": [ + "Configured ADL authority, or a signer holding a risk-rooted adl-execution permission." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "atLossAccount", + "writable": true, + "docs": [ + "Trader account currently at a loss whose matched exposure will be closed." + ] + }, + { + "name": "inProfitAccount", + "writable": true, + "docs": [ + "Trader account with offsetting profitable exposure to receive the transfer." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "optional": true, + "docs": [ + "Optional risk-rooted adl-execution permission. Unused when caller is the configured adl_authority." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ClosePositionsParams" + } + } + } + ] + }, + { + "name": "Log", + "docs": [ + "Internal instruction used by the Phoenix log authority to emit events on-chain.", + "This instruction is called via CPI and should not be invoked directly." + ], + "discriminator": [ + 141, + 230, + 214, + 242, + 9, + 209, + 207, + 170 + ], + "accounts": [ + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA that must sign via CPI." + ], + "signer": true, + "pda": { + "seeds": [ { - "name": "orderFlags", - "type": { - "defined": { - "name": "OrderFlags" - } - } - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + } + ], + "args": [ + { + "name": "eventBatch", + "docs": [ + "Borsh event batch containing the batch index and MarketEvent vector." + ], + "type": { + "defined": { + "name": "OffChainMarketEvent" + } + } + } + ] + }, + { + "name": "LogEventLengths", + "docs": [ + "Internal instruction used by the Phoenix log authority to emit per-event byte lengths on-chain.", + "This instruction is called via CPI and should not be invoked directly." + ], + "discriminator": [ + 247, + 7, + 134, + 203, + 181, + 71, + 153, + 71 + ], + "accounts": [ + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA that must sign via CPI." + ], + "signer": true, + "pda": { + "seeds": [ { - "name": "cancelExisting", - "type": "bool" + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } - ] - }, - { - "name": "Limit", - "fields": [ + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + } + ], + "args": [ + { + "name": "eventLengths", + "docs": [ + "Borsh event-length batch containing the batch index and serialized event lengths." + ], + "type": { + "defined": { + "name": "OffChainMarketEventLengths" + } + } + } + ] + }, + { + "name": "WithdrawFunds", + "docs": [ + "Withdraws collateral tokens from the global vault to a trader's token account, debiting the trader's account balance." + ], + "discriminator": [ + 241, + 36, + 29, + 111, + 208, + 31, + 104, + 217 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ { - "name": "priceInTicks", - "type": { - "defined": { - "name": "Ticks" - } - } - }, + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Authority of the trader account initiating the withdrawal." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA from which collateral is debited." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map for margin and risk checks." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Global vault token account from which tokens are withdrawn." + ] + }, + { + "name": "destinationTokenAccount", + "writable": true, + "docs": [ + "Trader's destination token account to receive the withdrawn tokens." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for executing the transfer." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue PDA tracking pending withdrawal requests." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawFundsInstruction" + } + } + } + ] + }, + { + "name": "UpdateSplineParameters", + "docs": [ + "Updates spline curve parameters for automated market making on bid and ask sides." + ], + "discriminator": [ + 241, + 91, + 139, + 170, + 18, + 41, + 203, + 22 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "numBaseLots", - "type": { - "defined": { - "name": "BaseLots" - } - } - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "signerAccount", + "signer": true, + "docs": [ + "Authority authorized to update spline parameters." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Trader account associated with the spline." + ] + }, + { + "name": "splineAccount", + "writable": true, + "docs": [ + "Spline collection account holding bid and ask curve parameters." + ] + }, + { + "name": "orderbook", + "optional": true, + "docs": [ + "Optional market orderbook account included by some clients as trailing metadata." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateSplineParametersParamsWithOrdering" + } + } + } + ] + }, + { + "name": "PlaceStopLoss", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places a stop-loss order that triggers when the market price reaches the specified trigger price." + ], + "discriminator": [ + 236, + 46, + 164, + 66, + 235, + 92, + 162, + 137 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "selfTradeBehavior", - "type": { - "defined": { - "name": "SelfTradeBehavior" - } - } - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ { - "name": "matchLimit", - "type": { - "option": "u64" - } - }, + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Payer for creating the stop loss account." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map tracking positions and risk metrics." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader wallet authority." + ] + }, + { + "name": "stopLossAccount", + "writable": true, + "docs": [ + "Stop loss account PDA to be created or updated." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for creating the stop loss account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "PlaceStopLossInstruction" + } + } + } + ] + }, + { + "name": "ExecuteStopLoss", + "docs": [ + "Executes a stop-loss order when the trigger conditions are met." + ], + "discriminator": [ + 38, + 122, + 90, + 90, + 182, + 241, + 7, + 4 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 - ] - } - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "signer", + "signer": true, + "docs": [ + "Signer authorized to execute stop loss orders." + ] + }, + { + "name": "permissionAccount", + "writable": true, + "docs": [ + "Permission account verifying execution authority." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map tracking positions and risk metrics." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ { - "name": "lastValidSlot", - "type": { - "option": "u64" - } - }, - { - "name": "orderFlags", - "type": { - "defined": { - "name": "OrderFlags" - } - } - }, - { - "name": "cancelExisting", - "type": "bool" - } - ] - }, - { - "name": "ImmediateOrCancel", - "fields": [ - { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } - }, - { - "name": "priceInTicks", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } - } - }, - { - "name": "numBaseLots", - "type": { - "defined": { - "name": "BaseLots" - } - } - }, - { - "name": "numQuoteLots", - "type": { - "option": { - "defined": { - "name": "QuoteLots" - } - } - } - }, - { - "name": "minBaseLotsToFill", - "type": { - "defined": { - "name": "BaseLots" - } - } - }, - { - "name": "minQuoteLotsToFill", - "type": { - "defined": { - "name": "QuoteLots" - } - } - }, - { - "name": "selfTradeBehavior", - "type": { - "defined": { - "name": "SelfTradeBehavior" - } - } - }, - { - "name": "matchLimit", - "type": { - "option": "u64" - } - }, - { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 - ] - } - }, - { - "name": "lastValidSlot", - "type": { - "option": "u64" - } - }, - { - "name": "orderFlags", - "type": { - "defined": { - "name": "OrderFlags" - } - } - }, - { - "name": "cancelExisting", - "type": "bool" + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - ] + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "Side", - "docs": [ - "Trading direction for an order." + }, + { + "name": "funder", + "writable": true, + "docs": [ + "Account to receive rent from closed stop loss account." + ] + }, + { + "name": "stopLossAccount", + "writable": true, + "docs": [ + "Stop loss account to execute and close." + ] + } ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "Bid" - }, - { - "name": "Ask" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ExecuteStopLossInstruction" + } } - ] - } + } + ] }, { - "name": "SelfTradeBehavior", + "name": "CancelStopLoss", "docs": [ - "Policy for handling matches against the submitting trader's resting orders." + "Cancels a pending stop-loss order and closes the stop loss account." ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "Abort" - }, - { - "name": "CancelProvide" - }, - { - "name": "DecrementTake" - } - ] - } - }, - { - "name": "BaseLots", - "docs": [ - "Quantity wrapper representing discrete base lots." + "discriminator": [ + 120, + 201, + 10, + 102, + 12, + 9, + 111, + 126 ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "QuoteLots", - "docs": [ - "Quantity wrapper representing discrete quote lots." + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "funder", + "writable": true, + "docs": [ + "Account to receive rent from closed stop loss account." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Trader account associated with the stop loss order." + ] + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader wallet authority." + ] + }, + { + "name": "stopLossAccount", + "writable": true, + "docs": [ + "Stop loss account to cancel and close." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for closing the account." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CancelStopLossInstruction" + } } - ] - } + } + ] }, { - "name": "Ticks", + "name": "ForceCancelAll", "docs": [ - "Discrete price unit measured in quote lots per base lot." + "Force cancels all open orders for a trader account. Requires authority authorization." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "discriminator": [ + 201, + 0, + 188, + 127, + 41, + 55, + 80, + 48 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority authorized to force cancel orders." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map tracking positions and risk metrics." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [] }, { - "name": "BaseLotsPerTick", + "name": "ForceCancelRiskIncreasing", "docs": [ - "Density of liquidity per tick for spline market making." + "Force cancels risk-increasing orders for a trader to reduce margin exposure." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "discriminator": [ + 41, + 55, + 217, + 35, + 92, + 72, + 42, + 238 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Signer for the cancel; must be the risk authority while the exchange is in maintenance." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map tracking positions and risk metrics." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [] }, { - "name": "OrderFlags", + "name": "UpdateTraderState", "docs": [ - "Bitmask flags applied to an order: bit 7 reduce-only, bit 6 stop-loss, bit 5 conditional-order, bit 4 stop-loss-direction, bit 3 clear-on-position-flip." + "Settles funding for a trader and evicts them from the active trader buffer when they no longer have resting orders." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "flags", - "type": "u8" + "discriminator": [ + 249, + 139, + 82, + 44, + 126, + 66, + 133, + 220 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Read-only global configuration PDA used to verify the perp asset map and buffer keys." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA whose funding snapshot and active status are updated." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map providing funding parameters for settlement." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [] }, { - "name": "OrderIds", + "name": "ConsumeWithdrawQueue", "docs": [ - "Collection of cancel identifiers used to target specific resting orders." + "Processes pending withdrawals from the withdraw queue." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "orderIds", - "type": { - "vec": { - "defined": { - "name": "CancelId" - } + "discriminator": [ + 228, + 40, + 211, + 139, + 39, + 212, + 116, + 113 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Global vault token account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue account." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program." + ] + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet receiving the withdrawal." + ] + }, + { + "name": "destinationTokenAccount", + "writable": true, + "docs": [ + "Destination token account for withdrawn funds." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + } + ], + "args": [] }, { - "name": "CancelUpToInstruction", + "name": "CreatePermission", "docs": [ - "Parameters controlling cancel-up-to behaviour." + "Creates a new permission account PDA for delegating authority." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "side", - "type": { - "defined": { - "name": "Side" + "discriminator": [ + 190, + 182, + 26, + 164, + 156, + 221, + 8, + 0 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } - } - }, - { - "name": "numOrdersToCancel", - "type": { - "option": "u64" - } - }, - { - "name": "tickLimit", - "type": { - "option": "u64" + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Payer for account creation rent." + ] + }, + { + "name": "permission", + "writable": true, + "docs": [ + "Permission account PDA to create." + ] + }, + { + "name": "authority", + "docs": [ + "Authority granting the permission." + ] + }, + { + "name": "user", + "docs": [ + "User receiving the permission." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] }, { - "name": "CancelId", + "name": "DelegateTrader", "docs": [ - "Identifies a resting order along with an optional hint into the orderbook." + "Delegates position authority of a trader account to another address." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "nodePointer", - "type": { - "defined": { - "name": "NodePointer" + "discriminator": [ + 3, + 164, + 192, + 106, + 119, + 91, + 122, + 224 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "orderId", - "type": { - "defined": { - "name": "FIFOOrderId" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Current trader authority." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA to delegate." + ] + }, + { + "name": "newPositionAuthority", + "docs": [ + "New position authority address." + ] + } + ], + "args": [] }, { - "name": "FIFOOrderId", + "name": "RevokePermission", "docs": [ - "Stable identifier for a resting order composed of price ticks and sequence number." + "Revokes permission flags from a permission account." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "priceInTicks", - "type": { - "defined": { - "name": "Ticks" + "discriminator": [ + 116, + 82, + 33, + 181, + 121, + 144, + 249, + 227 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "orderSequenceNumber", - "type": "u64" } - ] - } - }, - { - "name": "NodePointer", - "docs": [ - "Pointer hint into the orderbook linked list; zero represents a null pointer." + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Root authority revoking the permission." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Optional permission account for delegation." + ] + }, + { + "name": "targetPermission", + "writable": true, + "docs": [ + "Permission account to revoke flags from." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "value", - "type": "u32" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RevokePermissionParams" + } } - ] - } + } + ] }, { - "name": "Symbol", + "name": "SetPermission", "docs": [ - "Fixed-width (16 byte) asset symbol stored as UTF-8 with trailing zero padding." + "Sets permission flags on a permission account." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "symbolBytes", - "type": { - "array": [ - "u8", - 16 + "discriminator": [ + 70, + 126, + 41, + 194, + 245, + 189, + 128, + 8 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } - }, - { - "name": "Price", - "docs": [ - "Price representation composed of a mantissa (`value`) and decimal exponent (`expo`)." + }, + { + "name": "permission", + "writable": true, + "docs": [ + "Permission account to modify." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority owning the permission." + ] + }, + { + "name": "user", + "docs": [ + "User the permission applies to." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "value", - "type": "u64" - }, - { - "name": "expo", - "type": "u8" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetPermissionInstruction" + } } - ] - } + } + ] }, { - "name": "OraclePriceUpdate", + "name": "SetTraderCapabilitiesDelegated", "docs": [ - "Single oracle price update targeting a specific perpetual asset." + "Sets trader capability flags via a delegated permission." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetId", - "type": { - "defined": { - "name": "Symbol" + "discriminator": [ + 207, + 170, + 17, + 21, + 53, + 35, + 88, + 151 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "newExchangePerpPrice", - "type": { - "option": { - "defined": { - "name": "Price" - } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "newExchangeSpotPrice", - "type": { - "defined": { - "name": "Price" - } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority with trader onboarding delegation." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account for delegation validation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TraderCapabilityUpdate" } } - ] - } + } + ] }, { - "name": "UpdateOraclePricesWithOrderingInstruction", + "name": "SetTraderCapability", "docs": [ - "Oracle instruction payload containing ordered price updates and timestamp reset policy." + "Sets trader capability flags via risk authority." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "updateTimestamp", - "type": "u64" - }, - { - "name": "updates", - "type": { - "vec": { - "defined": { - "name": "OraclePriceUpdate" - } + "discriminator": [ + 191, + 72, + 45, + 190, + 214, + 250, + 182, + 213 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "shouldResetTimestamp", - "type": "bool" } - ] - } - }, - { - "name": "UpdateSplinePriceParams", - "docs": [ - "Parameters for updating a spline's mid-price." + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account for delegation validation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "newMidPrice", - "type": "u64" - }, - { - "name": "userUpdateSlot", - "type": { - "option": "u64" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TraderCapabilityUpdate" } - }, - { - "name": "refreshRegions", - "type": "bool" } - ] - } + } + ] }, { - "name": "UpdateSplinePriceParamsWithOrdering", + "name": "SetTraderCapOverride", "docs": [ - "Parameters for updating a spline's mid-price with anti-reordering protection." + "Sets a trader-specific collateral cap override via risk authority or trader-management delegation." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "newMidPrice", - "type": "u64" - }, - { - "name": "userUpdateSlot", - "type": { - "option": "u64" + "discriminator": [ + 191, + 147, + 175, + 185, + 192, + 100, + 233, + 94 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "refreshRegions", - "type": "bool" - }, - { - "name": "userSequenceNumber", - "type": "u64" - }, - { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "overrideSequenceNumber", - "type": "bool" } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority or trader-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account for delegation validation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state account whose collateral cap is updated." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetTraderCapOverrideInstruction" + } + } + } + ] }, { - "name": "DepositFundsInstruction", + "name": "SyncParentToChild", "docs": [ - "Parameters for depositing collateral into the global vault." + "Syncs capabilities from a parent trader account to a child (isolated margin) account." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" + "discriminator": [ + 175, + 137, + 217, + 11, + 235, + 39, + 150, + 19 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet owning both accounts." + ] + }, + { + "name": "parentTraderAccount", + "docs": [ + "Parent (cross margin) trader account." + ] + }, + { + "name": "childTraderAccount", + "writable": true, + "docs": [ + "Child (isolated margin) trader account to sync." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + } + ], + "args": [] }, { - "name": "RegisterTraderParams", + "name": "TransferCollateral", "docs": [ - "Parameters for registering a new trader account." + "Transfers collateral between two trader accounts owned by the same wallet." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "maxPositions", - "type": "u32" - }, - { - "name": "traderPreferenceBits", - "type": "u32" - }, - { - "name": "traderPdaIndex", - "type": "u8" - }, - { - "name": "traderSubaccountIndex", - "type": "u8" + "discriminator": [ + 157, + 163, + 63, + 27, + 242, + 72, + 251, + 97 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader wallet owning both accounts." + ] + }, + { + "name": "srcTraderAccount", + "writable": true, + "docs": [ + "Source trader account." + ] + }, + { + "name": "dstTraderAccount", + "writable": true, + "docs": [ + "Destination trader account." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for margin calculations." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TransferCollateralInstruction" + } + } + } + ] }, { - "name": "LiquidationParams", + "name": "AuthorizedTransferCollateral", "docs": [ - "Parameters for liquidating a trader's position via market order." + "Transfers collateral from a trader account to any other trader account, co-authorized by the trader's position authority and a root-authorized collateral-transfer signer (used by Flight for builder fees)." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetMint", - "type": "pubkey" - }, - { - "name": "liquidationSize", - "type": { - "defined": { - "name": "BaseLots" + "discriminator": [ + 222, + 214, + 69, + 15, + 116, + 115, + 121, + 37 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "liquidationPrice", - "type": { - "defined": { - "name": "Ticks" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "fillOrKill", - "type": "bool" } - ] - } + }, + { + "name": "traderAuthority", + "signer": true, + "docs": [ + "Trader wallet owning both accounts." + ] + }, + { + "name": "positionPermissionAccount", + "docs": [ + "Permission account granting the signer position authority over the source trader, or any placeholder account when the signer is the owner or on-chain position authority." + ], + "writable": true + }, + { + "name": "delegatedAuthority", + "docs": [ + "Root authority, or a key holding the root-granted authorized-collateral-transfer permission (e.g. Flight's collateral transfer authority PDA)." + ], + "signer": true + }, + { + "name": "delegationPermissionAccount", + "docs": [ + "Permission account carrying the authorized-collateral-transfer flag granted by the root authority, or a placeholder when the root authority signs directly." + ], + "writable": true + }, + { + "name": "srcTraderAccount", + "writable": true, + "docs": [ + "Source trader account." + ] + }, + { + "name": "dstTraderAccount", + "writable": true, + "docs": [ + "Destination trader account." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for margin calculations." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TransferCollateralInstruction" + } + } + } + ] }, { - "name": "TransferRequest", + "name": "TransferCollateralChildToParent", "docs": [ - "Transfer payload describing how many base lots of an asset should move from the liquidatee to the liquidator." + "Transfers collateral from a child (isolated margin) account to its parent (cross margin) account." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetId", - "type": "u64" - }, - { - "name": "amount", - "type": { - "defined": { - "name": "BaseLots" + "discriminator": [ + 51, + 100, + 177, + 115, + 139, + 135, + 247, + 139 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet owning both accounts." + ] + }, + { + "name": "childTraderAccount", + "writable": true, + "docs": [ + "Child (isolated margin) trader account." + ] + }, + { + "name": "parentTraderAccount", + "writable": true, + "docs": [ + "Parent (cross margin) trader account." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for margin calculations." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [] }, { - "name": "LiquidationTransferParams", + "name": "UncrossCrank", "docs": [ - "Parameters controlling liquidation transfers between traders." + "Cranks the orderbook to uncross any crossed orders." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "transferRequests", - "type": { - "vec": { - "defined": { - "name": "TransferRequest" - } + "discriminator": [ + 196, + 121, + 94, + 253, + 139, + 42, + 252, + 147 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "executeAllTransfers", - "type": "bool" } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UncrossCrankParams" + } + } + } + ] }, { - "name": "ClosePositionsParams", + "name": "ClearExpiredOrders", "docs": [ - "Parameters for closing matched positions between at-loss and in-profit traders." + "Clears expired orders from the orderbook as a maintenance crank operation." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetId", - "type": "u64" - }, - { - "name": "amount", - "type": { - "defined": { - "name": "BaseLots" + "discriminator": [ + 128, + 95, + 102, + 169, + 146, + 205, + 248, + 94 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "atLossCloseValue", - "type": { - "option": { - "defined": { - "name": "QuoteLots" - } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "inProfitCloseValue", - "type": { - "option": { - "defined": { - "name": "QuoteLots" - } - } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map tracking positions and risk metrics." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ClearExpiredOrdersParams" } } - ] - } + } + ] }, { - "name": "MultipleOrderPacket", + "name": "ClearPositionFlipOrders", "docs": [ - "Batch order packet for placing multiple post-only limit orders in a single transaction." + "Permissionless crank that removes reduce-only clear-on-position-flip orders once the trader's position is zero or on the same side as each order." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "bids", - "type": { - "vec": { - "defined": { - "name": "CondensedOrder" - } - } - } - }, - { - "name": "asks", - "type": { - "vec": { - "defined": { - "name": "CondensedOrder" - } + "discriminator": [ + 245, + 113, + 37, + 98, + 82, + 52, + 120, + 91 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "clientOrderId", - "type": { - "option": { - "array": [ - "u8", - 16 + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that stores mutable exchange-wide configuration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "slide", - "type": "bool" } - ] - } + }, + { + "name": "signer", + "signer": true, + "docs": [ + "Any signer; the crank is permissionless." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account used for risk and market metadata validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account for this market." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ClearPositionFlipOrdersParams" + } + } + } + ] }, { - "name": "MultipleOrderPacketV2", + "name": "CreateConditionalOrdersAccount", "docs": [ - "V2 batch packet: post-only orders with per-order flags (slide, reduce-only) and a scale-set id." + "Allocates the trader's conditional-orders PDA so further conditional-order placements can store entries." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "bids", - "type": { - "vec": { - "defined": { - "name": "CondensedOrderV2" - } - } - } - }, - { - "name": "asks", - "type": { - "vec": { - "defined": { - "name": "CondensedOrderV2" - } + "discriminator": [ + 201, + 6, + 112, + 61, + 231, + 15, + 57, + 248 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "clientOrderId", - "type": { - "option": { - "array": [ - "u8", - 16 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 ] } - } - }, - { - "name": "scaleSetId", - "docs": [ - "Zero denotes no scale set; bits 0-6 hold a client-managed scale-order set id of 1-127 and bit 7 marks a continuation packet (0x80 alone is invalid). The id must be unique among the trader's resting orders on the market unless the packet is a continuation." ], - "type": "u8" + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "CondensedOrder", - "docs": [ - "Condensed order representation for batch order placement (price, size, and optional TiF)." + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Funds the rent for the new conditional-orders PDA." + ] + }, + { + "name": "traderWallet", + "docs": [ + "Trader authority wallet; not signed here so a sponsorship server may pay rent." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Trader state PDA whose key seeds the conditional-orders PDA." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Empty account that will be initialised as the conditional-orders collection." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for PDA creation." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "priceInTicks", - "type": "u64" - }, - { - "name": "sizeInBaseLots", - "type": "u64" - }, - { - "name": "lastValidSlot", - "type": { - "option": "u64" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateConditionalOrdersAccountInstruction" } } - ] - } + } + ] }, { - "name": "CondensedOrderV2", + "name": "PlacePositionConditionalOrder", + "optionalTrailingAdditionalFeeConfig": true, "docs": [ - "Condensed order with per-order flags, used by the V2 batch packet." + "Places a position-based conditional order that fires when mark price crosses the configured trigger." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "priceInTicks", - "type": "u64" - }, - { - "name": "sizeInBaseLots", - "type": "u64" - }, - { - "name": "lastValidSlot", - "docs": [ - "Expiry slot serialized as a raw u64; 0 means no expiry." - ], - "type": "u64" - }, - { - "name": "flags", - "type": { - "defined": { - "name": "CondensedOrderFlags" + "discriminator": [ + 65, + 108, + 83, + 129, + 76, + 193, + 92, + 143 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } - }, - { - "name": "CondensedOrderFlags", - "docs": [ - "Per-order flag bits for CondensedOrderV2: bit 0 = slide, bit 1 = reduce-only. Undefined bits are rejected." + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Pays for any account growth required by the conditional-orders PDA." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA; updated as part of risk validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA for the market." + ] + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority wallet authorising the conditional order." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program (used for any required reallocations)." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "flags", - "type": "u8" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "PlacePositionConditionalOrderInstruction" + } } - ] - } + } + ] }, { - "name": "WithdrawFundsInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64", - "docs": [ - "Amount in quote lots to withdraw." - ] + "name": "CancelConditionalOrder", + "docs": [ + "Cancels one or both trigger-order halves of a conditional-order entry." + ], + "discriminator": [ + 82, + 104, + 25, + 51, + 248, + 54, + 66, + 184 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA owning the conditional order." + ] + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority wallet." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CancelConditionalOrderInstruction" + } + } + } + ] }, { - "name": "UpdateSplineParametersParamsWithOrdering", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bidRegions", - "type": { - "vec": { - "defined": { - "name": "TickRegionParams" - } + "name": "PlaceAttachedConditionalOrder", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Attaches a conditional order to an existing resting limit order so it fires when that order is filled." + ], + "discriminator": [ + 43, + 117, + 136, + 128, + 72, + 150, + 101, + 122 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "askRegions", - "type": { - "vec": { - "defined": { - "name": "TickRegionParams" - } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "refreshRegions", - "type": "bool" - }, - { - "name": "userSequenceNumber", - "type": "u64" - }, - { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "overrideSequenceNumber", - "type": "bool" } - ] - } + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA owning the parent resting order." + ] + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority wallet." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account containing the parent limit order." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Pays for any conditional-orders PDA reallocation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for PDA reallocation." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "PlaceAttachedConditionalOrderInstruction" + } + } + } + ] }, { - "name": "TickRegionParams", - "type": { - "kind": "struct", - "fields": [ - { - "name": "startOffset", - "type": { - "defined": { - "name": "Ticks" + "name": "PlaceLimitOrderWithConditionals", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places a limit order and atomically attaches greater/less-than trigger orders to the resulting resting order." + ], + "discriminator": [ + 95, + 45, + 68, + 168, + 232, + 218, + 210, + 92 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "endOffset", - "type": { - "defined": { - "name": "Ticks" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "density", - "type": { - "defined": { - "name": "BaseLotsPerTick" - } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority wallet." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA." + ] + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Pays for any conditional-orders PDA reallocation." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for PDA reallocation." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "PlaceLimitOrderWithConditionalsInstruction" } - }, - { - "name": "lifespan", - "type": "u64" } - ] - } + } + ] }, { - "name": "PlaceStopLossInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "triggerPrice", - "type": "u64" - }, - { - "name": "executionPrice", - "type": "u64" - }, - { - "name": "tradeSize", - "type": "u64" - }, - { - "name": "tradeSide", - "type": { - "defined": { - "name": "Side" + "name": "ExecuteConditionalOrders", + "docs": [ + "Authority-driven execution of a triggered conditional order." + ], + "discriminator": [ + 101, + 214, + 110, + 0, + 190, + 226, + 177, + 236 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "executionDirection", - "type": { - "defined": { - "name": "Direction" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "orderKind", - "type": { - "defined": { - "name": "StopLossOrderKind" - } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority (or delegate) authorised to execute conditional orders." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Optional permission/delegation PDA validated against the authority." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ExecuteConditionalOrderInstruction" } } - ] - } + } + ] }, { - "name": "ExecuteStopLossInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "executionDirection", - "type": { - "defined": { - "name": "Direction" + "name": "PingConditionalOrders", + "docs": [ + "Authority-driven sweep that activates or invalidates a trader's conditional orders based on current state." + ], + "discriminator": [ + 135, + 170, + 166, + 34, + 208, + 230, + 91, + 64 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } - }, - { - "name": "CancelStopLossInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "executionDirection", - "type": { - "defined": { - "name": "Direction" + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority (or delegate) authorised to ping conditional orders." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Optional permission/delegation PDA." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + } + ], + "args": [] }, { - "name": "ForceCancelRiskIncreasingParams", - "type": { - "kind": "struct", - "fields": [] - } - }, - { - "name": "Direction", - "type": { - "kind": "enum", - "variants": [ - { - "name": "GreaterThan" - }, - { - "name": "LessThan" + "name": "CancelAllPlusConditional", + "docs": [ + "Cancels every resting order for a trader on a market and clears the matching conditional orders." + ], + "discriminator": [ + 142, + 173, + 238, + 126, + 79, + 19, + 53, + 103 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "StopLossOrderKind", - "type": { - "kind": "enum", - "variants": [ - { - "name": "IOC" - }, - { - "name": "Limit" + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA holding exchange-wide parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority wallet." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Trader's conditional-orders collection PDA." + ] + } + ], + "args": [] }, { - "name": "LogHeader", - "type": { - "kind": "struct", - "fields": [ - { - "name": "logBatchIndex", - "docs": [ - "Index of this log batch for tracking event sequences." + "name": "MarketClosedForceCancelAll", + "docs": [ + "Force cancels all open orders and conditional orders for a trader after a market has closed." + ], + "discriminator": [ + 229, + 101, + 143, + 249, + 150, + 175, + 175, + 219 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } ], - "type": "u32" - }, - { - "name": "totalEvents", - "docs": [ - "Number of events serialized in this log batch." + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } ], - "type": "u32" + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account used for risk and market metadata validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account for this market." + ] + }, + { + "name": "traderConditionalOrders", + "writable": true, + "docs": [ + "Optional conditional orders account for the trader; clears stop loss and trigger state when present." + ] + } + ], + "args": [] }, { - "name": "OrderbookHeader", + "name": "ForceCancelAfterHours", "docs": [ - "Fixed metadata prefix for a Phoenix Eternal orderbook account.", - "The large recent-trade buffer and dynamic orderbook bytes follow this prefix and are left as trailing account data by generic decoders." + "Force cancels orders that are invalid during commodity after-hours trading." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "marketStatus", - "type": "u8" - }, - { - "name": "baseLotsDecimals", - "type": "i8" - }, - { - "name": "padding0", - "type": { - "array": [ - "u8", - 6 + "discriminator": [ + 147, + 13, + 224, + 61, + 248, + 234, + 64, + 164 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that stores mutable exchange-wide configuration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "assetId", - "type": "u32" - }, - { - "name": "assetIdPadding", - "type": { - "array": [ - "u8", - 4 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "assetSymbol", - "type": { - "defined": { - "name": "Symbol" + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Signer for the cancel; must be the risk authority while the exchange is in maintenance." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account used for risk and market metadata validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account for this market." + ] + } + ], + "args": [] + }, + { + "name": "AuthorizedForceCancel", + "docs": [ + "Force cancels orders under the configured cancel authority until the requested side sizes remain." + ], + "discriminator": [ + 13, + 22, + 221, + 234, + 211, + 165, + 172, + 2 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "tickSizeInQuoteLotsPerBaseLot", - "type": "u64" - }, - { - "name": "orderSequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that stores mutable exchange-wide configuration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "tradeSequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" - } + } + }, + { + "name": "cancelAuthority", + "signer": true, + "docs": [ + "Configured cancel authority signer." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account used for risk and market metadata validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account for this market." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AuthorizedForceCancelParams" } - }, - { - "name": "defaultTakerFeeMicro", - "type": "u32" - }, - { - "name": "defaultMakerFeeMicro", - "type": "i32" - }, - { - "name": "totalMakerQuoteLotFees", - "type": { - "defined": { - "name": "SignedQuoteLots" + } + } + ] + }, + { + "name": "AuthorizedForceCancelById", + "docs": [ + "Force cancels specific bid and ask orders under the configured cancel authority." + ], + "discriminator": [ + 122, + 159, + 12, + 159, + 144, + 108, + 225, + 241 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "totalTakerQuoteLotFees", - "type": { - "defined": { - "name": "QuoteLots" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that stores mutable exchange-wide configuration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "cancelAuthority", + "signer": true, + "docs": [ + "Configured cancel authority signer." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account PDA that holds position and order state." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account used for risk and market metadata validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account for this market." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AuthorizedForceCancelByIdParams" + } + } + } + ] }, { - "name": "SplineCollectionHeader", + "name": "ChangeMarketStatus", "docs": [ - "Fixed SplineCollectionHeader prefix for a Phoenix Eternal spline collection account.", - "Dynamic spline map bytes follow this header and are left as trailing account data by generic decoders." + "Changes an orderbook market status through the market authority path." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "market", - "type": "pubkey" - }, - { - "name": "assetSymbol", - "type": { - "defined": { - "name": "Symbol" + "discriminator": [ + 221, + 127, + 224, + 41, + 177, + 145, + 126, + 8 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "numSplines", - "type": "u32" - }, - { - "name": "numActive", - "type": "u32" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 32 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Orderbook account whose market status is updated." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map used to validate and update market metadata." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ChangeMarketStatusParams" + } + } + } + ] }, { - "name": "TraderState", + "name": "ChangeExchangeStatus", "docs": [ - "Fixed trader state embedded in the Phoenix Eternal DynamicTraderHeader." + "Patches the exchange-wide status bits; each optional flag preserves the current bit when omitted." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "quoteLotCollateral", - "type": "i64" - }, - { - "name": "flags", - "type": "u32" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 1 + "discriminator": [ + 212, + 102, + 200, + 118, + 138, + 162, + 228, + 165 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "globalPositionSequenceNumber", - "type": "u8" - }, - { - "name": "makerFeeOverrideMultiplier", - "type": "i8" - }, - { - "name": "takerFeeOverrideMultiplier", - "type": "i8" } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA whose exchange status bits are updated." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Direct root authority, or a disable-exchange-features delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account for delegated calls; direct root calls repeat the authority account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ChangeExchangeStatusParams" + } + } + } + ] }, { - "name": "Trader", + "name": "CreateEscrowAccount", "docs": [ - "Fixed DynamicTraderHeader prefix for a Phoenix Eternal trader account.", - "Dynamic position map bytes follow this header and are left as trailing account data by generic decoders." + "Creates or initializes the escrow PDA for a trader wallet." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + "discriminator": [ + 146, + 147, + 225, + 47, + 51, + 64, + 112, + 1 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "key", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "traderState", - "type": { - "defined": { - "name": "TraderState" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "padding0", - "type": { - "array": [ - "u8", - 4 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "withdrawQueueNode", - "type": "u32" - }, - { - "name": "maxPositions", - "type": "u32" - }, - { - "name": "traderPreferenceBits", - "type": "u32" - }, - { - "name": "positionAuthority", - "type": "pubkey" - }, - { - "name": "numMarketsWithSplines", - "type": "u16" - }, - { - "name": "traderPdaIndex", - "type": "u8" - }, - { - "name": "traderSubaccountIndex", - "type": "u8" - }, - { - "name": "fundingKey", - "type": "pubkey" - }, - { - "name": "padding1", - "type": { - "array": [ - "u8", - 4 + } + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Payer funding escrow account creation." + ] + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet that owns the escrow account." + ] + }, + { + "name": "traderEscrow", + "writable": true, + "docs": [ + "Escrow account PDA for the trader wallet." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for creating the escrow account." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateEscrowAccountParams" + } + } + } + ] + }, + { + "name": "CreateEscrowRequest", + "docs": [ + "Creates an escrow request between sender and receiver trader accounts." + ], + "discriminator": [ + 116, + 164, + 236, + 204, + 187, + 132, + 195, + 219 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "lastDepositSlot", - "type": "u64" - }, - { - "name": "conditionalOrderBits", - "type": { - "array": [ - "u8", - 24 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "senderWallet", + "signer": true, + "docs": [ + "Sender wallet signing the escrow request." + ] + }, + { + "name": "senderTraderAccount", + "docs": [ + "Sender trader account whose authority is validated." + ] + }, + { + "name": "permissionAccount", + "writable": true, + "docs": [ + "Permission account authorizing delegated escrow request creation." + ] + }, + { + "name": "receiverWallet", + "docs": [ + "Receiver wallet that owns the escrow account." + ] + }, + { + "name": "receiverTraderAccount", + "docs": [ + "Receiver trader account whose authority is validated." + ] + }, + { + "name": "receiverEscrow", + "writable": true, + "docs": [ + "Receiver escrow account holding pending requests." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map used for margin validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CreateEscrowRequestParams" + } + } + } + ] }, { - "name": "ActiveTraderBufferHeader", + "name": "AcceptEscrowRequest", "docs": [ - "Discriminator-only account definition for viewer compatibility.", - "The full active trader buffer layout is not modeled in this IDL." + "Accepts a pending escrow request and applies its transfer actions." ], - "type": { - "kind": "struct", - "fields": [] - } + "discriminator": [ + 182, + 41, + 62, + 227, + 228, + 175, + 22, + 137 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "senderWallet", + "docs": [ + "Sender wallet for the pending escrow request." + ] + }, + { + "name": "senderTraderAccount", + "writable": true, + "docs": [ + "Sender trader account affected by accepted escrow actions." + ] + }, + { + "name": "receiverWallet", + "signer": true, + "docs": [ + "Receiver wallet accepting the escrow request." + ] + }, + { + "name": "receiverTraderAccount", + "writable": true, + "docs": [ + "Receiver trader account affected by accepted escrow actions." + ] + }, + { + "name": "receiverEscrow", + "writable": true, + "docs": [ + "Receiver escrow account containing the pending request." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map used for margin validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + } + ], + "args": [] }, { - "name": "ActiveTraderBufferArenaHeader", + "name": "CancelEscrowRequest", "docs": [ - "Discriminator-only account definition for viewer compatibility.", - "The full active trader buffer arena layout is not modeled in this IDL." + "Cancels a pending escrow request by sequence number." ], - "type": { - "kind": "struct", - "fields": [] - } + "discriminator": [ + 202, + 26, + 163, + 239, + 32, + 8, + 85, + 2 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "signerWallet", + "signer": true, + "docs": [ + "Sender or receiver wallet authorized to cancel the escrow request." + ] + }, + { + "name": "receiverWallet", + "docs": [ + "Receiver wallet that owns the escrow account." + ] + }, + { + "name": "receiverEscrow", + "writable": true, + "docs": [ + "Receiver escrow account containing the request." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CancelEscrowRequestParams" + } + } + } + ] }, { - "name": "GlobalTraderIndexHeader", + "name": "UpsertEscrowRequest", "docs": [ - "Discriminator-only account definition for viewer compatibility.", - "The full global trader index layout is not modeled in this IDL." + "Creates or increments a cash escrow request for a sender and receiver pair." ], - "type": { - "kind": "struct", - "fields": [] - } - }, - { - "name": "GlobalTraderIndexArenaHeader", - "docs": [ - "Discriminator-only account definition for viewer compatibility.", - "The full global trader index arena layout is not modeled in this IDL." + "discriminator": [ + 213, + 56, + 86, + 93, + 34, + 103, + 160, + 99 ], - "type": { - "kind": "struct", - "fields": [] - } - }, - { - "name": "GlobalConfiguration", - "type": { - "kind": "struct", - "fields": [ - { - "name": "accountKey", - "type": "pubkey" - }, - { - "name": "rootAuthority", - "type": "pubkey" - }, - { - "name": "riskAuthority", - "type": "pubkey" - }, - { - "name": "marketAuthority", - "type": "pubkey" - }, - { - "name": "oracleAuthority", - "type": "pubkey" - }, - { - "name": "successorAuthority", - "type": "pubkey" - }, - { - "name": "adlAuthority", - "type": "pubkey" - }, - { - "name": "cancelAuthority", - "type": "pubkey" - }, - { - "name": "backstopAuthority", - "type": "pubkey" - }, - { - "name": "canonicalTokenMintKey", - "type": "pubkey" - }, - { - "name": "globalVaultKey", - "type": "pubkey" - }, - { - "name": "perpAssetMapKey", - "type": "pubkey" - }, - { - "name": "globalTraderIndexHeaderKey", - "type": "pubkey" - }, - { - "name": "activeTraderBufferHeaderKey", - "type": "pubkey" - }, - { - "name": "totalQuoteLotFees", - "type": { - "defined": { - "name": "QuoteLots" + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "unclaimedQuoteLotFees", - "type": { - "defined": { - "name": "QuoteLots" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "withdrawQueueKey", - "type": "pubkey" - }, - { - "name": "exchangeStatus", - "type": "u8" - }, - { - "name": "quoteDecimals", - "type": "u8" - }, - { - "name": "withdrawalMarginFactorBps", - "type": "u16" - }, - { - "name": "padding0", - "type": { - "array": [ - "u8", - 4 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding1", - "type": { - "array": [ - "u64", - 73 - ] + } + }, + { + "name": "senderWallet", + "signer": true, + "docs": [ + "Sender wallet signing the escrow upsert." + ] + }, + { + "name": "senderTraderAccount", + "docs": [ + "Sender trader account whose authority is validated." + ] + }, + { + "name": "permissionAccount", + "writable": true, + "docs": [ + "Permission account authorizing delegated escrow request creation." + ] + }, + { + "name": "receiverWallet", + "docs": [ + "Receiver wallet that owns the escrow account." + ] + }, + { + "name": "receiverTraderAccount", + "docs": [ + "Receiver trader account whose authority is validated." + ] + }, + { + "name": "receiverEscrow", + "writable": true, + "docs": [ + "Receiver escrow account holding pending requests." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map used for margin validation." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpsertEscrowRequestParams" } - }, - { - "name": "acknowledgedRestartSlot", - "type": "u64" - }, - { - "name": "padding2", - "type": { - "array": [ - "u64", - 22 + } + } + ] + }, + { + "name": "RegisterSpline", + "docs": [ + "Registers a trader as an active spline participant for a market." + ], + "discriminator": [ + 30, + 176, + 160, + 79, + 252, + 117, + 241, + 58 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding3", - "type": { - "array": [ - "u64", - 32 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding4", - "type": { - "array": [ - "u64", - 32 + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "payerAccount", + "writable": true, + "signer": true, + "docs": [ + "Payer funding any required spline account growth." + ] + }, + { + "name": "splineAccount", + "writable": true, + "docs": [ + "Spline collection account for the market." + ] + }, + { + "name": "marketAccount", + "docs": [ + "Market orderbook account for the spline." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account being registered as a spline participant." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map used for isolated-only validation." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for rent transfers or account growth." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "UpdateCommodityMarketState", + "docs": [ + "Updates commodity market state and related mark price inputs." + ], + "discriminator": [ + 45, + 245, + 97, + 184, + 200, + 147, + 142, + 73 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding5", - "type": { - "array": [ - "u64", - 32 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding6", - "type": { - "array": [ - "u64", - 32 + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or commodity-market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account updated with commodity market state." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Orderbook account for the commodity market." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection account for the commodity market." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateCommodityMarketStateInstruction" + } + } + } + ] + }, + { + "name": "UpdatePerpIsolatedOnly", + "docs": [ + "Updates the isolated-only flag for a perp asset." + ], + "discriminator": [ + 101, + 56, + 163, + 250, + 73, + 35, + 115, + 91 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding7", - "type": { - "array": [ - "u64", - 32 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpIsolatedOnlyInstruction" + } + } + } + ] }, { - "name": "SequenceNumber", + "name": "UpdatePerpLeverageTiers", "docs": [ - "Pair of sequence number and last update slot." + "Updates leverage tiers for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "lastUpdateSlot", - "type": "u64" + "discriminator": [ + 110, + 223, + 244, + 99, + 34, + 83, + 97, + 121 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpLeverageTiersInstruction" + } + } + } + ] }, { - "name": "PerpAssetMetadata", + "name": "UpdatePerpOpenInterestCap", "docs": [ - "Opaque fixed-size zero-copy PerpAssetMetadata payload stored inside PerpAssetMap." + "Updates open interest cap for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "raw", - "type": { - "array": [ - "u8", - 1568 + "discriminator": [ + 75, + 137, + 116, + 5, + 80, + 123, + 107, + 177 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpOpenInterestCapInstruction" + } + } + } + ] }, { - "name": "PerpAssetMapEntry", + "name": "UpdatePerpMaxLiquidationSize", "docs": [ - "One fixed slot in the PerpAssetMap StableIndexedShortMap backing array." + "Updates max liquidation size for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "key", - "type": { - "defined": { - "name": "Symbol" + "discriminator": [ + 221, + 63, + 2, + 199, + 44, + 241, + 124, + 222 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "value", - "type": { - "defined": { - "name": "PerpAssetMetadata" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpMaxLiquidationSizeInstruction" + } + } + } + ] }, { - "name": "PerpAssetMetadataStableIndexedShortMap", + "name": "UpdatePerpRiskFactors", "docs": [ - "Zero-copy StableIndexedShortMap layout." + "Updates risk tier thresholds for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "slotsUsed", - "type": "u32" - }, - { - "name": "tombstones", - "type": "u32" - }, - { - "name": "capacity", - "type": "u64" - }, - { - "name": "data", - "type": { - "array": [ - { - "defined": { - "name": "PerpAssetMapEntry" - } - }, - 1024 + "discriminator": [ + 8, + 242, + 209, + 204, + 79, + 145, + 24, + 37 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpRiskFactorsInstruction" + } + } + } + ] }, { - "name": "PerpAssetMap", + "name": "UpdatePerpTransferFeeTiers", "docs": [ - "Canonical Eternal perpetual asset map account." + "Updates liquidation-transfer fee tiers for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + "discriminator": [ + 170, + 246, + 183, + 193, + 115, + 189, + 151, + 94 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } - } - }, - { - "name": "numAssets", - "type": "u16" - }, - { - "name": "padding0", - "type": { - "array": [ - "u8", - 6 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "metadatas", - "type": { - "defined": { - "name": "PerpAssetMetadataStableIndexedShortMap" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpTransferFeeTiersInstruction" + } + } + } + ] }, { - "name": "WithdrawThrottle", + "name": "UpdatePerpUPnlRiskFactor", "docs": [ - "Fixed WithdrawThrottle payload embedded in WithdrawQueueHeader." + "Updates uPnL risk factor for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "maxBudget", - "type": { - "defined": { - "name": "QuoteLots" + "discriminator": [ + 113, + 214, + 226, + 254, + 147, + 9, + 94, + 220 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "remainingBudget", - "type": { - "defined": { - "name": "QuoteLots" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "replenishAmountPerSlot", - "type": { - "defined": { - "name": "QuoteLots" - } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpUPnlRiskFactorInstruction" } - }, - { - "name": "lastUpdateSlot", - "type": "u64" } - ] - } + } + ] }, { - "name": "WithdrawQueueHeader", + "name": "UpdatePerpUPnlRiskFactorForWithdrawals", "docs": [ - "Fixed WithdrawQueueHeader prefix for the Phoenix Eternal withdraw queue account.", - "The deque of pending withdrawal requests follows this header and is left as trailing account data by generic decoders." + "Updates withdrawal uPnL risk factor for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + "discriminator": [ + 252, + 32, + 10, + 88, + 238, + 245, + 104, + 59 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "withdrawThrottle", - "type": { - "defined": { - "name": "WithdrawThrottle" - } - } - }, - { - "name": "totalQueuedAmount", - "type": { - "defined": { - "name": "QuoteLots" - } - } - }, - { - "name": "withdrawalFee", - "type": { - "defined": { - "name": "QuoteLots" - } - } - }, - { - "name": "enqueueingFee", - "type": { - "defined": { - "name": "QuoteLots" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "reserved", - "type": { - "array": [ - "u64", - 5 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpUPnlRiskFactorForWithdrawalsInstruction" + } + } + } + ] }, { - "name": "PermissionAccount", + "name": "UpdatePerpCancelRiskFactor", "docs": [ - "Fixed Phoenix Eternal permission delegation account layout." + "Updates cancel-order risk factor for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "user", - "type": "pubkey" - }, - { - "name": "bump", - "type": "u8" - }, - { - "name": "padding1", - "type": { - "array": [ - "u8", - 7 + "discriminator": [ + 88, + 6, + 185, + 205, + 118, + 28, + 129, + 242 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "permission", - "type": "u64" - }, - { - "name": "expiresAtTimestamp", - "type": "i64" - }, - { - "name": "numSignerActionsRemaining", - "type": "i64" - }, - { - "name": "padding2", - "type": { - "array": [ - "u8", - 64 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpCancelRiskFactorInstruction" + } + } + } + ] }, { - "name": "StopLoss", + "name": "UpdatePerpMarkPriceParameters", "docs": [ - "One fixed trigger order slot stored inside the StopLosses account." + "Updates mark price calculation parameters for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "triggerPrice", - "type": { - "defined": { - "name": "Ticks" - } - } - }, - { - "name": "executionPrice", - "type": { - "defined": { - "name": "Ticks" + "discriminator": [ + 254, + 86, + 13, + 94, + 43, + 224, + 160, + 226 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "tradeSize", - "type": { - "defined": { - "name": "BaseLots" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "slot", - "type": "u64" - }, - { - "name": "positionSequenceNumber", - "type": "u8" - }, - { - "name": "flags", - "type": "u8" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 6 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding2", - "type": { - "array": [ - "u8", - 32 - ] + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpMarkPriceParametersInstruction" } } - ] - } + } + ] }, { - "name": "StopLosses", + "name": "UpdatePerpParametersDelegated", "docs": [ - "Fixed Phoenix Eternal StopLosses account layout." + "Updates the EMA radius, book price radius, and maximum funding rate for a perp asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "stopLosses", - "type": { - "array": [ - { - "defined": { - "name": "StopLoss" - } - }, - 2 + "discriminator": [ + 245, + 185, + 52, + 228, + 181, + 88, + 142, + 222 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "initialized", - "type": "u64" - }, - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } - } - }, - { - "name": "fundingKey", - "type": "pubkey" - }, - { - "name": "traderKey", - "type": "pubkey" - }, - { - "name": "assetId", - "type": "u32" - }, - { - "name": "assetIdPadding", - "type": { - "array": [ - "u8", - 4 + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding", - "type": { - "array": [ - "u64", - 8 - ] + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority or price-parameter-maintenance delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpParametersDelegatedInstruction" } } - ] - } + } + ] }, { - "name": "EscrowHeader", + "name": "UpdateTraderFees", "docs": [ - "Fixed EscrowHeader prefix for a Phoenix Eternal escrow account.", - "The dynamic escrow request list follows this header and is left as trailing account data by generic decoders." + "Updates fee override multipliers for a trader." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "funderKey", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + "discriminator": [ + 182, + 233, + 57, + 205, + 174, + 168, + 73, + 237 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "len", - "type": "u64" - }, - { - "name": "capacity", - "type": "u64" } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or fee-update delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account whose fee overrides are updated." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account; trailing arena accounts may follow in the transaction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateTraderFeesParams" + } + } + } + ] }, { - "name": "ConditionalOrderHeader", + "name": "UpdateMarketFees", "docs": [ - "Fixed ConditionalOrderHeader prefix for a Phoenix Eternal conditional orders account.", - "The dynamic conditional order array follows this header and is left as trailing account data by generic decoders." + "Updates default fee parameters for a market orderbook." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "traderKey", - "type": "pubkey" - }, - { - "name": "fundingKey", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": { - "defined": { - "name": "SequenceNumber" + "discriminator": [ + 187, + 36, + 121, + 171, + 131, + 19, + 243, + 117 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "len", - "type": "u8" - }, - { - "name": "capacity", - "type": "u8" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 6 + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } - }, - { - "name": "RevokePermissionParams", - "type": { - "kind": "struct", - "fields": [ - { - "name": "permissionFlagsToRevoke", - "type": "u64" + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "orderbookAccount", + "writable": true, + "docs": [ + "Orderbook account whose default fee parameters are updated." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateMarketFeesParams" + } } - ] - } + } + ] }, { - "name": "SetPermissionInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "permission", - "type": "u64" - }, - { - "name": "expiresAtTimestamp", - "type": { - "option": "i64" + "name": "UpdateWithdrawRateLimits", + "docs": [ + "Updates withdrawal throttle budget and replenishment parameters." + ], + "discriminator": [ + 187, + 246, + 146, + 182, + 141, + 216, + 36, + 1 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "allowedSignerActions", - "type": { - "option": "i64" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } - }, - { - "name": "TraderCapabilityToggleTarget", - "type": { - "kind": "enum", - "variants": [ - { - "name": "PlaceLimitOrder" - }, - { - "name": "PlaceMarketOrder" - }, - { - "name": "RiskIncreasingTrade" - }, - { - "name": "RiskReducingTrade" - }, - { - "name": "DepositCollateral" - }, - { - "name": "WithdrawCollateral" + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Root authority or withdrawal-suspension delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "withdrawQueueAccount", + "writable": true, + "docs": [ + "Withdraw queue account whose throttle settings are updated." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateWithdrawRateLimitsParams" + } } - ] - } + } + ] }, { - "name": "TraderCapabilityToggle", - "type": { - "kind": "struct", - "fields": [ - { - "name": "target", - "type": { - "defined": { - "name": "TraderCapabilityToggleTarget" + "name": "UpdateWithdrawParameters", + "docs": [ + "Updates withdraw cooldown and fee parameters." + ], + "discriminator": [ + 87, + 209, + 225, + 132, + 91, + 151, + 249, + 132 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "enable", - "type": "bool" } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that stores mutable exchange-wide configuration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "withdrawQueueAccount", + "writable": true, + "docs": [ + "Withdraw queue account whose fee parameters are updated." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateWithdrawParametersParams" + } + } + } + ] }, { - "name": "TraderCapabilityUpdate", - "type": { - "kind": "struct", - "fields": [ - { - "name": "toggles", - "type": { - "vec": { - "defined": { - "name": "TraderCapabilityToggle" - } + "name": "UpdateAuthorities", + "docs": [ + "Updates subordinate authorities stored in global configuration." + ], + "discriminator": [ + 175, + 228, + 137, + 18, + 175, + 70, + 220, + 165 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that stores mutable exchange-wide configuration." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Root authority or permitted delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateAuthoritiesParams" + } + } + } + ] }, { - "name": "SetTraderCapOverrideInstruction", + "name": "InitializeArena", "docs": [ - "Borsh payload for updating a trader's native SOL or spot collateral cap override by asset index." + "Initializes a new global trader index or active trader buffer arena account." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetIndex", - "type": "u32" - }, - { - "name": "maxPerTraderBalanceOverride", - "type": "u64" + "discriminator": [ + 11, + 37, + 221, + 1, + 205, + 120, + 25, + 230 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "TransferCollateralInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "TransferCollateralChildToParentInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority or arena-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "headerAccount", + "writable": true, + "docs": [ + "Arena family header account." + ] + }, + { + "name": "newArenaAccount", + "writable": true, + "docs": [ + "New arena account to create and initialize." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program for creating the arena account." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitializeArenaParams" + } } - ] - } + } + ] }, { - "name": "UncrossCrankParams", - "type": { - "kind": "struct", - "fields": [ - { - "name": "matchLimit", - "type": "u64" + "name": "RemoveEmptyArena", + "docs": [ + "Removes an empty active trader buffer arena account." + ], + "discriminator": [ + 155, + 65, + 162, + 79, + 211, + 221, + 197, + 168 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "ClearExpiredOrdersParams", - "type": { - "kind": "struct", - "fields": [ - { - "name": "maxOrdersPerSide", - "type": { - "option": "u64" + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority or arena-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "headerAccount", + "writable": true, + "docs": [ + "Active trader buffer header account." + ] + }, + { + "name": "removedAccount", + "writable": true, + "docs": [ + "Empty arena account to remove." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] }, { - "name": "ClearPositionFlipOrdersParams", + "name": "RemoveOracle", "docs": [ - "Borsh payload identifying the reduce-only clear-on-position-flip orders to remove." + "Removes one or more oracle public keys from the perp asset map." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "cancelIds", - "type": { - "vec": { - "defined": { - "name": "CancelId" - } + "discriminator": [ + 60, + 93, + 51, + 197, + 182, + 42, + 170, + 26 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Oracle authority or oracle-update delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RemoveOracleInstruction" + } + } + } + ] }, { - "name": "TriggerOrderParams", + "name": "AcknowledgeRestart", "docs": [ - "Trigger and execution parameters for one half of a conditional order." + "Permissionlessly acknowledges the cluster restart slot only when the runtime sysvar still matches the expected slot encoded in the instruction." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "triggerDirection", - "type": { - "defined": { - "name": "Direction" - } - } - }, - { - "name": "tradeSide", - "type": { - "defined": { - "name": "Side" + "discriminator": [ + 211, + 56, + 236, + 97, + 132, + 106, + 54, + 156 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "orderKind", - "type": { - "defined": { - "name": "StopLossOrderKind" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA whose acknowledged restart slot is updated." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "triggerPrice", - "type": "u64" - }, - { - "name": "executionPrice", - "type": "u64" } - ] - } + } + ], + "args": [ + { + "name": "expectedRestartSlot", + "type": "u64" + } + ] }, { - "name": "CreateConditionalOrdersAccountInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "capacity", - "type": "u8" + "name": "RemoveAllOracles", + "docs": [ + "Removes all oracle public keys for a perp asset." + ], + "discriminator": [ + 164, + 64, + 91, + 210, + 137, + 189, + 135, + 38 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Root authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RemoveAllOraclesInstruction" + } + } + } + ] }, { - "name": "PlacePositionConditionalOrderInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "assetId", - "type": "u32" - }, - { - "name": "greaterTriggerOrder", - "type": { - "option": { - "defined": { - "name": "TriggerOrderParams" - } + "name": "SetPermissionDelegated", + "docs": [ + "Sets a whitelisted permission on a target permission account through delegated authority." + ], + "discriminator": [ + 35, + 25, + 57, + 68, + 255, + 26, + 161, + 229 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "lessTriggerOrder", - "type": { - "option": { - "defined": { - "name": "TriggerOrderParams" - } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "sizeBaseLots", - "type": { - "option": "u64" - } - }, - { - "name": "sizePercent", - "type": { - "option": "u8" + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority with permission to set delegated permissions." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "targetPermission", + "writable": true, + "docs": [ + "Permission account whose flags are updated." + ] + }, + { + "name": "from", + "docs": [ + "Authority seed of the target permission PDA." + ] + }, + { + "name": "to", + "docs": [ + "User seed of the target permission PDA." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetPermissionInstruction" } } - ] - } + } + ] }, { - "name": "CancelConditionalOrderInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "index", - "type": "u8" - }, - { - "name": "disableFirst", - "type": "bool" - }, - { - "name": "disableSecond", - "type": "bool" - } - ] - } - }, - { - "name": "PlaceAttachedConditionalOrderInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "orderId", - "type": { - "defined": { - "name": "FIFOOrderId" - } - } - }, - { - "name": "assetId", - "type": "u32" - }, - { - "name": "greaterTriggerOrder", - "type": { - "option": { - "defined": { - "name": "TriggerOrderParams" - } + "name": "DeactivateSpline", + "docs": [ + "Deactivates a trader spline for a market." + ], + "discriminator": [ + 0, + 254, + 22, + 100, + 238, + 197, + 118, + 181 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "lessTriggerOrder", - "type": { - "option": { - "defined": { - "name": "TriggerOrderParams" - } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "splineAccount", + "writable": true, + "docs": [ + "Spline collection account for the market." + ] + }, + { + "name": "marketAccount", + "docs": [ + "Market orderbook account for the spline." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account being deactivated as a spline participant." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map used while updating trader activity." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] }, { - "name": "PlaceLimitOrderWithConditionalsInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "orderPacket", - "type": { - "defined": { - "name": "OrderPacket" - } - } - }, - { - "name": "slot", - "type": "u64" - }, - { - "name": "greaterTriggerOrder", - "type": { - "option": { - "defined": { - "name": "TriggerOrderParams" - } + "name": "ForceCancelStopLoss", + "docs": [ + "Force cancels stop loss state for a trader after market closure." + ], + "discriminator": [ + 114, + 230, + 115, + 170, + 66, + 79, + 19, + 172 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "lessTriggerOrder", - "type": { - "option": { - "defined": { - "name": "TriggerOrderParams" - } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "orderbook", + "docs": [ + "Closed market orderbook associated with the stop loss." + ] + }, + { + "name": "funder", + "writable": true, + "docs": [ + "Funder account receiving reclaimed stop loss rent." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Trader account that owns the stop loss." + ] + }, + { + "name": "stopLossAccount", + "writable": true, + "docs": [ + "Stop loss account to close." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program." + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [] }, { - "name": "ExecuteConditionalOrderInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "index", - "type": "u8" - }, - { - "name": "triggerDirection", - "type": { - "defined": { - "name": "Direction" + "name": "UpdateFundingParameters", + "docs": [ + "Updates funding parameters for a perp asset." + ], + "discriminator": [ + 101, + 195, + 104, + 104, + 116, + 20, + 223, + 188 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateFundingParametersInstruction" + } + } + } + ] }, { - "name": "OffChainMarketEvent", + "name": "CloseMarket", "docs": [ - "Borsh payload of the Log instruction after the 8-byte instruction discriminator." + "Closes a market and records its finalized mark price." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "batchIndex", - "type": "u32" - }, - { - "name": "events", - "type": { - "vec": { - "defined": { - "name": "MarketEvent" - } + "discriminator": [ + 88, + 154, + 248, + 186, + 48, + 14, + 123, + 244 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account updated with finalized mark price." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Orderbook account whose status is moved to Closed." + ] + } + ], + "args": [] }, { - "name": "OffChainMarketEventLengths", + "name": "TombstoneMarket", "docs": [ - "Borsh payload of the LogEventLengths instruction after the 8-byte instruction discriminator." + "Tombstones a closed market after open interest, orders, and splines are cleared." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "batchIndex", - "type": "u32" - }, - { - "name": "lengths", - "type": { - "vec": "u16" + "discriminator": [ + 90, + 177, + 17, + 88, + 85, + 11, + 177, + 96 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Closed orderbook account to tombstone." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map account used to verify open interest is zero." + ] + }, + { + "name": "splineCollection", + "docs": [ + "Spline collection account used to verify no active splines remain." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + } + ], + "args": [] }, { - "name": "SignedBaseLots", + "name": "SettleClosedMarketPosition", "docs": [ - "Signed quantity wrapper representing discrete base lots." + "Settles matching long and short trader positions in a closed market." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "i64" + "discriminator": [ + 27, + 222, + 221, + 233, + 136, + 15, + 254, + 210 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority or market-management delegate." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account containing finalized mark price." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Closed orderbook account for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header account; trailing arena accounts may follow in the transaction." + ] + }, + { + "name": "longTrader", + "writable": true, + "docs": [ + "Trader account with a long position to settle." + ] + }, + { + "name": "shortTrader", + "writable": true, + "docs": [ + "Trader account with a short position to settle." + ] + } + ], + "args": [] }, { - "name": "SignedQuoteLots", + "name": "DeleteTombstonedMarket", "docs": [ - "Signed quantity wrapper representing discrete quote lots." + "Deletes a tombstoned market and reclaims orderbook and spline rent." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "i64" + "discriminator": [ + 146, + 17, + 252, + 116, + 222, + 113, + 52, + 102 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Market authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "lamportRecipient", + "writable": true, + "docs": [ + "Account receiving reclaimed lamports." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account from which the market metadata is removed." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Tombstoned orderbook account to delete." + ] + }, + { + "name": "splineCollection", + "writable": true, + "docs": [ + "Spline collection account to delete." + ] + } + ], + "args": [] }, { - "name": "SignedFeeRateMicro", + "name": "UpdateSplinePositionLimitsConfig", "docs": [ - "Signed fee rate in millionths." + "Updates position-size limits and leverage decrease configuration for a spline trader." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "i32" + "discriminator": [ + 96, + 60, + 5, + 112, + 19, + 144, + 13, + 107 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "SignedQuoteLotsPerBaseLot", - "docs": [ - "Signed quote lots per base lot wrapper." + }, + { + "name": "signerAccount", + "signer": true, + "docs": [ + "Trader or authority signer updating spline position limits." + ] + }, + { + "name": "traderAccount", + "docs": [ + "Trader account associated with the spline." + ] + }, + { + "name": "splineAccount", + "writable": true, + "docs": [ + "Spline collection account holding the trader spline." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "i64" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateSplinePositionLimitsConfigParams" + } } - ] - } + } + ] }, { - "name": "SignedQuoteLotsPerBaseLotUpcasted", + "name": "AddSpotCollateral", "docs": [ - "Upcasted signed quote lots per base lot wrapper." + "Risk authority registers a new SPL spot collateral asset in the perp asset map, allocating its encoded SpotAssetIndex; the asset starts inactive." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "i128" + "discriminator": [ + 238, + 15, + 82, + 195, + 85, + 71, + 216, + 137 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "SignedTicks", - "docs": [ - "Signed discrete tick offset." + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate the risk authority and the perp asset map key." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority signer." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account slot for delegated authority validation; with no delegation this is the authority itself." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account into which the new spot collateral entry is registered." + ] + }, + { + "name": "mint", + "docs": [ + "SPL mint of the collateral asset being registered; must be owned by the legacy SPL Token program and match the params mint address." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "i64" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AddSpotCollateralParams" + } } - ] - } + } + ] }, { - "name": "FundingRateUnitInSeconds", + "name": "ConfigureSpotCollateral", "docs": [ - "Funding time unit measured in seconds." + "Risk authority applies partial risk-parameter updates to a registered spot collateral asset and optionally activates it. Activation is one-way." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "discriminator": [ + 112, + 103, + 162, + 63, + 108, + 147, + 62, + 213 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate the risk authority, the perp asset map key, and the SpotCollaterals feature flag on activation." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority signer." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account slot for delegated authority validation; with no delegation this is the authority itself." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose spot collateral entry is updated." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ConfigureSpotCollateralParams" + } + } + } + ] }, { - "name": "BaseLotsU32", + "name": "SyncSpot", "docs": [ - "u32-backed base lot wrapper." + "Permissionlessly reconciles the balance of the trader wallet PDA's associated token account into the trader account's spot collateral balance for one asset." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u32" + "discriminator": [ + 248, + 53, + 128, + 214, + 54, + 165, + 13, + 2 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Read-only global configuration PDA used to verify the perp asset map and active trader buffer keys." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map providing the asset's metadata and tracking its global collateral balance." + ] + }, + { + "name": "mint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA whose spot collateral balance is reconciled." + ] + }, + { + "name": "traderPhoenixWalletAta", + "docs": [ + "Associated token account of the trader's wallet PDA for the asset's mint; the custody account whose balance is reconciled." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [] }, { - "name": "BaseLotsPerTickU32", + "name": "TransferSpot", "docs": [ - "u32-backed spline liquidity density wrapper." + "Transfers SPL spot collateral between two trader accounts (margin-validated debit on the source), moving the backing tokens between the traders' wallet-PDA custody ATAs." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u32" + "discriminator": [ + 255, + 127, + 0, + 119, + 77, + 53, + 77, + 58 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "mint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "srcTraderPhoenixWallet", + "docs": [ + "Source trader's wallet PDA; CPI authority for the custody token movement." + ] + }, + { + "name": "srcCustodyTokenAccount", + "writable": true, + "docs": [ + "Source trader's custody ATA (owned by srcWallet) debited by the token transfer." + ] + }, + { + "name": "dstCustodyTokenAccount", + "writable": true, + "docs": [ + "Destination trader's custody ATA (owned by the destination trader's wallet PDA); must already exist." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for the custody token transfer." + ] + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Signing authority for the transfer (trader wallet, or a position authority via the optional trailing permission account)." + ] + }, + { + "name": "srcTraderAccount", + "writable": true, + "docs": [ + "Source trader account whose spot collateral is debited." + ] + }, + { + "name": "dstTraderAccount", + "writable": true, + "docs": [ + "Destination trader account whose spot collateral is credited." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for margin calculations and spot asset metadata." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TransferSpotParams" + } + } + } + ] }, { - "name": "BasisPointsU16", + "name": "TransferSpotFromChildToParent", "docs": [ - "Basis points wrapper, u16 backed." + "Sweeps a flat child trader's full balance of one SPL spot collateral asset into its cross-margin parent, moving the backing tokens between the custody ATAs (idempotent no-op if the child is not flat)." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u16" + "discriminator": [ + 49, + 4, + 0, + 150, + 126, + 152, + 163, + 226 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "mint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "childTraderPhoenixWallet", + "docs": [ + "Child trader's wallet PDA; CPI authority for the custody token movement." + ] + }, + { + "name": "childCustodyTokenAccount", + "writable": true, + "docs": [ + "Child trader's custody ATA (owned by childWallet) debited by the sweep." + ] + }, + { + "name": "parentCustodyTokenAccount", + "writable": true, + "docs": [ + "Parent trader's custody ATA (owned by the parent's wallet PDA); must already exist." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for the custody token transfer." + ] + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet owning both accounts; signature only required when the child has disabled collateral sweep." + ] + }, + { + "name": "childTraderAccount", + "writable": true, + "docs": [ + "Child (isolated margin) trader account being swept." + ] + }, + { + "name": "parentTraderAccount", + "writable": true, + "docs": [ + "Parent (cross margin) trader account receiving the collateral." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for spot asset metadata." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [] }, { - "name": "BasisPoints", + "name": "SwapSpotWithSpot", "docs": [ - "Basis points wrapper." + "Swaps between two SPL spot collateral assets through a caller-supplied external venue: debits the source asset from the trader, executes packed venue instructions, and credits the trader with the output received on the signer's destination token account, moved into the destination custody ATA." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "discriminator": [ + 70, + 110, + 253, + 51, + 88, + 242, + 231, + 26 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; must be active, gates position-authority swaps and spot-collateral activity." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map holding spot asset registrations, prices, and global tallies." + ] + }, + { + "name": "srcMint", + "docs": [ + "SPL mint account selecting the source spot asset." + ] + }, + { + "name": "dstMint", + "docs": [ + "SPL mint account selecting the destination spot asset." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for the custody transfers." + ] + }, + { + "name": "traderPhoenixWallet", + "docs": [ + "Trader's wallet PDA; owner of both custody ATAs and CPI authority for the source custody debit." + ] + }, + { + "name": "signer", + "writable": true, + "signer": true, + "docs": [ + "The trader wallet or the trader's position authority; the only key allowed to sign inside the packed venue instructions." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA whose spot collateral balances are debited and credited." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue PDA; the swap's collateral-value decrease is consumed from the contract-wide withdrawal throttle." + ] + }, + { + "name": "srcTraderPhoenixWalletAta", + "writable": true, + "docs": [ + "Wallet PDA's custody associated token account for the source mint; debited by amountIn." + ] + }, + { + "name": "signerSrcTokenAccount", + "writable": true, + "docs": [ + "Signer-owned token account for the source mint; the external venue spends the withdrawn tokens from here." + ] + }, + { + "name": "signerDstTokenAccount", + "writable": true, + "docs": [ + "Signer-owned token account for the destination mint; the external venue pays the output here, and its balance increase is moved into the destination custody account and credited." + ] + }, + { + "name": "dstTraderPhoenixWalletAta", + "writable": true, + "docs": [ + "Wallet PDA's custody associated token account for the destination mint; receives the swept output." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account. Remaining accounts follow: caller-supplied venue accounts referenced by the packed instructions." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SwapSpotWithSpotInstruction" + } + } + } + ] }, { - "name": "Constant", + "name": "SwapSpotWithUsdc", "docs": [ - "Dimensionless constant wrapper." + "Swaps between an SPL spot collateral asset and USDC quote collateral through an external venue: Sell debits spot and sweeps received USDC into the global vault as quote collateral; Buy debits USDC from the vault and credits the spot received on the signer's spot token account, moved into the spot custody ATA." ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "discriminator": [ + 119, + 121, + 96, + 136, + 111, + 0, + 215, + 223 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "OptionalNonZeroU64", - "docs": [ - "Zero represents None; non-zero represents Some(value)." + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; must be active, supplies the canonical quote mint and deposit cooldown." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map holding spot asset registrations, prices, and global tallies." + ] + }, + { + "name": "spotMint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Global USDC vault; receives swept USDC on Sell and pays out the quote leg on Buy." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for the token transfers." + ] + }, + { + "name": "traderPhoenixWallet", + "docs": [ + "Trader's wallet PDA; owner of the spot custody ATA and CPI authority for the Sell custody debit." + ] + }, + { + "name": "signer", + "writable": true, + "signer": true, + "docs": [ + "The trader wallet or the trader's position authority; the only key allowed to sign inside the packed venue instructions." + ] + }, + { + "name": "signerQuoteTokenAccount", + "writable": true, + "docs": [ + "Signer-owned token account of the canonical quote mint; venue-facing USDC leg (measured on Sell, funded on Buy)." + ] + }, + { + "name": "signerSpotTokenAccount", + "writable": true, + "docs": [ + "Signer-owned token account for the spot mint; receives the Sell custody debit for the venue to spend, and the Buy output the venue pays in." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA whose quote and spot collateral balances change." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue PDA; the swap's collateral-value decrease is consumed from the contract-wide withdrawal throttle." + ] + }, + { + "name": "spotTraderPhoenixWalletAta", + "writable": true, + "docs": [ + "Wallet PDA's custody associated token account for the spot mint; debited on Sell, receives the swept output on Buy." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account. Remaining accounts follow: caller-supplied venue accounts referenced by the packed instructions." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SwapSpotWithUsdcInstruction" + } } - ] - } + } + ] }, { - "name": "OptionalFIFOOrderId", - "type": { - "kind": "struct", - "fields": [ - { - "name": "priceInTicks", - "type": { - "defined": { - "name": "Ticks" + "name": "SwapSpotWithSol", + "docs": [ + "Swaps between an SPL spot collateral asset and native SOL collateral through an external venue: Sell debits spot and credits lamports received on the signer as native SOL collateral; Buy debits native SOL via the privileged withdraw self-CPI and credits the spot received on the signer's spot token account, moved into the spot custody ATA." + ], + "discriminator": [ + 236, + 150, + 197, + 249, + 54, + 231, + 86, + 101 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "orderSequenceNumber", - "type": "u64" } - ] - } - }, - { - "name": "TraderCapabilityFlags", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bits", - "type": "u32" + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; must be active and have native SOL collateral enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "MarketStatus", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Uninitialized" - }, - { - "name": "Active" - }, - { - "name": "PostOnly" - }, - { - "name": "Paused" - }, - { - "name": "Closed" - }, - { - "name": "Tombstoned" + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map holding spot asset registrations, prices, and global tallies." + ] + }, + { + "name": "spotMint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for the spot-leg token transfers." + ] + }, + { + "name": "systemProgram", + "docs": [ + "System program, required for the native SOL lamport movements." + ] + }, + { + "name": "nativeSolAuthority", + "docs": [ + "Native SOL authority PDA, required by the Buy direction's withdraw self-CPI." + ] + }, + { + "name": "traderPhoenixWallet", + "docs": [ + "Trader's wallet PDA; owner of the spot custody ATA and CPI authority for the Sell custody debit." + ] + }, + { + "name": "signer", + "writable": true, + "signer": true, + "docs": [ + "The trader wallet or the trader's position authority; writable because lamports flow through it in both directions." + ] + }, + { + "name": "signerSpotTokenAccount", + "writable": true, + "docs": [ + "Signer-owned token account for the spot mint; receives the Sell custody debit for the venue to spend, and the Buy output the venue pays in." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA; holds the native SOL lamports and the spot collateral ledger entries." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue PDA; the swap's collateral-value decrease is consumed from the contract-wide withdrawal throttle." + ] + }, + { + "name": "spotTraderPhoenixWalletAta", + "writable": true, + "docs": [ + "Wallet PDA's custody associated token account for the spot mint; debited on Sell, receives the swept output on Buy." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account. Remaining accounts follow: caller-supplied venue accounts referenced by the packed instructions." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SwapSpotWithSolInstruction" + } } - ] - } + } + ] }, { - "name": "CommodityMarketState", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Active" - }, - { - "name": "AfterHours" - }, - { - "name": "Reopen" + "name": "WithdrawSpot", + "docs": [ + "Withdraws SPL spot collateral (and/or uncounted custody excess) from the trader wallet's custody ATA to a destination token account of the same mint; accounted debits are margin-validated and consume the contract-wide withdrawal throttle, never enqueued." + ], + "discriminator": [ + 228, + 41, + 126, + 4, + 112, + 18, + 221, + 112 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "AuthorityType", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Root" - }, - { - "name": "Risk" - }, - { - "name": "Market" - }, - { - "name": "Oracle" - }, - { - "name": "Cancel" - }, - { - "name": "Backstop" - }, - { - "name": "ADL" + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } - }, - { - "name": "ValidationRule", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Ignore" - }, - { - "name": "Require" - }, - { - "name": "Forbid" + }, + { + "name": "signerWallet", + "signer": true, + "docs": [ + "Trader authority signing the withdrawal." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account whose recorded spot collateral is debited." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map for margin checks, oracle valuation, and the global spot collateral tally." + ] + }, + { + "name": "mint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "traderPhoenixWallet", + "docs": [ + "Trader's wallet PDA; owner of the custody ATA and CPI authority for the token movement." + ] + }, + { + "name": "custodyTokenAccount", + "writable": true, + "docs": [ + "Custody ATA (owned by traderWallet, for the asset's mint) debited by the withdrawal." + ] + }, + { + "name": "destinationTokenAccount", + "writable": true, + "docs": [ + "Destination token account of the same mint; must differ from the custody ATA." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for the custody token transfer." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue PDA whose throttle budget is consumed by accounted withdrawals." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawSpotParams" + } } - ] - } + } + ] }, { - "name": "LeverageTier", - "type": { - "kind": "struct", - "fields": [ - { - "name": "upperBoundSize", - "type": { - "defined": { - "name": "BaseLots" + "name": "LiquidateSpot", + "docs": [ + "Seizes a liquidatee's SPL spot collateral at the discounted oracle price, executing packed venue instructions to convert it and depositing the quote proceeds to the liquidatee, bypassing the withdrawal throttle and queue." + ], + "discriminator": [ + 107, + 0, + 128, + 41, + 35, + 229, + 251, + 18 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "maxLeverage", - "type": { - "defined": { - "name": "Constant" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; must be active and gates the spot-collateral feature and authority checks." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "limitOrderRiskFactor", - "type": { - "defined": { - "name": "BasisPoints" - } + } + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map holding the spot asset registry entry, oracle valuation data, and global spot collateral tallies." + ] + }, + { + "name": "spotMint", + "docs": [ + "SPL mint account selecting the spot collateral asset." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Global quote vault token account that receives the liquidator's quote deposit." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program used for the seizure debit and quote deposit transfers." + ] + }, + { + "name": "traderPhoenixWallet", + "docs": [ + "Liquidatee's wallet PDA; owner of the custody ATA and CPI authority for the seizure debit." + ] + }, + { + "name": "signer", + "writable": true, + "signer": true, + "docs": [ + "Liquidator; must be the risk authority or hold a delegated liquidation permission, and owns both venue-facing token accounts." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission PDA for the delegated-liquidation path; pass the signer's own key to select the direct risk-authority path." + ] + }, + { + "name": "signerQuoteTokenAccount", + "writable": true, + "docs": [ + "Liquidator's quote token account; its balance delta after the venue instructions is the deposit credited to the liquidatee." + ] + }, + { + "name": "signerSpotTokenAccount", + "writable": true, + "docs": [ + "Liquidator's token account for the seized asset's mint; receives the seized tokens so the venue instructions can spend them." + ] + }, + { + "name": "liquidateeAccount", + "writable": true, + "docs": [ + "Trader account being liquidated; its recorded spot balance is debited and its quote collateral credited." + ] + }, + { + "name": "traderPhoenixWalletAta", + "writable": true, + "docs": [ + "Custody ATA of the seized asset, owned by the liquidatee's wallet PDA." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account. Remaining accounts follow: shortfall-evidence trader accounts (numTradersToCheck of them, writable), then caller-supplied venue accounts referenced by the packed instructions." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidateSpotParams" } } - ] - } + } + ] }, { - "name": "LeverageTiers", - "type": { - "kind": "struct", - "fields": [ - { - "name": "tiers", - "type": { - "array": [ - { - "defined": { - "name": "LeverageTier" - } - }, - 4 + "name": "SetSpotAssetPrice", + "docs": [ + "Records the median spot-collateral price pushed by the Manticore oracle program;", + "only the asset's configured per-symbol proxy authority PDA may sign." + ], + "discriminator": [ + 246, + 172, + 230, + 25, + 190, + 58, + 58, + 9 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } - }, - { - "name": "PositionSizeLimits", - "type": { - "kind": "struct", - "fields": [ - { - "name": "long", - "type": "u32" - }, - { - "name": "short", - "type": "u32" + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "proxyAuthority", + "signer": true, + "docs": [ + "Per-symbol Manticore proxy authority PDA; must match the asset's configured proxy authority." + ] + }, + { + "name": "spotAssetMap", + "writable": true, + "docs": [ + "Perp asset map account holding the spot collateral collections." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetSpotAssetPriceInstruction" + } + } + } + ] }, { - "name": "PositionSizeLimit", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Disabled" - }, - { - "name": "Limit", - "fields": [ + "name": "ChangeSpotAssetParams", + "docs": [ + "Updates selected fields in a spot asset's oracle-price configuration: proxy", + "authority, expected price exponent, and valuation stale threshold." + ], + "discriminator": [ + 41, + 110, + 206, + 72, + 187, + 197, + 197, + 112 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "value", - "type": { - "defined": { - "name": "PositionSizeLimits" - } - } + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } - ] + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority; delegated signers are rejected." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account slot; unused placeholder because delegation is not permitted." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account holding the spot collateral collections." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ChangeSpotAssetParamsInstruction" + } + } + } + ] }, { - "name": "TickRegion", - "type": { - "kind": "struct", - "fields": [ - { - "name": "startOffset", - "type": { - "defined": { - "name": "Ticks" + "name": "SyncNative", + "docs": [ + "Reconciles native SOL collateral with the trader account lamports, excluding rent." + ], + "discriminator": [ + 155, + 219, + 36, + 36, + 239, + 128, + 21, + 65 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "endOffset", - "type": { - "defined": { - "name": "Ticks" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } }, - { - "name": "density", - "type": { - "defined": { - "name": "BaseLotsPerTickU32" + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account whose state is updated." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header, followed by any arena accounts." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header, followed by any arena accounts." + ] + } + ], + "args": [] + }, + { + "name": "WithdrawNativeSol", + "docs": [ + "Withdraws native SOL collateral or uncounted excess lamports." + ], + "discriminator": [ + 201, + 104, + 187, + 105, + 80, + 204, + 84, + 138 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "topLevelHiddenTakeSize", - "type": { - "defined": { - "name": "BaseLotsPerTickU32" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } }, - { - "name": "totalSize", - "type": { - "defined": { - "name": "BaseLots" - } + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "signerWallet", + "signer": true, + "docs": [ + "Wallet signing the native SOL withdrawal." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account whose state is updated." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perpetual and spot asset metadata account." + ] + }, + { + "name": "destination", + "writable": true, + "docs": [ + "Recipient of withdrawn native SOL lamports." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdrawal queue and throttle account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header, followed by any arena accounts." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header, followed by any arena accounts." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawNativeSolAction" } - }, - { - "name": "filledSize", - "type": { - "defined": { - "name": "BaseLotsU32" + } + } + ] + }, + { + "name": "TransferNativeSol", + "docs": [ + "Transfers native SOL collateral between trader accounts." + ], + "discriminator": [ + 204, + 65, + 217, + 206, + 56, + 180, + 74, + 252 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "hiddenFilledSize", - "type": { - "defined": { - "name": "BaseLotsU32" + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "lifespan", - "type": "u64" } - ] - } - }, - { - "name": "OrderRejectionReason", - "type": { - "kind": "enum", - "variants": [ - { - "name": "TooManyLimitOrders" - }, - { - "name": "PostOnlyCross" - }, - { - "name": "InvalidOrderPacket" - }, - { - "name": "TiFInvalid" - }, - { - "name": "OutsideExecutionPriceBand" + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader wallet owning both accounts." + ] + }, + { + "name": "srcTraderAccount", + "writable": true, + "docs": [ + "Source trader account." + ] + }, + { + "name": "dstTraderAccount", + "writable": true, + "docs": [ + "Destination trader account." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for margin calculations." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "TransferNativeSolInstruction" + } } - ] - } + } + ] }, { - "name": "OrderResidualDiscardReason", - "type": { - "kind": "enum", - "variants": [ - { - "name": "OutsideExecutionPriceBand" + "name": "TransferNativeSolFromChildToParent", + "docs": [ + "Sweeps native SOL collateral from a flat child trader to its parent." + ], + "discriminator": [ + 90, + 175, + 49, + 106, + 54, + 115, + 8, + 185 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet owning both accounts." + ] + }, + { + "name": "childTraderAccount", + "writable": true, + "docs": [ + "Child (isolated margin) trader account." + ] + }, + { + "name": "parentTraderAccount", + "writable": true, + "docs": [ + "Parent (cross margin) trader account." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perp asset map for margin calculations." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + } + ], + "args": [] }, { - "name": "OrderModificationReason", - "type": { - "kind": "enum", - "variants": [ - { - "name": "UserRequested" - }, - { - "name": "AuthorityForced" + "name": "LiquidateNativeSol", + "docs": [ + "Liquidates native SOL collateral. Remaining accounts contain the additional traders followed by external venue accounts." + ], + "discriminator": [ + 173, + 44, + 209, + 10, + 104, + 250, + 64, + 50 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } }, - { - "name": "SelfTradeCancelProvide" + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perpetual and spot asset metadata account." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Canonical quote-token vault." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program." + ] + }, + { + "name": "nativeSolAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 110, + 97, + 116, + 105, + 118, + 101, + 95, + 115, + 111, + 108 + ] + } + ] }, - { - "name": "SelfTradeDecrementTake" - }, - { - "name": "Expired" - }, - { - "name": "ReduceOnlyInvalidated" - }, - { - "name": "RiskCapacityExceeded" - }, - { - "name": "BookCapacityEvicted" - }, - { - "name": "Tombstone" - }, - { - "name": "TopOfBookOutOfBounds" - }, - { - "name": "StopLossCascade" - } - ] - } - }, - { - "name": "ConditionalOrderPingStateSnapshot", - "type": { - "kind": "struct", - "fields": [ - { - "name": "value", - "type": "u8" + "docs": [ + "Native SOL authority PDA, derived from the native_sol seed." + ] + }, + { + "name": "signer", + "writable": true, + "signer": true, + "docs": [ + "Wallet signing the swap or liquidation." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + }, + { + "name": "signerQuoteTokenAccount", + "writable": true, + "docs": [ + "Signer token account used for the quote leg." + ] + }, + { + "name": "liquidateeAccount", + "writable": true, + "docs": [ + "Trader account whose SOL collateral is liquidated." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdrawal queue and throttle account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header, followed by any arena accounts." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header, followed by any arena accounts." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidateNativeSolParams" + } } - ] - } + } + ] }, { - "name": "ConditionalOrderPingSnapshot", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": { - "defined": { - "name": "ConditionalOrderPingStateSnapshot" + "name": "SwapNative", + "docs": [ + "Swaps native SOL and quote collateral through packed external instructions; append venue accounts after the trader buffers." + ], + "discriminator": [ + 88, + 147, + 177, + 186, + 129, + 234, + 239, + 231 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "attachedOrderId", - "type": { - "defined": { - "name": "OptionalFIFOOrderId" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } }, - { - "name": "maxSize", - "type": { - "defined": { - "name": "BaseLots" + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perpetual and spot asset metadata account." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Canonical quote-token vault." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program." + ] + }, + { + "name": "systemProgram", + "address": "11111111111111111111111111111111", + "docs": [ + "Solana System program." + ] + }, + { + "name": "nativeSolAuthority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 110, + 97, + 116, + 105, + 118, + 101, + 95, + 115, + 111, + 108 + ] } - } + ] }, - { - "name": "fillableSize", - "type": { - "defined": { - "name": "BaseLots" + "docs": [ + "Native SOL authority PDA, derived from the native_sol seed." + ] + }, + { + "name": "signer", + "writable": true, + "signer": true, + "docs": [ + "Wallet signing the swap or liquidation." + ] + }, + { + "name": "signerQuoteTokenAccount", + "writable": true, + "docs": [ + "Signer token account used for the quote leg." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account whose state is updated." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdrawal queue and throttle account." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header, followed by any arena accounts." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header, followed by any arena accounts." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SwapNativeInstruction" + } + } + } + ] + }, + { + "name": "ConfigureNativeSol", + "docs": [ + "Updates native SOL collateral configuration; absent fields remain unchanged." + ], + "discriminator": [ + 241, + 19, + 44, + 37, + 119, + 51, + 197, + 27 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "filledSize", - "type": { - "defined": { - "name": "BaseLots" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } }, - { - "name": "coPositionSequenceNumber", - "type": { - "option": "u8" + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + }, + { + "name": "perpAssetMap", + "docs": [ + "Perpetual and spot asset metadata account." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ConfigureNativeSolParams" } } - ] - } + } + ] }, { - "name": "MarkPriceConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "emaPeriodSlots", - "type": "u64" - }, - { - "name": "emaDiffRadius", - "type": "u64" - }, - { - "name": "bookPriceRadius", - "type": "u64" - }, - { - "name": "commoditiesAfterHoursRadius", - "type": "u64" - }, - { - "name": "spotPriceWeight", - "type": "u64" - }, - { - "name": "bookPriceWeight", - "type": "u64" - }, - { - "name": "perpPriceWeight", - "type": "u64" - }, - { - "name": "spotPriceStaleThreshold", - "type": "u64" - }, - { - "name": "bookPriceStaleThreshold", - "type": "u64" - }, - { - "name": "perpPriceStaleThreshold", - "type": "u64" - }, - { - "name": "riskActionPriceValidityRules", - "type": { - "array": [ - { - "array": [ - { - "array": [ - { - "defined": { - "name": "ValidationRule" - } - }, - 8 - ] - }, - 4 - ] - }, - 8 + "name": "ActivateNativeSol", + "docs": [ + "Activates configured native SOL collateral." + ], + "discriminator": [ + 15, + 246, + 91, + 93, + 70, + 233, + 191, + 120 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "oracleDivergenceRadius", - "type": "u16" - }, - { - "name": "minOracleResponses", - "type": "u8" - }, - { - "name": "commoditiesAfterHoursRadiusBps", - "type": "u64" } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + }, + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + } + ], + "args": [] }, { - "name": "WithdrawConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "depositCooldownPeriodInSlots", - "type": "u64" - }, - { - "name": "withdrawalFee", - "type": "u64" - }, - { - "name": "enqueueingFee", - "type": "u64" + "name": "Rebirth", + "docs": [ + "Initializes the exchange configuration, vault, trader buffers, asset map, and withdrawal queue." + ], + "discriminator": [ + 143, + 241, + 154, + 79, + 116, + 226, + 255, + 144 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "payer", + "writable": true, + "signer": true, + "docs": [ + "Signer paying account creation rent." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Canonical quote-token vault." + ] + }, + { + "name": "canonicalTokenMint", + "docs": [ + "Canonical quote-token mint." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index header, followed by any arena accounts." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer header, followed by any arena accounts." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perpetual and spot asset metadata account." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdrawal queue and throttle account." + ] + }, + { + "name": "systemProgram", + "docs": [ + "Solana System program." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "RebirthParams" + } + } + } + ] }, { - "name": "WithdrawRateLimitConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "maxBudget", - "type": "u64" - }, - { - "name": "replenishAmountPerSlot", - "type": "u64" + "name": "AddMarket", + "docs": [ + "Adds a perpetual market and its orderbook and spline accounts." + ], + "discriminator": [ + 41, + 137, + 185, + 126, + 69, + 139, + 254, + 55 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perpetual and spot asset metadata account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Market spline collection PDA." + ] + }, + { + "name": "systemProgram", + "docs": [ + "Solana System program." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AddMarketParams" + } + } + } + ] }, { - "name": "FundingConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "fundingIntervalSeconds", - "type": "u64" - }, - { - "name": "fundingPeriodSeconds", - "type": "u64" - }, - { - "name": "maxFundingRate", - "type": "i64" + "name": "ClaimAuthority", + "docs": [ + "Accepts a pending authority role." + ], + "discriminator": [ + 222, + 132, + 185, + 123, + 127, + 107, + 6, + 31 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "newAuthority", + "signer": true, + "docs": [ + "Pending authority accepting the role." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ClaimAuthorityInstruction" + } + } + } + ] }, { - "name": "CommodityMarketStateConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "marketState", - "type": { - "defined": { - "name": "CommodityMarketState" + "name": "NameSuccessor", + "docs": [ + "Nominates a successor for an authority role." + ], + "discriminator": [ + 160, + 203, + 37, + 229, + 225, + 119, + 129, + 236 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "isCommodity", - "type": "bool" - }, - { - "name": "lastKnownIndexPrice", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "NameSuccessorInstruction" } - }, - { - "name": "lastIndexExpiryTimestamp", - "type": "u64" - }, - { - "name": "commoditiesAfterHoursRadius", - "type": { - "defined": { - "name": "Ticks" + } + } + ] + }, + { + "name": "ClaimFees", + "docs": [ + "Withdraws collected quote fees." + ], + "discriminator": [ + 82, + 251, + 233, + 156, + 12, + 52, + 184, + 202 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Canonical quote-token vault." + ] + }, + { + "name": "destinationTokenAccount", + "writable": true, + "docs": [ + "Token account receiving the claimed fees." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ClaimFeesInstruction" + } + } + } + ] }, { - "name": "MarketFeeConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "defaultTakerFeeMicro", - "type": "u32" - }, - { - "name": "defaultMakerFeeMicro", - "type": "i32" + "name": "CloseTraderAccount", + "docs": [ + "Closes an empty, inactive trader account and returns its rent." + ], + "discriminator": [ + 126, + 222, + 38, + 149, + 207, + 164, + 108, + 187 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + }, + { + "name": "traderWallet", + "docs": [ + "Trader wallet authority." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader account whose state is updated." + ] + }, + { + "name": "recipient", + "writable": true, + "docs": [ + "Account receiving the closed trader account lamports." + ] + }, + { + "name": "systemProgram", + "docs": [ + "Solana System program." + ] + } + ], + "args": [] }, { - "name": "FeatureSet", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bits", - "type": "u32" + "name": "EnableFeature", + "docs": [ + "Enables an exchange feature." + ], + "discriminator": [ + 27, + 23, + 206, + 255, + 190, + 19, + 231, + 92 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } } - ] - } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Exchange global configuration PDA." + ] + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Authority signing this instruction." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Writable permission account or direct-authority placeholder." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "EnableFeatureParams" + } + } + } + ] }, { - "name": "SpotCollateralMetadata", - "type": { - "kind": "struct", - "fields": [ - { - "name": "mintAddress", - "type": "pubkey" - }, - { - "name": "decimals", - "type": "u32" - }, - { - "name": "perpAssetIndex", - "type": "u32" - }, - { - "name": "maxPerTraderBalance", - "type": "u64" - }, - { - "name": "maxGlobalBalance", - "type": "u64" - }, - { - "name": "currGlobalBalance", - "type": "u64" - }, - { - "name": "minMarginDiscount", - "type": "u32" - }, - { - "name": "maxMarginDiscount", - "type": "u32" - }, - { - "name": "maxLiquidationDiscount", - "type": "u32" - }, - { - "name": "minLiquidationSlippage", - "type": "u32" - }, - { - "name": "maxLiquidationSize", - "type": "u64" - }, - { - "name": "postLiquidationBuffer", - "type": { - "defined": { - "name": "QuoteLots" + "name": "PlaceMarketOrderDelegated", + "optionalTrailingAdditionalFeeConfig": true, + "docs": [ + "Places an IOC order using the owner, primary position authority, or a delegated position authority. Accepts an optional Borsh AdditionalFeeConfig after the legacy payload; any separate conditional_taker rate is ignored." + ], + "discriminator": [ + 160, + 35, + 21, + 235, + 110, + 182, + 245, + 65 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "quoteLotCollateralShortfallBuffer", - "type": { - "defined": { - "name": "QuoteLots" + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA; enforces the market is registered and cross trading is enabled." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] } - }, - { - "name": "flags", - "type": "u8" - }, - { - "name": "paddingFlags", - "type": { - "array": [ - "u8", - 7 + } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Trader authority placing the market order." + ] + }, + { + "name": "positionAuthorityPermission", + "writable": true, + "docs": [ + "Trader-issued position-authority permission, or a non-permission placeholder for owner/primary authority." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA (\"trader\" account) holding balances and open positions." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map governing risk limits and funding parameters for the market." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "orderbook", + "writable": true, + "docs": [ + "Market orderbook account that stores resting liquidity." + ] + }, + { + "name": "splines", + "writable": true, + "docs": [ + "Spline collection PDA that tracks market-making programs for this market." + ] + } + ], + "args": [ + { + "name": "orderPacket", + "type": { + "defined": { + "name": "OrderPacket" + } + } + } + ] + }, + { + "name": "WithdrawFundsWithFee", + "docs": [ + "Withdraws quote collateral with the configured withdrawal fee." + ], + "discriminator": [ + 202, + 94, + 94, + 42, + 155, + 227, + 88, + 97 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } - }, - { - "name": "padding", - "type": { - "array": [ - "u64", - 26 + } + }, + { + "name": "globalConfiguration", + "writable": true, + "docs": [ + "Global configuration PDA that tracks vault and system parameters." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 ] } } - ] - } + }, + { + "name": "traderWallet", + "signer": true, + "docs": [ + "Authority of the trader account initiating the withdrawal." + ] + }, + { + "name": "traderAccount", + "writable": true, + "docs": [ + "Trader state PDA from which collateral is debited." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map for margin and risk checks." + ] + }, + { + "name": "globalVault", + "writable": true, + "docs": [ + "Global vault token account from which tokens are withdrawn." + ] + }, + { + "name": "destinationTokenAccount", + "writable": true, + "docs": [ + "Trader's destination token account to receive the withdrawn tokens." + ] + }, + { + "name": "tokenProgram", + "docs": [ + "SPL Token program for executing the transfer." + ] + }, + { + "name": "globalTraderIndex", + "writable": true, + "docs": [ + "Global trader index account." + ] + }, + { + "name": "activeTraderBuffer", + "writable": true, + "docs": [ + "Active trader buffer account." + ] + }, + { + "name": "withdrawQueue", + "writable": true, + "docs": [ + "Withdraw queue PDA tracking pending withdrawal requests." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawFundsInstruction" + } + } + } + ] }, { - "name": "AdminParameterUpdateKind", - "type": { - "kind": "enum", - "variants": [ - { - "name": "CancelRiskFactor", - "kind": "struct", - "fields": [ + "name": "UpdatePerpDisplayName", + "docs": [ + "Sets or clears the display name of a perp asset." + ], + "discriminator": [ + 205, + 2, + 78, + 167, + 86, + 227, + 2, + 66 + ], + "accounts": [ + { + "name": "phoenixProgram", + "docs": [ + "Phoenix Eternal program id; logged to assert the correct executable is invoked." + ] + }, + { + "name": "phoenixLogAuthority", + "docs": [ + "Phoenix log authority PDA used for emitting on-chain market events." + ], + "pda": { + "seeds": [ { - "name": "previous", - "type": "u16" - }, + "kind": "const", + "value": [ + 108, + 111, + 103 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "globalConfiguration", + "docs": [ + "Global configuration PDA used to validate exchange-wide account relationships." + ], + "pda": { + "seeds": [ { - "name": "new", - "type": "u16" + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 + ] } + ], + "program": { + "kind": "const", + "value": [ + 206, + 112, + 214, + 251, + 86, + 142, + 67, + 235, + 252, + 150, + 74, + 144, + 21, + 108, + 59, + 233, + 86, + 59, + 8, + 23, + 6, + 124, + 171, + 105, + 78, + 188, + 131, + 239, + 221, + 179, + 126, + 134 + ] + } + } + }, + { + "name": "authority", + "signer": true, + "docs": [ + "Risk authority." + ] + }, + { + "name": "maybePermissionAccount", + "writable": true, + "docs": [ + "Permission account used for delegated authority validation." + ] + }, + { + "name": "perpAssetMap", + "writable": true, + "docs": [ + "Perp asset map account whose metadata is updated by this instruction." + ] + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdatePerpDisplayNameInstruction" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "GlobalConfiguration", + "discriminator": [ + 37, + 146, + 212, + 210, + 47, + 136, + 111, + 20 + ], + "docs": [ + "Global exchange configuration account derived from the \"global\" PDA seed.", + "Stores configured authorities, canonical collateral mint, global vault, PerpAssetMap, GlobalTraderIndex, ActiveTraderBuffer, and WithdrawQueue addresses." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108 ] - }, + } + ] + } + }, + { + "name": "OrderbookHeader", + "discriminator": [ + 65, + 1, + 40, + 241, + 166, + 14, + 86, + 188 + ], + "docs": [ + "Market orderbook account header for a perpetual market.", + "Market orderbooks are explicit market accounts created during AddMarket and recorded in PerpAssetMap metadata; they are not PDAs." + ] + }, + { + "name": "SplineCollectionHeader", + "discriminator": [ + 192, + 245, + 182, + 191, + 88, + 37, + 79, + 48 + ], + "docs": [ + "Spline collection account header derived from [\"spline\", orderbook].", + "Stores spline market-making state for one market." + ], + "pda": { + "seeds": [ { - "name": "IsolatedOnly", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": "bool" - }, - { - "name": "new", - "type": "bool" - } + "kind": "const", + "value": [ + 115, + 112, + 108, + 105, + 110, + 101 ] }, { - "name": "LeverageTiers", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "LeverageTiers" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "LeverageTiers" - } - } - } + "kind": "account", + "path": "marketAccount", + "account": "OrderbookHeader" + } + ] + } + }, + { + "name": "Trader", + "discriminator": [ + 41, + 97, + 73, + 105, + 110, + 214, + 112, + 9 + ], + "docs": [ + "Trader account derived from [\"trader\", authority, trader_pda_index, trader_subaccount_index].", + "Stores collateral, positions, open orders, and per-trader risk state." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 116, + 114, + 97, + 100, + 101, + 114 ] }, { - "name": "MarkPriceParameters", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "MarkPriceConfig" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "MarkPriceConfig" - } - } - } - ] + "kind": "account", + "path": "authority" }, { - "name": "OpenInterestCap", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": "u64" - }, - { - "name": "new", - "type": "u64" - } - ] + "kind": "arg", + "path": "traderPdaIndex" }, { - "name": "UpnlRiskFactor", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": "u16" - }, - { - "name": "new", - "type": "u16" - } - ] - }, + "kind": "arg", + "path": "traderSubaccountIndex" + } + ] + } + }, + { + "name": "ActiveTraderBufferHeader", + "discriminator": [ + 192, + 255, + 205, + 165, + 80, + 154, + 131, + 5 + ], + "docs": [ + "Header arena for the active trader buffer derived from [\"active_trader_buffer\", 0u16]." + ], + "pda": { + "seeds": [ { - "name": "UpnlRiskFactorForWithdrawals", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": "u16" - }, - { - "name": "new", - "type": "u16" - } + "kind": "const", + "value": [ + 97, + 99, + 116, + 105, + 118, + 101, + 95, + 116, + 114, + 97, + 100, + 101, + 114, + 95, + 98, + 117, + 102, + 102, + 101, + 114 ] }, { - "name": "WithdrawParameters", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "WithdrawConfig" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "WithdrawConfig" - } - } - } + "kind": "const", + "value": [ + 0, + 0 ] - }, + } + ] + } + }, + { + "name": "ActiveTraderBufferArenaHeader", + "discriminator": [ + 6, + 249, + 57, + 137, + 82, + 121, + 191, + 107 + ], + "docs": [ + "Additional active trader buffer arena derived from [\"active_trader_buffer\", arena_index]." + ], + "pda": { + "seeds": [ { - "name": "WithdrawRateLimits", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "WithdrawRateLimitConfig" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "WithdrawRateLimitConfig" - } - } - } + "kind": "const", + "value": [ + 97, + 99, + 116, + 105, + 118, + 101, + 95, + 116, + 114, + 97, + 100, + 101, + 114, + 95, + 98, + 117, + 102, + 102, + 101, + 114 ] }, { - "name": "TraderCapability", - "kind": "struct", - "fields": [ - { - "name": "trader", - "type": "pubkey" - }, - { - "name": "previousFlags", - "type": { - "defined": { - "name": "TraderCapabilityFlags" - } - } - }, - { - "name": "newFlags", - "type": { - "defined": { - "name": "TraderCapabilityFlags" - } - } - } - ] - }, + "kind": "arg", + "path": "arenaIndex" + } + ] + } + }, + { + "name": "GlobalTraderIndexHeader", + "discriminator": [ + 145, + 92, + 169, + 6, + 5, + 144, + 1, + 205 + ], + "docs": [ + "Header arena for the global trader index derived from [\"global_trader_index\", 0u16]." + ], + "pda": { + "seeds": [ { - "name": "FundingParameters", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "FundingConfig" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "FundingConfig" - } - } - } + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 116, + 114, + 97, + 100, + 101, + 114, + 95, + 105, + 110, + 100, + 101, + 120 ] }, { - "name": "MarketFees", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "MarketFeeConfig" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "MarketFeeConfig" - } - } - } + "kind": "const", + "value": [ + 0, + 0 ] - }, + } + ] + } + }, + { + "name": "GlobalTraderIndexArenaHeader", + "discriminator": [ + 98, + 110, + 179, + 71, + 203, + 120, + 200, + 235 + ], + "docs": [ + "Additional global trader index arena derived from [\"global_trader_index\", arena_index]." + ], + "pda": { + "seeds": [ { - "name": "OpenInterestAdjustment", - "kind": "struct", - "fields": [ - { - "name": "previousOpenInterest", - "type": "u64" - }, - { - "name": "newOpenInterest", - "type": "u64" - } + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 116, + 114, + 97, + 100, + 101, + 114, + 95, + 105, + 110, + 100, + 101, + 120 ] }, { - "name": "CommodityMarketState", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "CommodityMarketStateConfig" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "CommodityMarketStateConfig" - } - } - } + "kind": "arg", + "path": "arenaIndex" + } + ] + } + }, + { + "name": "PerpAssetMap", + "discriminator": [ + 142, + 54, + 115, + 223, + 124, + 239, + 108, + 38 + ], + "docs": [ + "Perpetual asset map account storing market metadata, risk parameters, oracle metadata, and funding state.", + "The account pubkey is stored in GlobalConfiguration rather than derived from a fixed PDA seed; it is not a PDA." + ] + }, + { + "name": "WithdrawQueueHeader", + "discriminator": [ + 125, + 59, + 94, + 108, + 171, + 16, + 43, + 228 + ], + "docs": [ + "Withdraw queue account storing pending withdrawals and withdrawal throttle state.", + "The account pubkey is stored in GlobalConfiguration rather than derived from a fixed PDA seed; it is not a PDA." + ] + }, + { + "name": "PermissionAccount", + "discriminator": [ + 180, + 172, + 109, + 215, + 129, + 165, + 97, + 38 + ], + "docs": [ + "Permission delegation account derived from [\"permission\", authority, user].", + "Stores delegated capability bits and optional expiry/usage constraints." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 101, + 114, + 109, + 105, + 115, + 115, + 105, + 111, + 110 ] }, { - "name": "MaxLiquidationSize", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": "u64" - }, - { - "name": "new", - "type": "u64" - } - ] + "kind": "account", + "path": "authority" }, { - "name": "FeatureSet", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "defined": { - "name": "FeatureSet" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "FeatureSet" - } - } - } + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "StopLosses", + "discriminator": [ + 127, + 64, + 139, + 143, + 70, + 43, + 167, + 152 + ], + "docs": [ + "Stop-loss account derived from [\"stoploss\", trader_account, asset_id].", + "Stores trigger orders for one trader and market." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 116, + 111, + 112, + 108, + 111, + 115, + 115 ] }, { - "name": "SpotCollateralConfig", - "kind": "struct", - "fields": [ - { - "name": "assetIndex", - "type": "u32" - }, - { - "name": "previous", - "type": { - "defined": { - "name": "SpotCollateralMetadata" - } - } - }, - { - "name": "new", - "type": { - "defined": { - "name": "SpotCollateralMetadata" - } - } - } - ] + "kind": "account", + "path": "traderAccount", + "account": "Trader" }, { - "name": "RiskFactors", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "array": [ - { - "defined": { - "name": "BasisPointsU16" - } - }, - 3 - ] - } - }, - { - "name": "new", - "type": { - "array": [ - { - "defined": { - "name": "BasisPointsU16" - } - }, - 3 - ] - } - } + "kind": "arg", + "path": "assetId" + } + ] + } + }, + { + "name": "EscrowHeader", + "discriminator": [ + 50, + 4, + 48, + 59, + 209, + 138, + 251, + 204 + ], + "docs": [ + "Escrow inbox account derived from [\"escrow\", authority].", + "Stores pending escrow requests for the authority." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 115, + 99, + 114, + 111, + 119 ] }, { - "name": "TraderCapOverride", - "kind": "struct", - "fields": [ - { - "name": "trader", - "type": "pubkey" - }, - { - "name": "assetIndex", - "type": "u32" - }, - { - "name": "previous", - "type": "u64" - }, - { - "name": "new", - "type": "u64" - } + "kind": "account", + "path": "authority" + } + ] + } + }, + { + "name": "ConditionalOrderHeader", + "discriminator": [ + 147, + 116, + 229, + 177, + 249, + 200, + 146, + 49 + ], + "docs": [ + "Conditional orders account derived from [\"conditional_orders\", trader_account].", + "Stores attached and position-scoped conditional orders for a trader account." + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, + 111, + 110, + 100, + 105, + 116, + 105, + 111, + 110, + 97, + 108, + 95, + 111, + 114, + 100, + 101, + 114, + 115 ] }, { - "name": "TransferFeeTiers", - "kind": "struct", - "fields": [ - { - "name": "previous", - "type": { - "array": [ - { - "defined": { - "name": "TransferFeeTier" - } - }, - 4 - ] - } - }, - { - "name": "new", - "type": { - "array": [ - { - "defined": { - "name": "TransferFeeTier" - } - }, - 4 - ] - } + "kind": "account", + "path": "traderAccount", + "account": "Trader" + } + ] + } + } + ], + "types": [ + { + "name": "OrderPacket", + "docs": [ + "Serialized parameters for Phoenix order placement instructions." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "kind", + "type": { + "defined": { + "name": "OrderPacketKind" } - ] + } } ] } }, { - "name": "EscrowAction", + "name": "OrderPacketKind", + "docs": [ + "Supported order variants accepted by the Phoenix matching engine." + ], "type": { "kind": "enum", "variants": [ { - "name": "Noop", - "kind": "struct", + "name": "PostOnly", "fields": [ { - "name": "padding0", + "name": "side", "type": { - "array": [ - "u8", - 8 - ] + "defined": { + "name": "Side" + } } }, { - "name": "padding1", + "name": "priceInTicks", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "numBaseLots", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "clientOrderId", "type": { "array": [ "u8", - 128 + 16 ] } + }, + { + "name": "slide", + "type": "bool" + }, + { + "name": "lastValidSlot", + "type": { + "option": "u64" + } + }, + { + "name": "orderFlags", + "type": { + "defined": { + "name": "OrderFlags" + } + } + }, + { + "name": "cancelExisting", + "type": "bool" } ] }, { - "name": "Cash", - "kind": "struct", + "name": "Limit", "fields": [ { - "name": "amount", - "type": "u64" + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } }, { - "name": "padding0", + "name": "priceInTicks", "type": { - "array": [ - "u8", - 128 - ] + "defined": { + "name": "Ticks" + } } - } - ] - } - ] - } - }, - { - "name": "EscrowRequestCancelReason", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Expiration" - }, - { - "name": "CancelledBySender" + }, + { + "name": "numBaseLots", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": { + "name": "SelfTradeBehavior" + } + } + }, + { + "name": "matchLimit", + "type": { + "option": "u64" + } + }, + { + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } + }, + { + "name": "lastValidSlot", + "type": { + "option": "u64" + } + }, + { + "name": "orderFlags", + "type": { + "defined": { + "name": "OrderFlags" + } + } + }, + { + "name": "cancelExisting", + "type": "bool" + } + ] }, { - "name": "CancelledByReceiver" + "name": "ImmediateOrCancel", + "fields": [ + { + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "priceInTicks", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "numBaseLots", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "numQuoteLots", + "type": { + "option": { + "defined": { + "name": "QuoteLots" + } + } + } + }, + { + "name": "minBaseLotsToFill", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "minQuoteLotsToFill", + "type": { + "defined": { + "name": "QuoteLots" + } + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": { + "name": "SelfTradeBehavior" + } + } + }, + { + "name": "matchLimit", + "type": { + "option": "u64" + } + }, + { + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } + }, + { + "name": "lastValidSlot", + "type": { + "option": "u64" + } + }, + { + "name": "orderFlags", + "type": { + "defined": { + "name": "OrderFlags" + } + } + }, + { + "name": "cancelExisting", + "type": "bool" + } + ] } ] } }, { - "name": "SpotCollateralCap", + "name": "Side", + "docs": [ + "Trading direction for an order." + ], "type": { "kind": "enum", "variants": [ { - "name": "PerTrader" + "name": "Bid" }, { - "name": "Global" + "name": "Ask" } ] } }, { - "name": "SpotCollateralFlow", + "name": "SelfTradeBehavior", + "docs": [ + "Policy for handling matches against the submitting trader's resting orders." + ], "type": { "kind": "enum", "variants": [ { - "name": "Sync" - }, - { - "name": "Swap" + "name": "Abort" }, { - "name": "Transfer" + "name": "CancelProvide" }, { - "name": "Withdraw" + "name": "DecrementTake" } ] } }, { - "name": "SpotCollateralDepositedEvent", + "name": "BaseLots", + "docs": [ + "Quantity wrapper representing discrete base lots." + ], "type": { "kind": "struct", "fields": [ { - "name": "assetIndex", - "type": "u32" - }, - { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "requestedAmount", - "type": "u64" - }, - { - "name": "bindingCap", - "type": { - "option": { - "defined": { - "name": "SpotCollateralCap" - } - } - } - }, - { - "name": "flow", - "type": { - "defined": { - "name": "SpotCollateralFlow" - } - } - }, - { - "name": "newCollateralBalance", - "type": "u64" - }, - { - "name": "postGlobalCollateral", - "type": "u64" - }, - { - "name": "traderSequenceNumber", + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "QuoteLots", + "docs": [ + "Quantity wrapper representing discrete quote lots." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "prevSequenceNumberSlot", + "name": "inner", "type": "u64" } ] } }, { - "name": "SpotCollateralWithdrawnEvent", + "name": "Ticks", + "docs": [ + "Discrete price unit measured in quote lots per base lot." + ], "type": { "kind": "struct", "fields": [ { - "name": "assetIndex", - "type": "u32" - }, - { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "destination", - "type": "pubkey" - }, - { - "name": "amount", + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "BaseLotsPerTick", + "docs": [ + "Density of liquidity per tick for spline market making." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "excess", + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "OrderFlags", + "docs": [ + "Bitmask flags applied to an order: bit 7 reduce-only, bit 6 stop-loss, bit 5 conditional-order, bit 4 stop-loss-direction, bit 3 clear-on-position-flip." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "withdrawExcess", - "type": "bool" - }, + "name": "flags", + "type": "u8" + } + ] + } + }, + { + "name": "OrderIds", + "docs": [ + "Collection of cancel identifiers used to target specific resting orders." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "isSelfCpi", - "type": "bool" - }, + "name": "orderIds", + "type": { + "vec": { + "defined": { + "name": "CancelId" + } + } + } + } + ] + } + }, + { + "name": "CancelUpToInstruction", + "docs": [ + "Parameters controlling cancel-up-to behaviour." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "flow", + "name": "side", "type": { "defined": { - "name": "SpotCollateralFlow" + "name": "Side" } } }, { - "name": "newCollateralBalance", - "type": "u64" - }, - { - "name": "postGlobalCollateral", - "type": "u64" - }, - { - "name": "traderSequenceNumber", - "type": "u64" + "name": "numOrdersToCancel", + "type": { + "option": "u64" + } }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "tickLimit", + "type": { + "option": "u64" + } } ] } }, { - "name": "SpotCollateralLiquidatedEvent", + "name": "CancelId", + "docs": [ + "Identifies a resting order along with an optional hint into the orderbook." + ], "type": { "kind": "struct", "fields": [ { - "name": "liquidator", - "type": "pubkey" - }, - { - "name": "liquidatedTrader", - "type": "pubkey" - }, - { - "name": "assetIndex", - "type": "u32" - }, - { - "name": "liquidationSize", - "type": "u64" - }, - { - "name": "overCapExcess", - "type": "u64" - }, - { - "name": "quoteLotsDeposited", + "name": "nodePointer", "type": { "defined": { - "name": "QuoteLots" + "name": "NodePointer" } } }, { - "name": "oracleNotional", + "name": "orderId", "type": { "defined": { - "name": "QuoteLots" + "name": "FIFOOrderId" } } - }, + } + ] + } + }, + { + "name": "FIFOOrderId", + "docs": [ + "Stable identifier for a resting order composed of price ticks and sequence number." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "liquidationDiscount", + "name": "priceInTicks", "type": { "defined": { - "name": "BasisPoints" + "name": "Ticks" } } + }, + { + "name": "orderSequenceNumber", + "type": "u64" } ] } }, { - "name": "RestartAcknowledgedEvent", + "name": "NodePointer", + "docs": [ + "Pointer hint into the orderbook linked list; zero represents a null pointer." + ], "type": { "kind": "struct", "fields": [ { - "name": "previousRestartSlot", - "type": "u64" - }, + "name": "value", + "type": "u32" + } + ] + } + }, + { + "name": "Symbol", + "docs": [ + "Fixed-width (16 byte) asset symbol stored as UTF-8 with trailing zero padding." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "newRestartSlot", - "type": "u64" - }, + "name": "symbolBytes", + "type": { + "array": [ + "u8", + 16 + ] + } + } + ] + } + }, + { + "name": "DisplayName", + "docs": [ + "Fixed-width (16 byte) market display name stored as ASCII with trailing zero padding; all zeroes means unset." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "previousStatusBits", - "type": "u8" + "name": "bytes", + "type": { + "array": [ + "u8", + 16 + ] + } + } + ] + } + }, + { + "name": "Price", + "docs": [ + "Price representation composed of a mantissa (`value`) and decimal exponent (`expo`)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "value", + "type": "u64" }, { - "name": "newStatusBits", + "name": "expo", "type": "u8" } ] } }, { - "name": "MarketEvent", + "name": "OraclePriceUpdate", "docs": [ - "Borsh enum emitted by Eternal Log instructions. Variant discriminators are one-byte ordinals." + "Single oracle price update targeting a specific perpetual asset." ], "type": { - "kind": "enum", - "variants": [ + "kind": "struct", + "fields": [ { - "name": "SlotContext", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SlotContextEvent" - } - } + "name": "perpAssetId", + "type": { + "defined": { + "name": "Symbol" } - ] + } }, { - "name": "Header", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketEventHeader" - } + "name": "newExchangePerpPrice", + "type": { + "option": { + "defined": { + "name": "Price" } } - ] + } }, { - "name": "OrderPlaced", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "OrderPlacedEvent" - } - } + "name": "newExchangeSpotPrice", + "type": { + "defined": { + "name": "Price" } - ] - }, + } + } + ] + } + }, + { + "name": "UpdateOraclePricesWithOrderingInstruction", + "docs": [ + "Oracle instruction payload containing ordered price updates and timestamp reset policy." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "OrderFilled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "OrderFilledEvent" - } - } - } - ] + "name": "updateTimestamp", + "type": "u64" }, { - "name": "OrderRejected", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "OrderRejectedEvent" - } + "name": "updates", + "type": { + "vec": { + "defined": { + "name": "OraclePriceUpdate" } } - ] + } }, { - "name": "SplineFilled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplineFilledEvent" - } - } + "name": "flags", + "type": { + "defined": { + "name": "OraclePriceUpdateFlags" } - ] - }, + } + } + ] + } + }, + { + "name": "OraclePriceUpdateFlags", + "docs": [ + "Packed flags controlling timestamp reset, internal-price handling, and sync behavior." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "TradeSummary", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TradeSummaryEvent" - } - } - } - ] + "name": "flags", + "type": "u8" + } + ] + } + }, + { + "name": "UpdateSplinePriceParamsWithOrdering", + "docs": [ + "Parameters for updating a spline's mid-price with anti-reordering protection." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "newMidPrice", + "type": "u64" }, { - "name": "OrderModified", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "OrderModifiedEvent" - } - } - } - ] + "name": "userUpdateSlot", + "type": { + "option": "u64" + } }, { - "name": "MarketSummary", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketSummaryEvent" - } - } - } - ] + "name": "refreshRegions", + "type": "bool" }, { - "name": "TraderRegistered", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderRegisteredEvent" - } - } - } - ] + "name": "userSequenceNumber", + "type": "u64" }, { - "name": "TraderCollateralTransferred", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderCollateralTransferredEvent" - } - } - } - ] + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } }, { - "name": "TraderActivated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderActivatedEvent" - } - } - } - ] + "name": "overrideSequenceNumber", + "type": "bool" + } + ] + } + }, + { + "name": "DepositFundsInstruction", + "docs": [ + "Parameters for depositing collateral into the global vault." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "RegisterTraderParams", + "docs": [ + "Parameters for registering a new trader account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxPositions", + "type": "u32" }, { - "name": "TraderDeactivated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderDeactivatedEvent" - } - } - } - ] + "name": "traderPreferenceBits", + "type": "u32" }, { - "name": "TraderFundsDeposited", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFundsDepositedEvent" - } - } - } - ] + "name": "traderPdaIndex", + "type": "u8" }, { - "name": "TraderFundsWithdrawn", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFundsWithdrawnEvent" - } - } - } - ] + "name": "traderSubaccountIndex", + "type": "u8" + } + ] + } + }, + { + "name": "LiquidationParams", + "docs": [ + "Parameters for liquidating a trader's position via market order." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetMint", + "type": "pubkey" }, { - "name": "TraderFundsWithdrawnEnqueued", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFundsWithdrawnEvent" - } - } + "name": "liquidationSize", + "type": { + "defined": { + "name": "BaseLots" } - ] + } }, { - "name": "TraderFundsWithdrawnDropped", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFundsWithdrawnEvent" - } - } + "name": "liquidationPrice", + "type": { + "defined": { + "name": "Ticks" } - ] + } }, { - "name": "TraderWithdrawCancelled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderWithdrawCancelledEvent" - } - } - } - ] + "name": "fillOrKill", + "type": "bool" + } + ] + } + }, + { + "name": "TransferRequest", + "docs": [ + "Transfer payload describing how many base lots of an asset should move from the liquidatee to the liquidator." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetId", + "type": "u64" }, { - "name": "TraderFundingSettled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFundingSettledEvent" - } - } + "name": "amount", + "type": { + "defined": { + "name": "BaseLots" } - ] - }, + } + } + ] + } + }, + { + "name": "LiquidationTransferParams", + "docs": [ + "Parameters controlling liquidation transfers between traders." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "SplineRegistered", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplineRegisteredEvent" - } + "name": "transferRequests", + "type": { + "vec": { + "defined": { + "name": "TransferRequest" } } - ] + } }, { - "name": "SplineActivated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplineActivatedEvent" - } - } - } - ] + "name": "executeAllTransfers", + "type": "bool" + } + ] + } + }, + { + "name": "ClosePositionsParams", + "docs": [ + "Parameters for closing matched positions between at-loss and in-profit traders." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetId", + "type": "u64" }, { - "name": "SplineDeactivated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplineDeactivatedEvent" - } - } + "name": "amount", + "type": { + "defined": { + "name": "BaseLots" } - ] + } }, { - "name": "SplinePriceUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplinePriceUpdatedEvent" - } + "name": "atLossCloseValue", + "type": { + "option": { + "defined": { + "name": "QuoteLots" } } - ] + } }, { - "name": "SplineParametersUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplineParametersUpdatedEvent" - } + "name": "inProfitCloseValue", + "type": { + "option": { + "defined": { + "name": "QuoteLots" } } - ] - }, + } + } + ] + } + }, + { + "name": "MultipleOrderPacket", + "docs": [ + "Batch order packet for placing multiple post-only limit orders in a single transaction." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "MarketAdded", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketAddedEvent" - } + "name": "bids", + "type": { + "vec": { + "defined": { + "name": "CondensedOrder" } } - ] + } }, { - "name": "MarketStatusChanged", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketStatusChangedEvent" - } + "name": "asks", + "type": { + "vec": { + "defined": { + "name": "CondensedOrder" } } - ] + } }, { - "name": "MarketParametersUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketParametersUpdatedEvent" - } - } + "name": "clientOrderId", + "type": { + "option": { + "array": [ + "u8", + 16 + ] } - ] + } }, { - "name": "FundingParametersUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "FundingParametersUpdatedEvent" - } + "name": "slide", + "type": "bool" + } + ] + } + }, + { + "name": "MultipleOrderPacketV2", + "docs": [ + "V2 batch packet: post-only orders with per-order flags (slide, reduce-only) and a scale-set id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bids", + "type": { + "vec": { + "defined": { + "name": "CondensedOrderV2" } } - ] + } }, { - "name": "FeesClaimed", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "FeesClaimedEvent" - } + "name": "asks", + "type": { + "vec": { + "defined": { + "name": "CondensedOrderV2" } } - ] + } }, { - "name": "PricesUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "PricesUpdatedEvent" - } - } + "name": "clientOrderId", + "type": { + "option": { + "array": [ + "u8", + 16 + ] } - ] + } }, { - "name": "LiquidationTransferSummary", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "LiquidationTransferSummaryEvent" - } - } - } - ] + "name": "scaleSetId", + "docs": [ + "Zero denotes no scale set; bits 0-6 hold a client-managed scale-order set id of 1-127 and bit 7 marks a continuation packet (0x80 alone is invalid). The id must be unique among the trader's resting orders on the market unless the packet is a continuation." + ], + "type": "u8" + } + ] + } + }, + { + "name": "CondensedOrder", + "docs": [ + "Condensed order representation for batch order placement (price, size, and optional TiF)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "priceInTicks", + "type": "u64" }, { - "name": "LiquidationTransfer", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "LiquidationTransferEvent" - } - } - } - ] + "name": "sizeInBaseLots", + "type": "u64" }, { - "name": "Liquidation", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "LiquidationEvent" - } - } - } - ] + "name": "lastValidSlot", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "CondensedOrderV2", + "docs": [ + "Condensed order with per-order flags, used by the V2 batch packet." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "priceInTicks", + "type": "u64" }, { - "name": "CloseMatchedPositions", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "CloseMatchedPositionsEvent" - } - } - } - ] + "name": "sizeInBaseLots", + "type": "u64" }, { - "name": "NameSuccessor", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "NameSuccessorEvent" - } - } - } - ] + "name": "lastValidSlot", + "docs": [ + "Expiry slot serialized as a raw u64; 0 means no expiry." + ], + "type": "u64" }, { - "name": "ClaimAuthority", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "ClaimAuthorityEvent" - } - } + "name": "flags", + "type": { + "defined": { + "name": "CondensedOrderFlags" } + } + } + ] + } + }, + { + "name": "CondensedOrderFlags", + "docs": [ + "Per-order flag bits for CondensedOrderV2: bit 0 = slide, bit 1 = reduce-only. Undefined bits are rejected." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "flags", + "type": "u8" + } + ] + } + }, + { + "name": "WithdrawFundsInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64", + "docs": [ + "Amount in quote lots to withdraw." ] - }, + } + ] + } + }, + { + "name": "UpdateSplineParametersParamsWithOrdering", + "type": { + "kind": "struct", + "fields": [ { - "name": "WithdrawStateTransition", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "WithdrawStateTransitionEvent" - } + "name": "bidRegions", + "type": { + "vec": { + "defined": { + "name": "TickRegionParams" } } - ] + } }, { - "name": "PnL", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "PnLEvent" - } + "name": "askRegions", + "type": { + "vec": { + "defined": { + "name": "TickRegionParams" } } - ] + } }, { - "name": "StopLossPlaced", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "StopLossPlacedEvent" - } - } - } - ] + "name": "refreshRegions", + "type": "bool" }, { - "name": "StopLossCancelled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "StopLossCancelledEvent" - } - } - } - ] + "name": "userSequenceNumber", + "type": "u64" }, { - "name": "StopLossExecuted", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "StopLossExecutedEvent" - } - } - } - ] + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } }, { - "name": "ExchangeStatusChanged", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "ExchangeStatusChangedEvent" - } - } - } - ] - }, + "name": "overrideSequenceNumber", + "type": "bool" + } + ] + } + }, + { + "name": "TickRegionParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "TraderCapabilitiesEnabled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderCapabilitiesEnabledEvent" - } - } + "name": "startOffset", + "type": { + "defined": { + "name": "Ticks" } - ] + } }, { - "name": "TraderFundsWithdrawnFeePayment", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFundsWithdrawnFeePaymentEvent" - } - } + "name": "endOffset", + "type": { + "defined": { + "name": "Ticks" } - ] + } }, { - "name": "OrderPacket", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "OrderPacketEvent" - } - } + "name": "density", + "type": { + "defined": { + "name": "BaseLotsPerTick" } - ] + } }, { - "name": "SetPermission", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SetPermissionEvent" - } - } - } - ] - }, + "name": "lifespan", + "type": "u64" + } + ] + } + }, + { + "name": "PlaceStopLossInstruction", + "type": { + "kind": "struct", + "fields": [ { - "name": "AuthorityChanged", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "AuthorityChangedEvent" - } - } - } - ] + "name": "triggerPrice", + "type": "u64" }, { - "name": "TraderDelegated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderDelegatedEvent" - } - } - } - ] + "name": "executionPrice", + "type": "u64" }, { - "name": "AdminParameterUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "AdminParameterUpdatedEvent" - } - } - } - ] + "name": "tradeSize", + "type": "u64" }, { - "name": "TraderFeesUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TraderFeesUpdatedEvent" - } - } + "name": "tradeSide", + "type": { + "defined": { + "name": "Side" } - ] + } }, { - "name": "MarketClosed", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketClosedEvent" - } - } + "name": "executionDirection", + "type": { + "defined": { + "name": "Direction" } - ] + } }, { - "name": "MarketDeleted", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketDeletedEvent" - } - } + "name": "orderKind", + "type": { + "defined": { + "name": "StopLossOrderKind" } - ] - }, + } + } + ] + } + }, + { + "name": "ExecuteStopLossInstruction", + "type": { + "kind": "struct", + "fields": [ { - "name": "EscrowAccountCreated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "EscrowAccountCreatedEvent" - } - } + "name": "executionDirection", + "type": { + "defined": { + "name": "Direction" } - ] - }, + } + } + ] + } + }, + { + "name": "CancelStopLossInstruction", + "type": { + "kind": "struct", + "fields": [ { - "name": "EscrowRequestCreated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "EscrowRequestCreatedEvent" - } - } - } - ] + "name": "executionDirection", + "type": { + "defined": { + "name": "Direction" + } + } + } + ] + } + }, + { + "name": "ForceCancelRiskIncreasingParams", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "Direction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "GreaterThan" }, { - "name": "EscrowRequestAccepted", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "EscrowRequestAcceptedEvent" - } - } - } - ] + "name": "LessThan" + } + ] + } + }, + { + "name": "StopLossOrderKind", + "type": { + "kind": "enum", + "variants": [ + { + "name": "IOC" }, { - "name": "EscrowRequestCancelled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "EscrowRequestCancelledEvent" - } - } - } - ] + "name": "Limit" + } + ] + } + }, + { + "name": "LogHeader", + "type": { + "kind": "struct", + "fields": [ + { + "name": "logBatchIndex", + "docs": [ + "Index of this log batch for tracking event sequences." + ], + "type": "u32" }, { - "name": "SplinePriceUpdatedWithOrdering", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplinePriceUpdatedWithOrderingEvent" - } - } - } - ] + "name": "totalEvents", + "docs": [ + "Number of events serialized in this log batch." + ], + "type": "u32" + } + ] + } + }, + { + "name": "OrderbookHeader", + "docs": [ + "Fixed metadata prefix for a Phoenix Eternal orderbook account.", + "The large recent-trade buffer and dynamic orderbook bytes follow this prefix and are left as trailing account data by generic decoders." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketStatus", + "type": "u8" }, { - "name": "SplineParametersUpdatedWithOrdering", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplineParametersUpdatedWithOrderingEvent" - } - } - } - ] + "name": "baseLotsDecimals", + "type": "i8" }, { - "name": "TriggerOrderPlaced", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TriggerOrderPlacedEvent" - } - } - } - ] + "name": "padding0", + "type": { + "array": [ + "u8", + 6 + ] + } }, { - "name": "TriggerOrderCancelled", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TriggerOrderCancelledEvent" - } - } + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" } - ] + } }, { - "name": "TriggerOrderExecuted", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "TriggerOrderExecutedEvent" - } - } - } - ] + "name": "assetId", + "type": "u32" }, { - "name": "PingInvalidated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "PingInvalidatedEvent" - } - } - } - ] + "name": "assetIdPadding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "PingActivated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "PingActivatedEvent" - } - } + "name": "assetSymbol", + "type": { + "defined": { + "name": "Symbol" } - ] + } }, { - "name": "SplinePositionLimitsConfigUpdated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SplinePositionLimitsConfigUpdatedEvent" - } - } - } - ] + "name": "tickSizeInQuoteLotsPerBaseLot", + "type": "u64" }, { - "name": "MarketTombstoned", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "MarketTombstonedEvent" - } - } + "name": "orderSequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" } - ] + } }, { - "name": "ShutdownClosePositions", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "ShutdownClosePositionsEvent" - } - } + "name": "tradeSequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" } - ] + } }, { - "name": "OrderResidualDiscarded", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "OrderResidualDiscardedEvent" - } - } - } - ] + "name": "defaultTakerFeeMicro", + "type": "u32" }, { - "name": "SpotCollateralDeposited", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SpotCollateralDepositedEvent" - } - } - } - ] - }, - { - "name": "SpotCollateralWithdrawn", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SpotCollateralWithdrawnEvent" - } - } - } - ] + "name": "defaultMakerFeeMicro", + "type": "i32" }, { - "name": "SpotCollateralLiquidated", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "SpotCollateralLiquidatedEvent" - } - } + "name": "totalMakerQuoteLotFees", + "type": { + "defined": { + "name": "SignedQuoteLots" } - ] + } }, { - "name": "RestartAcknowledged", - "fields": [ - { - "name": "value", - "type": { - "defined": { - "name": "RestartAcknowledgedEvent" - } - } + "name": "totalTakerQuoteLotFees", + "type": { + "defined": { + "name": "QuoteLots" } - ] - } - ] - } - }, - { - "name": "SlotContextEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "timestamp", - "type": "u64" - }, - { - "name": "slot", - "type": "u64" + } } ] } }, { - "name": "MarketEventHeader", + "name": "SplineCollectionHeader", + "docs": [ + "Fixed SplineCollectionHeader prefix for a Phoenix Eternal spline collection account.", + "Dynamic spline map bytes follow this header and are left as trailing account data by generic decoders." + ], "type": { "kind": "struct", "fields": [ { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "market", + "type": "pubkey" }, { "name": "assetSymbol", @@ -5873,355 +23697,287 @@ } }, { - "name": "assetId", - "type": "u32" + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" + } + } }, { - "name": "tickSize", + "name": "numSplines", "type": "u32" }, { - "name": "baseLotDecimals", - "type": "i8" - }, - { - "name": "quoteLotDecimals", - "type": "u8" - }, - { - "name": "signer", - "type": "pubkey" + "name": "numActive", + "type": "u32" }, { - "name": "traderAccount", - "type": "pubkey" + "name": "padding", + "type": { + "array": [ + "u8", + 32 + ] + } } ] } }, { - "name": "OrderPlacedEvent", + "name": "TraderState", + "docs": [ + "Fixed trader state embedded in the Phoenix Eternal DynamicTraderHeader." + ], "type": { "kind": "struct", "fields": [ { - "name": "orderId", - "type": "u32" - }, - { - "name": "orderFlags", - "type": { - "defined": { - "name": "OrderFlags" - } - } - }, - { - "name": "orderSequenceNumber", - "type": "u64" + "name": "quoteLotCollateral", + "type": "i64" }, { - "name": "prevOrderSequenceNumberSlot", - "type": "u64" + "name": "flags", + "type": "u32" }, { - "name": "clientOrderId", + "name": "padding", "type": { "array": [ "u8", - 16 + 1 ] } }, { - "name": "price", - "type": { - "defined": { - "name": "Ticks" - } - } - }, - { - "name": "quantity", - "type": { - "defined": { - "name": "SignedBaseLots" - } - } + "name": "globalPositionSequenceNumber", + "type": "u8" }, { - "name": "lastValidSlot", - "type": { - "defined": { - "name": "OptionalNonZeroU64" - } - } + "name": "makerFeeOverrideMultiplier", + "type": "i8" }, { - "name": "initialSlot", - "type": "u64" + "name": "takerFeeOverrideMultiplier", + "type": "i8" } ] } }, { - "name": "OrderPacketEvent", + "name": "Trader", + "docs": [ + "Fixed DynamicTraderHeader prefix for a Phoenix Eternal trader account.", + "Dynamic position map bytes follow this header and are left as trailing account data by generic decoders." + ], "type": { "kind": "struct", "fields": [ { - "name": "orderPacket", + "name": "sequenceNumber", "type": { "defined": { - "name": "OrderPacket" + "name": "SequenceNumber" } } }, { - "name": "trader", + "name": "key", "type": "pubkey" }, { - "name": "nextOrderSequenceNumber", - "type": "u64" - } - ] - } - }, - { - "name": "OrderRejectedEvent", - "type": { - "kind": "struct", - "fields": [ + "name": "authority", + "type": "pubkey" + }, { - "name": "orderIndex", - "type": "u32" + "name": "traderState", + "type": { + "defined": { + "name": "TraderState" + } + } }, { - "name": "clientOrderId", + "name": "padding0", "type": { "array": [ "u8", - 16 + 4 ] } }, { - "name": "price", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "withdrawQueueNode", + "type": "u32" }, { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } + "name": "maxPositions", + "type": "u32" }, { - "name": "numBaseLots", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "traderPreferenceBits", + "type": "u32" }, { - "name": "reason", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "OrderResidualDiscardedEvent", - "type": { - "kind": "struct", - "fields": [ + "name": "positionAuthority", + "type": "pubkey" + }, { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 - ] - } + "name": "numMarketsWithSplines", + "type": "u16" }, { - "name": "price", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "traderPdaIndex", + "type": "u8" }, { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } + "name": "traderSubaccountIndex", + "type": "u8" }, { - "name": "baseLotsDiscarded", + "name": "fundingKey", + "type": "pubkey" + }, + { + "name": "padding1", "type": { - "defined": { - "name": "BaseLots" - } + "array": [ + "u8", + 4 + ] } }, { - "name": "reason", + "name": "lastDepositSlot", + "type": "u64" + }, + { + "name": "conditionalOrderBits", "type": { - "defined": { - "name": "OrderResidualDiscardReason" - } + "array": [ + "u8", + 24 + ] } } ] } }, { - "name": "OrderFilledEvent", + "name": "ActiveTraderBufferHeader", + "docs": [ + "Discriminator-only account definition for viewer compatibility.", + "The full active trader buffer layout is not modeled in this IDL." + ], + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "ActiveTraderBufferArenaHeader", + "docs": [ + "Discriminator-only account definition for viewer compatibility.", + "The full active trader buffer arena layout is not modeled in this IDL." + ], + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "GlobalTraderIndexHeader", + "docs": [ + "Discriminator-only account definition for viewer compatibility.", + "The full global trader index layout is not modeled in this IDL." + ], + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "GlobalTraderIndexArenaHeader", + "docs": [ + "Discriminator-only account definition for viewer compatibility.", + "The full global trader index arena layout is not modeled in this IDL." + ], + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "GlobalConfiguration", "type": { "kind": "struct", "fields": [ { - "name": "orderSequenceNumber", - "type": "u64" + "name": "accountKey", + "type": "pubkey" }, { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } + "name": "rootAuthority", + "type": "pubkey" }, { - "name": "price", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "riskAuthority", + "type": "pubkey" }, { - "name": "baseLotsFilled", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "marketAuthority", + "type": "pubkey" }, { - "name": "quoteLotsFilled", - "type": { - "defined": { - "name": "QuoteLots" - } - } + "name": "oracleAuthority", + "type": "pubkey" }, { - "name": "quantityRemaining", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "successorAuthority", + "type": "pubkey" }, { - "name": "maker", + "name": "adlAuthority", "type": "pubkey" }, { - "name": "makerFeeRate", - "type": { - "defined": { - "name": "SignedFeeRateMicro" - } - } + "name": "cancelAuthority", + "type": "pubkey" }, { - "name": "makerBaseLotPosition", - "type": { - "defined": { - "name": "SignedBaseLots" - } - } + "name": "backstopAuthority", + "type": "pubkey" }, { - "name": "makerVirtualQuoteLotPosition", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "canonicalTokenMintKey", + "type": "pubkey" }, { - "name": "makerQuoteLotCollateral", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "globalVaultKey", + "type": "pubkey" }, { - "name": "makerCumulativeFundingSnapshot", - "type": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } - } - } - ] - } - }, - { - "name": "SplineFilledEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "splineSequenceNumber", - "type": "u64" + "name": "perpAssetMapKey", + "type": "pubkey" }, { - "name": "side", - "type": { - "defined": { - "name": "Side" - } - } + "name": "globalTraderIndexHeaderKey", + "type": "pubkey" }, { - "name": "price", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "activeTraderBufferHeaderKey", + "type": "pubkey" }, { - "name": "baseLotsFilled", + "name": "totalQuoteLotFees", "type": { "defined": { - "name": "BaseLots" + "name": "QuoteLots" } } }, { - "name": "quoteLotsFilled", + "name": "unclaimedQuoteLotFees", "type": { "defined": { "name": "QuoteLots" @@ -6229,246 +23985,204 @@ } }, { - "name": "maker", + "name": "withdrawQueueKey", "type": "pubkey" }, { - "name": "makerFeeRate", - "type": { - "defined": { - "name": "SignedFeeRateMicro" - } - } + "name": "exchangeStatus", + "type": "u8" }, { - "name": "makerBaseLotPosition", - "type": { - "defined": { - "name": "SignedBaseLots" - } - } + "name": "quoteDecimals", + "type": "u8" }, { - "name": "makerVirtualQuoteLotPosition", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "withdrawalMarginFactorBps", + "type": "u16" }, { - "name": "makerQuoteLotCollateral", + "name": "padding0", "type": { - "defined": { - "name": "SignedQuoteLots" - } + "array": [ + "u8", + 4 + ] } }, { - "name": "makerCumulativeFundingSnapshot", - "type": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } - } - } - ] - } - }, - { - "name": "OrderModifiedEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "orderSequenceNumber", + "name": "depositCooldownPeriodInSlots", "type": "u64" }, { - "name": "price", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "pendingRootAuthority", + "type": "pubkey" }, { - "name": "baseLotsReleased", - "type": { - "defined": { - "name": "SignedBaseLots" - } - } + "name": "pendingRiskAuthority", + "type": "pubkey" }, { - "name": "quoteLotsReleased", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "pendingMarketAuthority", + "type": "pubkey" }, { - "name": "baseLotsRemaining", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "pendingOracleAuthority", + "type": "pubkey" }, { - "name": "reason", - "type": { - "defined": { - "name": "OrderModificationReason" - } - } - } - ] - } - }, - { - "name": "TradeSummaryEvent", - "type": { - "kind": "struct", - "fields": [ + "name": "pendingReservedAuthority", + "type": "pubkey" + }, { - "name": "trader", + "name": "pendingAdlAuthority", "type": "pubkey" }, { - "name": "tradeSequenceNumber", - "type": "u64" + "name": "pendingCancelAuthority", + "type": "pubkey" }, { - "name": "prevTradeSequenceNumberSlot", - "type": "u64" + "name": "pendingBackstopAuthority", + "type": "pubkey" }, { - "name": "side", + "name": "padding1", "type": { - "defined": { - "name": "Side" - } + "array": [ + "u64", + 40 + ] } }, { - "name": "baseLotsFilled", + "name": "acknowledgedRestartSlot", + "type": "u64" + }, + { + "name": "pendingWithdrawThrottleReplenishment", "type": { "defined": { - "name": "BaseLots" + "name": "QuoteLots" } } }, { - "name": "quoteLotsFilled", + "name": "padding2", "type": { - "defined": { - "name": "QuoteLots" - } + "array": [ + "u64", + 21 + ] } }, { - "name": "feeInQuoteLots", + "name": "padding3", "type": { - "defined": { - "name": "QuoteLots" - } + "array": [ + "u64", + 32 + ] } }, { - "name": "baseLotPosition", + "name": "padding4", "type": { - "defined": { - "name": "SignedBaseLots" - } + "array": [ + "u64", + 32 + ] } }, { - "name": "virtualQuoteLotPosition", + "name": "padding5", "type": { - "defined": { - "name": "SignedQuoteLots" - } + "array": [ + "u64", + 32 + ] } }, { - "name": "quoteLotCollateral", + "name": "padding6", "type": { - "defined": { - "name": "SignedQuoteLots" - } + "array": [ + "u64", + 32 + ] } }, { - "name": "cumulativeFundingSnapshot", + "name": "padding7", "type": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } + "array": [ + "u64", + 32 + ] } } ] } }, { - "name": "MarketSummaryEvent", + "name": "SequenceNumber", + "docs": [ + "Pair of sequence number and last update slot." + ], "type": { "kind": "struct", "fields": [ { - "name": "assetSymbol", - "type": { - "defined": { - "name": "Symbol" - } - } - }, - { - "name": "assetId", - "type": "u32" - }, - { - "name": "openInterest", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "sequenceNumber", + "type": "u64" }, { - "name": "totalMakerQuoteLotFees", - "type": { - "option": { - "defined": { - "name": "SignedQuoteLots" - } - } - } - }, + "name": "lastUpdateSlot", + "type": "u64" + } + ] + } + }, + { + "name": "PerpAssetMetadata", + "docs": [ + "Opaque fixed-size zero-copy PerpAssetMetadata payload stored inside PerpAssetMap." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "totalTakerQuoteLotFees", + "name": "raw", "type": { - "option": { - "defined": { - "name": "QuoteLots" - } - } + "array": [ + "u8", + 1568 + ] } - }, + } + ] + } + }, + { + "name": "PerpAssetMapEntry", + "docs": [ + "One fixed slot in the PerpAssetMap StableIndexedShortMap backing array." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "markPrice", + "name": "key", "type": { "defined": { - "name": "Ticks" + "name": "Symbol" } } }, { - "name": "spotPrice", + "name": "value", "type": { "defined": { - "name": "Ticks" + "name": "PerpAssetMetadata" } } } @@ -6476,439 +24190,552 @@ } }, { - "name": "TraderRegisteredEvent", + "name": "PerpAssetMetadataStableIndexedShortMap", + "docs": [ + "Zero-copy StableIndexedShortMap layout." + ], "type": { "kind": "struct", "fields": [ { - "name": "traderSequenceNumber", - "type": "u64" - }, - { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "maxPositions", + "name": "slotsUsed", "type": "u32" }, { - "name": "traderPreferenceBits", + "name": "tombstones", "type": "u32" }, { - "name": "traderPdaIndex", - "type": "u8" + "name": "capacity", + "type": "u64" }, { - "name": "traderSubaccountIndex", - "type": "u8" + "name": "data", + "type": { + "array": [ + { + "defined": { + "name": "PerpAssetMapEntry" + } + }, + 1024 + ] + } } ] } }, { - "name": "TraderDelegatedEvent", + "name": "PerpAssetMap", + "docs": [ + "Canonical Eternal perpetual asset map account." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" + } + } }, { - "name": "authority", - "type": "pubkey" + "name": "numAssets", + "type": "u16" }, { - "name": "oldPositionAuthority", - "type": "pubkey" + "name": "padding0", + "type": { + "array": [ + "u8", + 6 + ] + } }, { - "name": "newPositionAuthority", - "type": "pubkey" + "name": "metadatas", + "type": { + "defined": { + "name": "PerpAssetMetadataStableIndexedShortMap" + } + } } ] } }, { - "name": "TraderFeesUpdatedEvent", + "name": "WithdrawThrottle", + "docs": [ + "Fixed WithdrawThrottle payload embedded in WithdrawQueueHeader." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "previousMakerFeeOverrideMultiplier", - "type": "i8" - }, - { - "name": "newMakerFeeOverrideMultiplier", - "type": "i8" + "name": "maxBudget", + "type": { + "defined": { + "name": "QuoteLots" + } + } }, { - "name": "previousTakerFeeOverrideMultiplier", - "type": "i8" + "name": "remainingBudget", + "type": { + "defined": { + "name": "QuoteLots" + } + } }, { - "name": "newTakerFeeOverrideMultiplier", - "type": "i8" + "name": "replenishAmountPerSlot", + "type": { + "defined": { + "name": "QuoteLots" + } + } }, { - "name": "isHotTrader", - "type": "bool" + "name": "lastUpdateSlot", + "type": "u64" } ] } }, { - "name": "TraderCapabilitiesEnabledEvent", + "name": "WithdrawQueueHeader", + "docs": [ + "Fixed WithdrawQueueHeader prefix for the Phoenix Eternal withdraw queue account.", + "The deque of pending withdrawal requests follows this header and is left as trailing account data by generic decoders." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" + } + } }, { - "name": "authority", - "type": "pubkey" + "name": "withdrawThrottle", + "type": { + "defined": { + "name": "WithdrawThrottle" + } + } }, { - "name": "previousFlags", + "name": "totalQueuedAmount", "type": { "defined": { - "name": "TraderCapabilityFlags" + "name": "QuoteLots" } } }, { - "name": "newFlags", + "name": "withdrawalFee", "type": { "defined": { - "name": "TraderCapabilityFlags" + "name": "QuoteLots" } } }, { - "name": "globalTraderIndex", - "type": "u32" + "name": "enqueueingFee", + "type": { + "defined": { + "name": "QuoteLots" + } + } + }, + { + "name": "reserved", + "type": { + "array": [ + "u64", + 5 + ] + } } ] } }, { - "name": "TraderActivatedEvent", + "name": "PermissionAccount", + "docs": [ + "Fixed Phoenix Eternal permission delegation account layout." + ], "type": { "kind": "struct", "fields": [ { - "name": "globalTraderIndex", - "type": "u32" + "name": "authority", + "type": "pubkey" }, { - "name": "authority", + "name": "user", "type": "pubkey" - } - ] - } - }, - { - "name": "TraderDeactivatedEvent", - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "prevGlobalTraderIndex", - "type": "u32" + "name": "bump", + "type": "u8" }, { - "name": "authority", - "type": "pubkey" + "name": "padding1", + "type": { + "array": [ + "u8", + 7 + ] + } + }, + { + "name": "permission", + "type": "u64" + }, + { + "name": "expiresAtTimestamp", + "type": "i64" + }, + { + "name": "numSignerActionsRemaining", + "type": "i64" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 64 + ] + } } ] } }, { - "name": "TraderFundsDepositedEvent", + "name": "StopLoss", + "docs": [ + "One fixed trigger order slot stored inside the StopLosses account." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "sequenceNumber", + "type": "u64" }, { - "name": "authority", - "type": "pubkey" + "name": "triggerPrice", + "type": { + "defined": { + "name": "Ticks" + } + } }, { - "name": "amount", + "name": "executionPrice", "type": { "defined": { - "name": "QuoteLots" + "name": "Ticks" } } }, { - "name": "traderFlags", + "name": "tradeSize", "type": { "defined": { - "name": "TraderCapabilityFlags" + "name": "BaseLots" } } }, { - "name": "newCollateralBalance", + "name": "slot", + "type": "u64" + }, + { + "name": "positionSequenceNumber", + "type": "u8" + }, + { + "name": "flags", + "type": "u8" + }, + { + "name": "additionalFees", "type": { "defined": { - "name": "SignedQuoteLots" + "name": "AdditionalFees" } } }, { - "name": "traderSequenceNumber", - "type": "u64" + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "padding2", + "type": { + "array": [ + "u8", + 32 + ] + } } ] } }, { - "name": "TraderFundsWithdrawnEvent", + "name": "StopLosses", + "docs": [ + "Fixed Phoenix Eternal StopLosses account layout." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" + "name": "stopLosses", + "type": { + "array": [ + { + "defined": { + "name": "StopLoss" + } + }, + 2 + ] + } }, { - "name": "amount", + "name": "initialized", "type": "u64" }, { - "name": "traderSequenceNumber", - "type": "u64" - }, - { - "name": "traderPrevSequenceNumberSlot", - "type": "u64" + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" + } + } }, { - "name": "postWithdrawalBudget", - "type": "u64" + "name": "fundingKey", + "type": "pubkey" }, { - "name": "postQueueSize", - "type": "u64" + "name": "traderKey", + "type": "pubkey" }, { - "name": "totalQueuedAmount", - "type": "u64" + "name": "assetId", + "type": "u32" }, { - "name": "withdrawQueueSequenceNumber", - "type": "u64" + "name": "assetIdPadding", + "type": { + "array": [ + "u8", + 4 + ] + } }, { - "name": "withdrawQueuePrevSequenceNumberSlot", - "type": "u64" + "name": "padding", + "type": { + "array": [ + "u64", + 8 + ] + } } ] } }, { - "name": "TraderFundsWithdrawnFeePaymentEvent", + "name": "EscrowHeader", + "docs": [ + "Fixed EscrowHeader prefix for a Phoenix Eternal escrow account.", + "The dynamic escrow request list follows this header and is left as trailing account data by generic decoders." + ], "type": { "kind": "struct", "fields": [ - { - "name": "trader", - "type": "pubkey" - }, { "name": "authority", "type": "pubkey" }, { - "name": "fee", - "type": "u64" - }, - { - "name": "traderSequenceNumber", - "type": "u64" + "name": "funderKey", + "type": "pubkey" }, { - "name": "traderPrevSequenceNumberSlot", - "type": "u64" + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" + } + } }, { - "name": "withdrawQueueSequenceNumber", + "name": "len", "type": "u64" }, { - "name": "withdrawQueuePrevSequenceNumberSlot", + "name": "capacity", "type": "u64" } ] } }, { - "name": "TraderWithdrawCancelledEvent", + "name": "ConditionalOrderHeader", + "docs": [ + "Fixed ConditionalOrderHeader prefix for a Phoenix Eternal conditional orders account.", + "The dynamic conditional order array follows this header and is left as trailing account data by generic decoders." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", + "name": "traderKey", "type": "pubkey" }, { - "name": "authority", + "name": "fundingKey", "type": "pubkey" }, { - "name": "amount", - "type": "u64" + "name": "sequenceNumber", + "type": { + "defined": { + "name": "SequenceNumber" + } + } }, { - "name": "traderSequenceNumber", - "type": "u64" + "name": "len", + "type": "u8" }, { - "name": "traderPrevSequenceNumberSlot", - "type": "u64" + "name": "capacity", + "type": "u8" }, { - "name": "withdrawQueueSequenceNumber", - "type": "u64" - }, + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "RevokePermissionParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "withdrawQueuePrevSequenceNumberSlot", + "name": "permissionFlagsToRevoke", "type": "u64" } ] } }, { - "name": "TraderCollateralTransferredEvent", + "name": "SetPermissionInstruction", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "type": "pubkey" + "name": "permission", + "type": "u64" }, { - "name": "amount", + "name": "expiresAtTimestamp", "type": { - "defined": { - "name": "QuoteLots" - } + "option": "i64" } }, { - "name": "srcTrader", - "type": "pubkey" - }, - { - "name": "srcTraderSequenceNumber", - "type": "u64" - }, + "name": "allowedSignerActions", + "type": { + "option": "i64" + } + } + ] + } + }, + { + "name": "TraderCapabilityToggleTarget", + "type": { + "kind": "enum", + "variants": [ { - "name": "srcTraderPrevSequenceNumberSlot", - "type": "u64" + "name": "PlaceLimitOrder" }, { - "name": "srcTraderNewCollateralBalance", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "PlaceMarketOrder" }, { - "name": "dstTrader", - "type": "pubkey" + "name": "RiskIncreasingTrade" }, { - "name": "dstTraderSequenceNumber", - "type": "u64" + "name": "RiskReducingTrade" }, { - "name": "dstTraderPrevSequenceNumberSlot", - "type": "u64" + "name": "DepositCollateral" }, { - "name": "dstTraderNewCollateralBalance", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "WithdrawCollateral" } ] } }, { - "name": "TraderFundingSettledEvent", + "name": "TraderCapabilityToggle", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "assetSymbol", - "type": { - "defined": { - "name": "Symbol" - } - } - }, - { - "name": "assetId", - "type": "u32" - }, - { - "name": "fundingPayment", + "name": "target", "type": { "defined": { - "name": "SignedQuoteLots" + "name": "TraderCapabilityToggleTarget" } } }, { - "name": "cumulativeFundingSnapshot", - "type": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } - } - }, + "name": "enable", + "type": "bool" + } + ] + } + }, + { + "name": "TraderCapabilityUpdate", + "type": { + "kind": "struct", + "fields": [ { - "name": "newCollateralBalance", + "name": "toggles", "type": { - "defined": { - "name": "SignedQuoteLots" + "vec": { + "defined": { + "name": "TraderCapabilityToggle" + } } } } @@ -6916,107 +24743,89 @@ } }, { - "name": "SplineRegisteredEvent", + "name": "SetTraderCapOverrideInstruction", + "docs": [ + "Borsh payload for updating a trader's native SOL or spot collateral cap override by asset index." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" + "name": "assetIndex", + "type": "u32" }, { - "name": "prevSequenceNumberSlot", + "name": "maxPerTraderBalanceOverride", "type": "u64" - }, - { - "name": "market", - "type": "pubkey" - }, - { - "name": "symbol", - "type": { - "defined": { - "name": "Symbol" - } - } } ] } }, { - "name": "SplineActivatedEvent", + "name": "TransferCollateralInstruction", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", + "name": "amount", "type": "u64" - }, + } + ] + } + }, + { + "name": "TransferCollateralChildToParentInstruction", + "type": { + "kind": "struct", + "fields": [ { - "name": "prevSequenceNumberSlot", + "name": "amount", "type": "u64" - }, - { - "name": "authority", - "type": "pubkey" - }, + } + ] + } + }, + { + "name": "UncrossCrankParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "market", - "type": "pubkey" - }, + "name": "matchLimit", + "type": "u64" + } + ] + } + }, + { + "name": "ClearExpiredOrdersParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "symbol", + "name": "maxOrdersPerSide", "type": { - "defined": { - "name": "Symbol" - } + "option": "u64" } - }, - { - "name": "midPrice", - "type": "u64" } ] } }, { - "name": "SplineDeactivatedEvent", + "name": "ClearPositionFlipOrdersParams", + "docs": [ + "Borsh payload identifying the reduce-only clear-on-position-flip orders to remove." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "market", - "type": "pubkey" - }, - { - "name": "symbol", + "name": "cancelIds", "type": { - "defined": { - "name": "Symbol" + "vec": { + "defined": { + "name": "CancelId" + } } } } @@ -7024,465 +24833,475 @@ } }, { - "name": "SplinePositionLimitsConfigUpdatedEvent", + "name": "TriggerOrderParams", + "docs": [ + "Trigger and execution parameters for one half of a conditional order." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "triggerDirection", + "type": { + "defined": { + "name": "Direction" + } + } }, { - "name": "sequenceNumber", - "type": "u64" + "name": "tradeSide", + "type": { + "defined": { + "name": "Side" + } + } }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "orderKind", + "type": { + "defined": { + "name": "StopLossOrderKind" + } + } }, { - "name": "authority", - "type": "pubkey" + "name": "triggerPrice", + "type": "u64" }, { - "name": "market", - "type": "pubkey" + "name": "executionPrice", + "type": "u64" + } + ] + } + }, + { + "name": "CreateConditionalOrdersAccountInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "capacity", + "type": "u8" + } + ] + } + }, + { + "name": "PlacePositionConditionalOrderInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetId", + "type": "u32" }, { - "name": "symbol", + "name": "greaterTriggerOrder", "type": { - "defined": { - "name": "Symbol" + "option": { + "defined": { + "name": "TriggerOrderParams" + } } } }, { - "name": "maxPositionSize", + "name": "lessTriggerOrder", "type": { "option": { "defined": { - "name": "PositionSizeLimit" + "name": "TriggerOrderParams" } } } }, { - "name": "prevMaxPositionSize", + "name": "sizeBaseLots", "type": { - "defined": { - "name": "PositionSizeLimit" - } + "option": "u64" } }, { - "name": "leverageDecreaseInBps", + "name": "sizePercent", "type": { - "option": "u32" + "option": "u8" } - }, - { - "name": "prevLeverageDecreaseInBps", - "type": "u32" } ] } }, { - "name": "SplinePriceUpdatedEvent", + "name": "CancelConditionalOrderInstruction", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "index", + "type": "u8" }, { - "name": "authority", - "type": "pubkey" + "name": "disableFirst", + "type": "bool" }, { - "name": "market", - "type": "pubkey" - }, + "name": "disableSecond", + "type": "bool" + } + ] + } + }, + { + "name": "PlaceAttachedConditionalOrderInstruction", + "type": { + "kind": "struct", + "fields": [ { - "name": "symbol", + "name": "orderId", "type": { "defined": { - "name": "Symbol" + "name": "FIFOOrderId" } } }, { - "name": "priceInTicks", - "type": "u64" + "name": "assetId", + "type": "u32" }, { - "name": "userUpdateSlot", - "type": "u64" + "name": "greaterTriggerOrder", + "type": { + "option": { + "defined": { + "name": "TriggerOrderParams" + } + } + } }, { - "name": "refreshRegions", - "type": "bool" + "name": "lessTriggerOrder", + "type": { + "option": { + "defined": { + "name": "TriggerOrderParams" + } + } + } } ] } }, { - "name": "SplineParametersUpdatedEvent", + "name": "PlaceLimitOrderWithConditionalsInstruction", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "orderPacket", + "type": { + "defined": { + "name": "OrderPacket" + } + } }, { - "name": "sequenceNumber", + "name": "slot", "type": "u64" }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "greaterTriggerOrder", + "type": { + "option": { + "defined": { + "name": "TriggerOrderParams" + } + } + } }, { - "name": "authority", - "type": "pubkey" - }, - { - "name": "market", - "type": "pubkey" - }, - { - "name": "symbol", + "name": "lessTriggerOrder", "type": { - "defined": { - "name": "Symbol" + "option": { + "defined": { + "name": "TriggerOrderParams" + } } } - }, - { - "name": "midPrice", - "type": "u64" - }, - { - "name": "bidRegions", - "type": { - "array": [ - { - "defined": { - "name": "TickRegion" - } - }, - 10 - ] - } - }, - { - "name": "askRegions", - "type": { - "array": [ - { - "defined": { - "name": "TickRegion" - } - }, - 10 - ] - } } ] } }, { - "name": "SplinePriceUpdatedWithOrderingEvent", + "name": "ExecuteConditionalOrderInstruction", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "market", - "type": "pubkey" + "name": "index", + "type": "u8" }, { - "name": "symbol", + "name": "triggerDirection", "type": { "defined": { - "name": "Symbol" + "name": "Direction" } } - }, - { - "name": "priceInTicks", - "type": "u64" - }, - { - "name": "userUpdateSlot", - "type": "u64" - }, - { - "name": "refreshRegions", - "type": "bool" - }, - { - "name": "userSequenceNumber", - "type": "u64" - }, - { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 - ] - } } ] } }, { - "name": "SplineParametersUpdatedWithOrderingEvent", + "name": "OffChainMarketEvent", + "docs": [ + "Borsh payload of the Log instruction after the 8-byte instruction discriminator." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" - }, - { - "name": "authority", - "type": "pubkey" - }, - { - "name": "market", - "type": "pubkey" + "name": "batchIndex", + "type": "u32" }, { - "name": "symbol", + "name": "events", "type": { - "defined": { - "name": "Symbol" + "vec": { + "defined": { + "name": "MarketEvent" + } } } - }, - { - "name": "midPrice", - "type": "u64" - }, - { - "name": "bidRegions", - "type": { - "array": [ - { - "defined": { - "name": "TickRegion" - } - }, - 10 - ] - } - }, - { - "name": "askRegions", - "type": { - "array": [ - { - "defined": { - "name": "TickRegion" - } - }, - 10 - ] - } - }, - { - "name": "userSequenceNumber", - "type": "u64" - }, - { - "name": "clientOrderId", - "type": { - "array": [ - "u8", - 16 - ] - } } ] } }, { - "name": "StopLossPlacedEvent", + "name": "OffChainMarketEventLengths", + "docs": [ + "Borsh payload of the LogEventLengths instruction after the 8-byte instruction discriminator." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" - }, - { - "name": "assetId", - "type": "u64" - }, - { - "name": "triggerPrice", - "type": { - "defined": { - "name": "Ticks" - } - } - }, - { - "name": "executionPrice", - "type": { - "defined": { - "name": "Ticks" - } - } - }, - { - "name": "tradeSize", - "type": { - "defined": { - "name": "BaseLots" - } - } - }, - { - "name": "tradeSide", - "type": { - "defined": { - "name": "Side" - } - } - }, - { - "name": "executionDirection", - "type": { - "defined": { - "name": "Direction" - } - } - }, - { - "name": "positionSequenceNumber", - "type": "u8" - }, - { - "name": "placeSlot", - "type": "u64" - }, - { - "name": "fundingKey", - "type": "pubkey" + "name": "batchIndex", + "type": "u32" }, { - "name": "orderKind", + "name": "lengths", "type": { - "defined": { - "name": "StopLossOrderKind" - } + "vec": "u16" } } ] } }, { - "name": "StopLossCancelledEvent", - "type": { - "kind": "struct", - "fields": [ + "name": "SignedBaseLots", + "docs": [ + "Signed quantity wrapper representing discrete base lots." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "trader", - "type": "pubkey" - }, + "name": "inner", + "type": "i64" + } + ] + } + }, + { + "name": "SignedQuoteLots", + "docs": [ + "Signed quantity wrapper representing discrete quote lots." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "sequenceNumber", - "type": "u64" - }, + "name": "inner", + "type": "i64" + } + ] + } + }, + { + "name": "SignedFeeRateMicro", + "docs": [ + "Signed fee rate in millionths." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "prevSequenceNumberSlot", - "type": "u64" - }, + "name": "inner", + "type": "i32" + } + ] + } + }, + { + "name": "SignedQuoteLotsPerBaseLot", + "docs": [ + "Signed quote lots per base lot wrapper." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "assetId", + "name": "inner", + "type": "i64" + } + ] + } + }, + { + "name": "SignedQuoteLotsPerBaseLotUpcasted", + "docs": [ + "Upcasted signed quote lots per base lot wrapper." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "inner", + "type": "i128" + } + ] + } + }, + { + "name": "SignedTicks", + "docs": [ + "Signed discrete tick offset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "inner", + "type": "i64" + } + ] + } + }, + { + "name": "FundingRateUnitInSeconds", + "docs": [ + "Funding time unit measured in seconds." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "BaseLotsU32", + "docs": [ + "u32-backed base lot wrapper." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "executionDirection", - "type": { - "defined": { - "name": "Direction" - } - } + "name": "inner", + "type": "u32" } ] } }, { - "name": "StopLossExecutedEvent", + "name": "BaseLotsPerTickU32", + "docs": [ + "u32-backed spline liquidity density wrapper." + ], "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, + "name": "inner", + "type": "u32" + } + ] + } + }, + { + "name": "BasisPointsU16", + "docs": [ + "Basis points wrapper, u16 backed." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "sequenceNumber", + "name": "inner", + "type": "u16" + } + ] + } + }, + { + "name": "BasisPoints", + "docs": [ + "Basis points wrapper." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "Constant", + "docs": [ + "Dimensionless constant wrapper." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "prevSequenceNumberSlot", + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "OptionalNonZeroU64", + "docs": [ + "Zero represents None; non-zero represents Some(value)." + ], + "type": { + "kind": "struct", + "fields": [ { - "name": "assetId", + "name": "inner", "type": "u64" - }, + } + ] + } + }, + { + "name": "OptionalFIFOOrderId", + "type": { + "kind": "struct", + "fields": [ { - "name": "executionDirection", + "name": "priceInTicks", "type": { "defined": { - "name": "Direction" + "name": "Ticks" } } }, @@ -7494,80 +25313,113 @@ } }, { - "name": "TriggerOrderPlacedEvent", + "name": "TraderCapabilityFlags", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "bits", + "type": "u32" + } + ] + } + }, + { + "name": "MarketStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Uninitialized" }, { - "name": "sequenceNumber", - "type": "u64" + "name": "Active" }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "PostOnly" }, { - "name": "assetId", - "type": "u64" + "name": "Paused" }, { - "name": "conditionalOrderIndex", - "type": "u8" + "name": "Closed" }, { - "name": "triggerPrice", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "Tombstoned" + } + ] + } + }, + { + "name": "CommodityMarketState", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Active" }, { - "name": "executionPrice", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "AfterHours" }, { - "name": "tradeSide", - "type": { - "defined": { - "name": "Side" - } - } + "name": "Reopen" + } + ] + } + }, + { + "name": "AuthorityType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Root" }, { - "name": "triggerDirection", - "type": { - "defined": { - "name": "Direction" - } - } + "name": "Risk" }, { - "name": "orderKind", - "type": { - "defined": { - "name": "StopLossOrderKind" - } - } + "name": "Market" }, { - "name": "maxSize", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "Oracle" }, { - "name": "fillableSize", + "name": "Cancel" + }, + { + "name": "Backstop" + }, + { + "name": "ADL" + } + ] + } + }, + { + "name": "ValidationRule", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Ignore" + }, + { + "name": "Require" + }, + { + "name": "Forbid" + } + ] + } + }, + { + "name": "LeverageTier", + "type": { + "kind": "struct", + "fields": [ + { + "name": "upperBoundSize", "type": { "defined": { "name": "BaseLots" @@ -7575,30 +25427,18 @@ } }, { - "name": "filledSize", + "name": "maxLeverage", "type": { "defined": { - "name": "BaseLots" + "name": "Constant" } } }, { - "name": "usePercent", - "type": "bool" - }, - { - "name": "percent", - "type": "u8" - }, - { - "name": "positionSequenceNumber", - "type": "u8" - }, - { - "name": "attachedOrderId", + "name": "limitOrderRiskFactor", "type": { "defined": { - "name": "OptionalFIFOOrderId" + "name": "BasisPoints" } } } @@ -7606,393 +25446,450 @@ } }, { - "name": "TriggerOrderCancelledEvent", + "name": "LeverageTiers", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" - }, - { - "name": "assetId", - "type": "u64" - }, - { - "name": "conditionalOrderIndex", - "type": "u8" - }, - { - "name": "triggerDirection", + "name": "tiers", "type": { - "defined": { - "name": "Direction" - } + "array": [ + { + "defined": { + "name": "LeverageTier" + } + }, + 4 + ] } } ] } }, { - "name": "TriggerOrderExecutedEvent", + "name": "PositionSizeLimits", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "long", + "type": "u32" }, { - "name": "assetId", - "type": "u64" - }, + "name": "short", + "type": "u32" + } + ] + } + }, + { + "name": "PositionSizeLimit", + "type": { + "kind": "enum", + "variants": [ { - "name": "conditionalOrderIndex", - "type": "u8" + "name": "Disabled" }, { - "name": "triggerDirection", - "type": { - "defined": { - "name": "Direction" + "name": "Limit", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "PositionSizeLimits" + } + } } - } - }, - { - "name": "orderSequenceNumber", - "type": "u64" + ] } ] } }, { - "name": "PingInvalidatedEvent", + "name": "TickRegion", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" + "name": "startOffset", + "type": { + "defined": { + "name": "Ticks" + } + } }, { - "name": "sequenceNumber", - "type": "u64" + "name": "endOffset", + "type": { + "defined": { + "name": "Ticks" + } + } }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "density", + "type": { + "defined": { + "name": "BaseLotsPerTickU32" + } + } }, { - "name": "assetId", - "type": "u64" + "name": "topLevelHiddenTakeSize", + "type": { + "defined": { + "name": "BaseLotsPerTickU32" + } + } }, { - "name": "conditionalOrderIndex", - "type": "u8" + "name": "totalSize", + "type": { + "defined": { + "name": "BaseLots" + } + } }, { - "name": "pre", + "name": "filledSize", "type": { "defined": { - "name": "ConditionalOrderPingSnapshot" + "name": "BaseLotsU32" } } }, { - "name": "post", + "name": "hiddenFilledSize", "type": { "defined": { - "name": "ConditionalOrderPingSnapshot" + "name": "BaseLotsU32" } } }, { - "name": "currentPositionSequenceNumber", - "type": "u8" + "name": "lifespan", + "type": "u64" } ] } }, { - "name": "PingActivatedEvent", + "name": "OrderRejectionReason", "type": { - "kind": "struct", - "fields": [ + "kind": "enum", + "variants": [ { - "name": "trader", - "type": "pubkey" + "name": "TooManyLimitOrders" }, { - "name": "sequenceNumber", - "type": "u64" + "name": "PostOnlyCross" }, { - "name": "prevSequenceNumberSlot", - "type": "u64" + "name": "InvalidOrderPacket" }, { - "name": "assetId", - "type": "u64" + "name": "TiFInvalid" }, { - "name": "conditionalOrderIndex", - "type": "u8" + "name": "OutsideExecutionPriceBand" + } + ] + } + }, + { + "name": "OrderResidualDiscardReason", + "type": { + "kind": "enum", + "variants": [ + { + "name": "OutsideExecutionPriceBand" + } + ] + } + }, + { + "name": "OrderModificationReason", + "type": { + "kind": "enum", + "variants": [ + { + "name": "UserRequested" }, { - "name": "pre", - "type": { - "defined": { - "name": "ConditionalOrderPingSnapshot" - } - } + "name": "AuthorityForced" }, { - "name": "post", - "type": { - "defined": { - "name": "ConditionalOrderPingSnapshot" - } - } + "name": "SelfTradeCancelProvide" }, { - "name": "bookOrderRemainingBaseLots", - "type": { - "option": { - "defined": { - "name": "BaseLots" - } - } - } + "name": "SelfTradeDecrementTake" + }, + { + "name": "Expired" + }, + { + "name": "ReduceOnlyInvalidated" + }, + { + "name": "RiskCapacityExceeded" + }, + { + "name": "BookCapacityEvicted" + }, + { + "name": "Tombstone" + }, + { + "name": "TopOfBookOutOfBounds" + }, + { + "name": "StopLossCascade" } ] } }, { - "name": "MarketAddedEvent", + "name": "ConditionalOrderPingStateSnapshot", "type": { "kind": "struct", - "fields": [] + "fields": [ + { + "name": "value", + "type": "u8" + } + ] } }, { - "name": "MarketStatusChangedEvent", + "name": "ConditionalOrderPingSnapshot", "type": { "kind": "struct", "fields": [ { - "name": "previousMarketStatus", + "name": "state", "type": { "defined": { - "name": "MarketStatus" + "name": "ConditionalOrderPingStateSnapshot" } } }, { - "name": "newMarketStatus", + "name": "attachedOrderId", "type": { "defined": { - "name": "MarketStatus" + "name": "OptionalFIFOOrderId" } } - } - ] - } - }, - { - "name": "MarketClosedEvent", - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "previousMarketStatus", + "name": "maxSize", "type": { "defined": { - "name": "MarketStatus" + "name": "BaseLots" } } }, { - "name": "finalizedMarkPrice", + "name": "fillableSize", "type": { "defined": { - "name": "Ticks" + "name": "BaseLots" + } + } + }, + { + "name": "filledSize", + "type": { + "defined": { + "name": "BaseLots" } } + }, + { + "name": "coPositionSequenceNumber", + "type": { + "option": "u8" + } } ] } }, { - "name": "MarketTombstonedEvent", + "name": "MarkPriceConfig", "type": { "kind": "struct", "fields": [ { - "name": "previousMarketStatus", - "type": { - "defined": { - "name": "MarketStatus" - } - } + "name": "emaPeriodSlots", + "type": "u64" }, { - "name": "finalSequenceNumber", + "name": "emaDiffRadius", "type": "u64" }, { - "name": "finalTradeSequenceNumber", + "name": "bookPriceRadius", "type": "u64" }, { - "name": "finalOrderSequenceNumber", + "name": "commoditiesAfterHoursRadius", "type": "u64" - } - ] - } - }, - { - "name": "ShutdownClosePositionsEvent", - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "longTrader", - "type": "pubkey" + "name": "spotPriceWeight", + "type": "u64" }, { - "name": "shortTrader", - "type": "pubkey" + "name": "bookPriceWeight", + "type": "u64" }, { - "name": "assetId", - "type": "u32" + "name": "perpPriceWeight", + "type": "u64" }, { - "name": "baseLotsClosed", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "spotPriceStaleThreshold", + "type": "u64" }, { - "name": "settlementPrice", + "name": "bookPriceStaleThreshold", + "type": "u64" + }, + { + "name": "perpPriceStaleThreshold", + "type": "u64" + }, + { + "name": "riskActionPriceValidityRules", "type": { - "defined": { - "name": "Ticks" - } + "array": [ + { + "array": [ + { + "array": [ + { + "defined": { + "name": "ValidationRule" + } + }, + 8 + ] + }, + 4 + ] + }, + 8 + ] } }, { - "name": "tradeSequenceNumber", - "type": "u64" + "name": "oracleDivergenceRadius", + "type": "u16" }, { - "name": "sequenceNumber", + "name": "minOracleResponses", + "type": "u8" + }, + { + "name": "commoditiesAfterHoursRadiusBps", "type": "u64" } ] } }, { - "name": "MarketDeletedEvent", + "name": "WithdrawConfig", "type": { "kind": "struct", "fields": [ { - "name": "assetId", - "type": "u32" + "name": "depositCooldownPeriodInSlots", + "type": "u64" }, { - "name": "lamportsReclaimed", + "name": "withdrawalFee", + "type": "u64" + }, + { + "name": "enqueueingFee", "type": "u64" } ] } }, { - "name": "ExchangeStatusChangedEvent", + "name": "WithdrawRateLimitConfig", "type": { "kind": "struct", "fields": [ { - "name": "previousBits", - "type": "u8" - }, - { - "name": "newBits", - "type": "u8" + "name": "maxBudget", + "type": "u64" }, { - "name": "authority", - "type": "pubkey" + "name": "replenishAmountPerSlot", + "type": "u64" } ] } }, { - "name": "MarketParametersUpdatedEvent", + "name": "FundingConfig", "type": { "kind": "struct", - "fields": [] + "fields": [ + { + "name": "fundingIntervalSeconds", + "type": "u64" + }, + { + "name": "fundingPeriodSeconds", + "type": "u64" + }, + { + "name": "maxFundingRate", + "type": "i64" + } + ] } }, { - "name": "FundingParametersUpdatedEvent", + "name": "CommodityMarketStateConfig", "type": { "kind": "struct", "fields": [ { - "name": "symbol", + "name": "marketState", "type": { "defined": { - "name": "Symbol" + "name": "CommodityMarketState" } } }, { - "name": "newFundingIntervalSeconds", - "type": { - "option": { - "defined": { - "name": "FundingRateUnitInSeconds" - } - } - } + "name": "isCommodity", + "type": "bool" }, { - "name": "newFundingPeriodSeconds", + "name": "lastKnownIndexPrice", "type": { "option": { "defined": { - "name": "FundingRateUnitInSeconds" + "name": "Ticks" } } } }, { - "name": "newMaxFundingRate", + "name": "lastIndexExpiryTimestamp", + "type": "u64" + }, + { + "name": "commoditiesAfterHoursRadius", "type": { - "option": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } + "defined": { + "name": "Ticks" } } } @@ -8000,751 +25897,839 @@ } }, { - "name": "FeesClaimedEvent", + "name": "MarketFeeConfig", "type": { "kind": "struct", "fields": [ { - "name": "amount", - "type": "u64" + "name": "defaultTakerFeeMicro", + "type": "u32" + }, + { + "name": "defaultMakerFeeMicro", + "type": "i32" } ] } }, { - "name": "SetPermissionEvent", + "name": "FeatureSet", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "type": "pubkey" - }, - { - "name": "user", - "type": "pubkey" - }, - { - "name": "previousPermission", - "type": "u64" - }, - { - "name": "newPermission", - "type": "u64" - }, - { - "name": "previousExpiresAtTimestamp", - "type": "i64" - }, - { - "name": "newExpiresAtTimestamp", - "type": "i64" - }, - { - "name": "previousNumSignerActionsRemaining", - "type": "i64" - }, - { - "name": "newNumSignerActionsRemaining", - "type": "i64" - }, - { - "name": "created", - "type": "bool" + "name": "bits", + "type": "u32" } ] } }, { - "name": "PricesUpdatedEvent", + "name": "SpotCollateralMetadata", "type": { "kind": "struct", "fields": [ { - "name": "oracleSigner", - "type": { - "option": "pubkey" - } - }, - { - "name": "assetSymbol", - "type": { - "defined": { - "name": "Symbol" - } - } + "name": "mintAddress", + "type": "pubkey" }, { - "name": "assetId", + "name": "decimals", "type": "u32" }, { - "name": "newBestBid", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } - } - }, - { - "name": "newBestAsk", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } - } - }, - { - "name": "newLastTrade", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } - } - }, - { - "name": "newExchangeSpotPrice", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } - } + "name": "perpAssetIndex", + "type": "u32" }, { - "name": "newExchangePerpPrice", - "type": { - "option": { - "defined": { - "name": "Ticks" - } - } - } + "name": "maxPerTraderBalance", + "type": "u64" }, { - "name": "newMidSpotDiffEmaTicks", - "type": { - "option": { - "defined": { - "name": "SignedTicks" - } - } - } + "name": "maxGlobalBalance", + "type": "u64" }, { - "name": "newMarkPrice", - "type": { - "defined": { - "name": "Ticks" - } - } + "name": "currGlobalBalance", + "type": "u64" }, { - "name": "cumulativeFundingRate", - "type": { - "option": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } - } - } + "name": "minMarginDiscount", + "type": "u32" }, { - "name": "settledContribution", - "type": { - "option": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" - } - } - } + "name": "maxMarginDiscount", + "type": "u32" }, { - "name": "intervalAccumulator", - "type": { - "option": { - "defined": { - "name": "SignedQuoteLotsPerBaseLotUpcasted" - } - } - } + "name": "maxLiquidationDiscount", + "type": "u32" }, { - "name": "assetSequenceNumber", - "type": "u64" + "name": "minLiquidationSlippage", + "type": "u32" }, { - "name": "prevAssetSequenceNumberSlot", + "name": "maxLiquidationSize", "type": "u64" - } - ] - } - }, - { - "name": "LiquidationEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "liquidator", - "type": "pubkey" - }, - { - "name": "liquidatedTrader", - "type": "pubkey" - }, - { - "name": "assetId", - "type": "u32" }, { - "name": "liquidationSize", + "name": "postLiquidationBuffer", "type": { "defined": { - "name": "BaseLots" + "name": "QuoteLots" } } }, { - "name": "markPrice", + "name": "quoteLotCollateralShortfallBuffer", "type": { "defined": { - "name": "Ticks" + "name": "QuoteLots" } } }, { - "name": "baseLotsFilled", - "type": { - "defined": { - "name": "BaseLots" - } - } + "name": "flags", + "type": "u8" }, { - "name": "quoteLotsFilled", + "name": "paddingFlags", "type": { - "defined": { - "name": "QuoteLots" - } + "array": [ + "u8", + 7 + ] } }, { - "name": "positionClosed", - "type": "bool" + "name": "padding", + "type": { + "array": [ + "u64", + 26 + ] + } } ] } }, { - "name": "LiquidationTransferSummaryEvent", + "name": "AdminParameterUpdateKind", "type": { - "kind": "struct", - "fields": [ + "kind": "enum", + "variants": [ { - "name": "liquidatee", - "type": "pubkey" + "name": "CancelRiskFactor", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": "u16" + }, + { + "name": "new", + "type": "u16" + } + ] }, { - "name": "liquidator", - "type": "pubkey" + "name": "IsolatedOnly", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": "bool" + }, + { + "name": "new", + "type": "bool" + } + ] }, { - "name": "totalTransfers", - "type": "u32" + "name": "LeverageTiers", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "LeverageTiers" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "LeverageTiers" + } + } + } + ] }, { - "name": "liquidateeCollateralChange", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "MarkPriceParameters", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "MarkPriceConfig" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "MarkPriceConfig" + } + } } - } + ] }, { - "name": "liquidatorCollateralChange", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "OpenInterestCap", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": "u64" + }, + { + "name": "new", + "type": "u64" } - } + ] }, { - "name": "haircutCollected", - "type": { - "defined": { - "name": "QuoteLots" + "name": "UpnlRiskFactor", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": "u16" + }, + { + "name": "new", + "type": "u16" } - } - } - ] - } - }, - { - "name": "LiquidationTransferEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "liquidatee", - "type": "pubkey" + ] }, { - "name": "liquidator", - "type": "pubkey" + "name": "UpnlRiskFactorForWithdrawals", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": "u16" + }, + { + "name": "new", + "type": "u16" + } + ] }, { - "name": "assetId", - "type": "u64" + "name": "WithdrawParameters", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "WithdrawConfig" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "WithdrawConfig" + } + } + } + ] }, { - "name": "baseLotsTransferred", - "type": { - "defined": { - "name": "SignedBaseLots" + "name": "WithdrawRateLimits", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "WithdrawRateLimitConfig" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "WithdrawRateLimitConfig" + } + } } - } + ] }, { - "name": "virtualQuoteLotsTransferred", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "TraderCapability", + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "previousFlags", + "type": { + "defined": { + "name": "TraderCapabilityFlags" + } + } + }, + { + "name": "newFlags", + "type": { + "defined": { + "name": "TraderCapabilityFlags" + } + } } - } + ] }, { - "name": "haircutRate", - "type": "u16" + "name": "FundingParameters", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "FundingConfig" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "FundingConfig" + } + } + } + ] }, { - "name": "liquidateeCollateralChange", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "MarketFees", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "MarketFeeConfig" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "MarketFeeConfig" + } + } } - } + ] }, { - "name": "liquidatorCollateralChange", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "OpenInterestAdjustment", + "kind": "struct", + "fields": [ + { + "name": "previousOpenInterest", + "type": "u64" + }, + { + "name": "newOpenInterest", + "type": "u64" } - } - } - ] - } - }, - { - "name": "CloseMatchedPositionsEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "caller", - "type": "pubkey" - }, - { - "name": "closedShort", - "type": "pubkey" - }, - { - "name": "closedLong", - "type": "pubkey" + ] }, { - "name": "inProfitAccount", - "type": "pubkey" + "name": "CommodityMarketState", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "CommodityMarketStateConfig" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "CommodityMarketStateConfig" + } + } + } + ] }, { - "name": "assetId", - "type": "u64" + "name": "MaxLiquidationSize", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": "u64" + }, + { + "name": "new", + "type": "u64" + } + ] }, { - "name": "baseLotsClosed", - "type": { - "defined": { - "name": "SignedBaseLots" + "name": "FeatureSet", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "FeatureSet" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "FeatureSet" + } + } } - } + ] }, { - "name": "atLossCloseValue", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "SpotCollateralConfig", + "kind": "struct", + "fields": [ + { + "name": "assetIndex", + "type": "u32" + }, + { + "name": "previous", + "type": { + "defined": { + "name": "SpotCollateralMetadata" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "SpotCollateralMetadata" + } + } } - } + ] }, { - "name": "inProfitCloseValue", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "RiskFactors", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "array": [ + { + "defined": { + "name": "BasisPointsU16" + } + }, + 3 + ] + } + }, + { + "name": "new", + "type": { + "array": [ + { + "defined": { + "name": "BasisPointsU16" + } + }, + 3 + ] + } } - } + ] }, { - "name": "atLossCollateralChange", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "TraderCapOverride", + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "assetIndex", + "type": "u32" + }, + { + "name": "previous", + "type": "u64" + }, + { + "name": "new", + "type": "u64" } - } + ] }, { - "name": "inProfitCollateralChange", - "type": { - "defined": { - "name": "SignedQuoteLots" + "name": "TransferFeeTiers", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "array": [ + { + "defined": { + "name": "TransferFeeTier" + } + }, + 4 + ] + } + }, + { + "name": "new", + "type": { + "array": [ + { + "defined": { + "name": "TransferFeeTier" + } + }, + 4 + ] + } } - } - } - ] - } - }, - { - "name": "NameSuccessorEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "pubkey" + ] }, { - "name": "newAuthority", - "type": "pubkey" + "name": "DisplayName", + "kind": "struct", + "fields": [ + { + "name": "previous", + "type": { + "defined": { + "name": "DisplayName" + } + } + }, + { + "name": "new", + "type": { + "defined": { + "name": "DisplayName" + } + } + } + ] } ] } }, { - "name": "ClaimAuthorityEvent", + "name": "EscrowAction", "type": { - "kind": "struct", - "fields": [ - { - "name": "previousAuthority", - "type": "pubkey" - }, + "kind": "enum", + "variants": [ { - "name": "newAuthority", - "type": "pubkey" + "name": "Noop", + "kind": "struct", + "fields": [ + { + "name": "padding0", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 128 + ] + } + } + ] + }, + { + "name": "Cash", + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + }, + { + "name": "padding0", + "type": { + "array": [ + "u8", + 128 + ] + } + } + ] } ] } }, { - "name": "AuthorityChangedEvent", + "name": "EscrowRequestCancelReason", "type": { - "kind": "struct", - "fields": [ + "kind": "enum", + "variants": [ { - "name": "previousAuthority", - "type": "pubkey" + "name": "Expiration" }, { - "name": "newAuthority", - "type": "pubkey" + "name": "CancelledBySender" }, { - "name": "authorityType", - "type": { - "defined": { - "name": "AuthorityType" - } - } + "name": "CancelledByReceiver" } ] } }, { - "name": "WithdrawStateTransitionEvent", + "name": "SpotCollateralCap", "type": { - "kind": "struct", - "fields": [ - { - "name": "trader", - "type": "pubkey" - }, + "kind": "enum", + "variants": [ { - "name": "amount", - "type": { - "defined": { - "name": "QuoteLots" - } - } + "name": "PerTrader" }, { - "name": "fromState", - "type": "u8" - }, + "name": "Global" + } + ] + } + }, + { + "name": "SpotCollateralFlow", + "type": { + "kind": "enum", + "variants": [ { - "name": "toState", - "type": "u8" + "name": "Sync" }, { - "name": "reason", - "type": "u8" + "name": "Swap" }, { - "name": "transitionCount", - "type": "u16" + "name": "Transfer" }, { - "name": "nodeIndex", - "type": { - "defined": { - "name": "NodePointer" - } - } + "name": "Withdraw" } ] } }, { - "name": "PnLEvent", + "name": "SpotCollateralDepositedEvent", "type": { "kind": "struct", "fields": [ + { + "name": "assetIndex", + "type": "u32" + }, { "name": "trader", "type": "pubkey" }, { - "name": "assetId", - "type": "u32" + "name": "authority", + "type": "pubkey" }, { - "name": "assetSymbol", - "type": { - "defined": { - "name": "Symbol" - } - } + "name": "amount", + "type": "u64" }, { - "name": "realizedPnl", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "requestedAmount", + "type": "u64" }, { - "name": "fundingPayment", + "name": "bindingCap", "type": { - "defined": { - "name": "SignedQuoteLots" + "option": { + "defined": { + "name": "SpotCollateralCap" + } } } }, { - "name": "baseLotsBefore", + "name": "flow", "type": { "defined": { - "name": "SignedBaseLots" + "name": "SpotCollateralFlow" } } }, { - "name": "baseLotsAfter", - "type": { - "defined": { - "name": "SignedBaseLots" - } - } + "name": "newCollateralBalance", + "type": "u64" }, { - "name": "virtualQuoteLotsBefore", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "postGlobalCollateral", + "type": "u64" }, { - "name": "virtualQuoteLotsAfter", - "type": { - "defined": { - "name": "SignedQuoteLots" - } - } + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" } ] } }, { - "name": "AdminParameterUpdatedEvent", + "name": "SpotCollateralWithdrawnEvent", "type": { "kind": "struct", "fields": [ + { + "name": "assetIndex", + "type": "u32" + }, + { + "name": "trader", + "type": "pubkey" + }, { "name": "authority", "type": "pubkey" }, { - "name": "assetSymbol", - "type": { - "option": { - "defined": { - "name": "Symbol" - } - } - } + "name": "destination", + "type": "pubkey" }, { - "name": "assetId", - "type": { - "option": "u32" - } + "name": "amount", + "type": "u64" }, { - "name": "updateKind", + "name": "excess", + "type": "u64" + }, + { + "name": "withdrawExcess", + "type": "bool" + }, + { + "name": "isSelfCpi", + "type": "bool" + }, + { + "name": "flow", "type": { "defined": { - "name": "AdminParameterUpdateKind" + "name": "SpotCollateralFlow" } } - } - ] - } - }, - { - "name": "EscrowAccountCreatedEvent", - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "authority", - "type": "pubkey" + "name": "newCollateralBalance", + "type": "u64" }, { - "name": "capacity", + "name": "postGlobalCollateral", + "type": "u64" + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", "type": "u64" } ] } }, { - "name": "EscrowRequestCreatedEvent", + "name": "SpotCollateralLiquidatedEvent", "type": { "kind": "struct", "fields": [ { - "name": "receiverAuthority", + "name": "liquidator", "type": "pubkey" }, { - "name": "senderAuthority", + "name": "liquidatedTrader", "type": "pubkey" }, { - "name": "sequenceNumber", + "name": "assetIndex", + "type": "u32" + }, + { + "name": "liquidationSize", "type": "u64" }, { - "name": "senderPdaIndex", - "type": "u8" + "name": "overCapExcess", + "type": "u64" }, { - "name": "senderSubaccountIndex", - "type": "u8" - }, - { - "name": "receiverPdaIndex", - "type": "u8" - }, - { - "name": "receiverSubaccountIndex", - "type": "u8" - }, - { - "name": "expirationOffset", - "type": "u32" - }, - { - "name": "initialSlot", - "type": "u64" - }, - { - "name": "actions", + "name": "quoteLotsDeposited", "type": { - "array": [ - { - "defined": { - "name": "EscrowAction" - } - }, - 4 - ] + "defined": { + "name": "QuoteLots" + } } - } - ] - } - }, - { - "name": "EscrowRequestAcceptedEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "receiverAuthority", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" - } - ] - } - }, - { - "name": "EscrowRequestCancelledEvent", - "type": { - "kind": "struct", - "fields": [ - { - "name": "receiverAuthority", - "type": "pubkey" - }, - { - "name": "sequenceNumber", - "type": "u64" }, { - "name": "reason", + "name": "oracleNotional", "type": { "defined": { - "name": "EscrowRequestCancelReason" + "name": "QuoteLots" } } - } - ] - } - }, - { - "name": "ChangeMarketStatusParams", - "docs": [ - "Borsh payload for changing an orderbook market status." - ], - "type": { - "kind": "struct", - "fields": [ + }, { - "name": "nextMarketStatus", + "name": "liquidationDiscount", "type": { "defined": { - "name": "MarketStatus" + "name": "BasisPoints" } } } @@ -8752,1246 +26737,1275 @@ } }, { - "name": "ChangeExchangeStatusParams", - "docs": [ - "Borsh payload patching the exchange-wide status bits; `None` preserves a bit." - ], + "name": "RestartAcknowledgedEvent", "type": { "kind": "struct", "fields": [ { - "name": "active", - "type": { - "option": "bool" - } + "name": "previousRestartSlot", + "type": "u64" }, { - "name": "gated", - "type": { - "option": "bool" - } + "name": "newRestartSlot", + "type": "u64" }, { - "name": "maintenance", - "type": { - "option": "bool" - } + "name": "previousStatusBits", + "type": "u8" + }, + { + "name": "newStatusBits", + "type": "u8" } ] } }, { - "name": "AuthorizedForceCancelParams", - "docs": [ - "Borsh payload for authority-driven force cancellation by remaining side size." - ], + "name": "SpotAssetPriceUpdatedEvent", "type": { "kind": "struct", "fields": [ { - "name": "remainingBidBaseLots", + "name": "assetSymbol", "type": { "defined": { - "name": "BaseLots" + "name": "Symbol" } } }, { - "name": "remainingAskBaseLots", + "name": "assetIndex", + "type": "u32" + }, + { + "name": "price", "type": { "defined": { - "name": "BaseLots" + "name": "Ticks" } } + }, + { + "name": "priceSequenceNumber", + "type": "u64" + }, + { + "name": "prevPriceSequenceNumberSlot", + "type": "u64" } ] } }, { - "name": "AuthorizedForceCancelByIdParams", + "name": "MarketEvent", "docs": [ - "Borsh payload for authority-driven force cancellation by order id." + "Borsh enum emitted by Eternal Log instructions. Variant discriminators are one-byte ordinals." ], "type": { - "kind": "struct", - "fields": [ + "kind": "enum", + "variants": [ { - "name": "bidOrderIds", - "type": { - "vec": { - "defined": { - "name": "CancelId" + "name": "SlotContext", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SlotContextEvent" + } } } - } + ] }, { - "name": "askOrderIds", - "type": { - "vec": { - "defined": { - "name": "CancelId" + "name": "Header", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketEventHeader" + } } } - } - } - ] - } - }, - { - "name": "CreateEscrowAccountParams", - "docs": [ - "Borsh payload for creating an escrow account." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "capacity", - "type": "u64" - } - ] - } - }, - { - "name": "EscrowParticipantMetadata", - "docs": [ - "Trader PDA/subaccount metadata stored with escrow requests." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "OrderPlaced", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "OrderPlacedEvent" + } + } + } + ] + }, { - "name": "senderPdaIndex", - "type": "u8" + "name": "OrderFilled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "OrderFilledEvent" + } + } + } + ] }, { - "name": "senderSubaccountIndex", - "type": "u8" + "name": "OrderRejected", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "OrderRejectedEvent" + } + } + } + ] }, { - "name": "receiverPdaIndex", - "type": "u8" + "name": "SplineFilled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplineFilledEvent" + } + } + } + ] }, { - "name": "receiverSubaccountIndex", - "type": "u8" - } - ] - } - }, - { - "name": "CreateEscrowRequestParams", - "docs": [ - "Borsh payload for creating an escrow request." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "participantMetadata", - "type": { - "defined": { - "name": "EscrowParticipantMetadata" + "name": "TradeSummary", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TradeSummaryEvent" + } + } } - } + ] }, { - "name": "lastValidSlot", - "type": { - "option": "u64" - } + "name": "OrderModified", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "OrderModifiedEvent" + } + } + } + ] }, { - "name": "actions", - "type": { - "vec": { - "defined": { - "name": "EscrowAction" + "name": "MarketSummary", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketSummaryEvent" + } } } - } - } - ] - } - }, - { - "name": "CancelEscrowRequestParams", - "docs": [ - "Borsh payload for cancelling an escrow request by sequence number." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": "u64" - } - ] - } - }, - { - "name": "UpsertEscrowRequestParams", - "docs": [ - "Borsh payload for upserting an escrow request." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "participantMetadata", - "type": { - "defined": { - "name": "EscrowParticipantMetadata" + "name": "TraderRegistered", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderRegisteredEvent" + } + } } - } + ] }, { - "name": "action", - "type": { - "defined": { - "name": "EscrowAction" + "name": "TraderCollateralTransferred", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderCollateralTransferredEvent" + } + } } - } - } - ] - } - }, - { - "name": "RegisterSplineParams", - "docs": [ - "Empty Borsh payload for registering a spline trader." - ], - "type": { - "kind": "struct", - "fields": [] - } - }, - { - "name": "UpdateCommodityMarketStateInstruction", - "docs": [ - "Borsh payload for updating commodity market state and mark price inputs." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "marketState", - "type": { - "defined": { - "name": "CommodityMarketState" + "name": "TraderActivated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderActivatedEvent" + } + } } - } + ] }, { - "name": "lastKnownIndexPrice", - "type": { - "option": { - "defined": { - "name": "Ticks" + "name": "TraderDeactivated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderDeactivatedEvent" + } } } - } + ] }, { - "name": "lastIndexExpiryTimestamp", - "type": "u64" + "name": "TraderFundsDeposited", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFundsDepositedEvent" + } + } + } + ] }, { - "name": "emaPeriodSlots", - "type": { - "option": "u64" - } - } - ] - } - }, - { - "name": "UpdatePerpIsolatedOnlyInstruction", - "docs": [ - "Borsh payload for updating isolated-only metadata for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "TraderFundsWithdrawn", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFundsWithdrawnEvent" + } + } } - } + ] }, { - "name": "isolatedOnly", - "type": "u8" - } - ] - } - }, - { - "name": "UpdatePerpLeverageTiersInstruction", - "docs": [ - "Borsh payload for updating leverage tiers for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "TraderFundsWithdrawnEnqueued", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFundsWithdrawnEvent" + } + } } - } + ] }, { - "name": "leverageTiers", - "type": { - "defined": { - "name": "LeverageTiers" + "name": "TraderFundsWithdrawnDropped", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFundsWithdrawnEvent" + } + } } - } - } - ] - } - }, - { - "name": "UpdatePerpOpenInterestCapInstruction", - "docs": [ - "Borsh payload for updating open interest cap for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "TraderWithdrawCancelled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderWithdrawCancelledEvent" + } + } } - } + ] }, { - "name": "openInterestCap", - "type": { - "defined": { - "name": "BaseLots" + "name": "TraderFundingSettled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFundingSettledEvent" + } + } } - } - } - ] - } - }, - { - "name": "UPnlRiskFactor", - "docs": [ - "uPnL risk factor encoded as basis points." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "inner", - "type": "u64" - } - ] - } - }, - { - "name": "UpdatePerpMaxLiquidationSizeInstruction", - "docs": [ - "Borsh payload for updating max liquidation size for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "SplineRegistered", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplineRegisteredEvent" + } + } } - } + ] }, { - "name": "maxLiquidationSize", - "type": { - "defined": { - "name": "BaseLots" + "name": "SplineActivated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplineActivatedEvent" + } + } } - } - } - ] - } - }, - { - "name": "UpdatePerpRiskFactorsInstruction", - "docs": [ - "Borsh payload for updating risk factors for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "SplineDeactivated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplineDeactivatedEvent" + } + } } - } + ] }, { - "name": "maintenanceRiskFactor", - "type": { - "defined": { - "name": "BasisPointsU16" + "name": "SplinePriceUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplinePriceUpdatedEvent" + } + } } - } + ] }, { - "name": "backstopRiskFactor", - "type": { - "defined": { - "name": "BasisPointsU16" + "name": "SplineParametersUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplineParametersUpdatedEvent" + } + } } - } + ] }, { - "name": "highRiskRiskFactor", - "type": { - "defined": { - "name": "BasisPointsU16" + "name": "MarketAdded", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketAddedEvent" + } + } } - } - } - ] - } - }, - { - "name": "UpdatePerpTransferFeeTiersInstruction", - "docs": [ - "Borsh payload for updating transfer fee tiers for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "MarketStatusChanged", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketStatusChangedEvent" + } + } } - } + ] }, { - "name": "transferFeeTiers", - "type": { - "array": [ - { + "name": "MarketParametersUpdated", + "fields": [ + { + "name": "value", + "type": { "defined": { - "name": "TransferFeeTier" + "name": "MarketParametersUpdatedEvent" } - }, - 4 - ] - } - } - ] - } - }, - { - "name": "TransferFeeTier", - "docs": [ - "Liquidation-transfer haircut tier keyed by position size." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "positionSizeLimit", - "type": { - "defined": { - "name": "BaseLots" + } } - } + ] }, { - "name": "feeRate", - "type": "u16" + "name": "FundingParametersUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "FundingParametersUpdatedEvent" + } + } + } + ] }, { - "name": "padding", - "type": { - "array": [ - "u8", - 6 - ] - } - } - ] - } - }, - { - "name": "UpdatePerpUPnlRiskFactorInstruction", - "docs": [ - "Borsh payload for updating uPnL risk factor for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "FeesClaimed", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "FeesClaimedEvent" + } + } } - } + ] }, { - "name": "upnlRiskFactor", - "type": { - "defined": { - "name": "UPnlRiskFactor" - } - } - } - ] - } - }, - { - "name": "UpdatePerpUPnlRiskFactorForWithdrawalsInstruction", - "docs": [ - "Borsh payload for updating withdrawal uPnL risk factor for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "PricesUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "PricesUpdatedEvent" + } + } } - } + ] }, { - "name": "upnlRiskFactorForWithdrawals", - "type": { - "defined": { - "name": "UPnlRiskFactor" + "name": "LiquidationTransferSummary", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "LiquidationTransferSummaryEvent" + } + } } - } - } - ] - } - }, - { - "name": "UpdatePerpCancelRiskFactorInstruction", - "docs": [ - "Borsh payload for updating cancel-order risk factor for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "LiquidationTransfer", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "LiquidationTransferEvent" + } + } } - } + ] }, { - "name": "cancelOrderRiskFactor", - "type": "u16" - } - ] - } - }, - { - "name": "UpdatePerpMarkPriceParametersInstruction", - "docs": [ - "Borsh payload for updating mark price calculation parameters for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "Liquidation", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "LiquidationEvent" + } + } } - } + ] }, { - "name": "emaPeriodSlots", - "type": { - "option": "u64" - } + "name": "CloseMatchedPositions", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "CloseMatchedPositionsEvent" + } + } + } + ] }, { - "name": "emaDiffRadius", - "type": { - "option": "u64" - } + "name": "NameSuccessor", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "NameSuccessorEvent" + } + } + } + ] }, { - "name": "bookPriceRadius", - "type": { - "option": "u64" - } + "name": "ClaimAuthority", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "ClaimAuthorityEvent" + } + } + } + ] }, { - "name": "commoditiesAfterHoursRadiusBps", - "type": { - "option": "u64" - } + "name": "WithdrawStateTransition", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "WithdrawStateTransitionEvent" + } + } + } + ] }, { - "name": "adjustedExchangeSpotPriceWeight", - "type": { - "option": "u64" - } + "name": "PnL", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "PnLEvent" + } + } + } + ] }, { - "name": "bookPriceWeight", - "type": { - "option": "u64" - } + "name": "StopLossPlaced", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "StopLossPlacedEvent" + } + } + } + ] }, { - "name": "exchangePerpPriceWeight", - "type": { - "option": "u64" - } + "name": "StopLossCancelled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "StopLossCancelledEvent" + } + } + } + ] }, { - "name": "spotPriceStaleThreshold", - "type": { - "option": "u64" - } + "name": "StopLossExecuted", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "StopLossExecutedEvent" + } + } + } + ] }, { - "name": "bookPriceStaleThreshold", - "type": { - "option": "u64" - } + "name": "ExchangeStatusChanged", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "ExchangeStatusChangedEvent" + } + } + } + ] }, { - "name": "perpPriceStaleThreshold", - "type": { - "option": "u64" - } + "name": "TraderCapabilitiesEnabled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderCapabilitiesEnabledEvent" + } + } + } + ] }, { - "name": "riskActionPriceValidityRules", - "type": { - "option": { - "array": [ - { - "array": [ - { - "array": [ - { - "defined": { - "name": "ValidationRule" - } - }, - 8 - ] - }, - 4 - ] - }, - 8 - ] + "name": "TraderFundsWithdrawnFeePayment", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFundsWithdrawnFeePaymentEvent" + } + } } - } + ] }, { - "name": "oracleDivergenceRadius", - "type": { - "option": "u16" - } + "name": "OrderPacket", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "OrderPacketEvent" + } + } + } + ] }, { - "name": "minOracleResponses", - "type": { - "option": "u8" - } - } - ] - } - }, - { - "name": "UpdatePerpParametersDelegatedInstruction", - "docs": [ - "Borsh payload for maintaining price-dependent parameters for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "SetPermission", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SetPermissionEvent" + } + } } - } + ] }, { - "name": "emaDiffRadius", - "type": { - "option": "u64" - } + "name": "AuthorityChanged", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "AuthorityChangedEvent" + } + } + } + ] }, { - "name": "bookPriceRadius", - "type": { - "option": "u64" - } + "name": "TraderDelegated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderDelegatedEvent" + } + } + } + ] }, { - "name": "maxFundingRate", - "type": { - "option": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" + "name": "AdminParameterUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "AdminParameterUpdatedEvent" + } } } - } - } - ] - } - }, - { - "name": "UpdateTraderFeesParams", - "docs": [ - "Borsh payload for updating trader fee override multipliers." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "makerFeeOverrideMultiplier", - "type": "i8" + ] }, { - "name": "takerFeeOverrideMultiplier", - "type": "i8" - } - ] - } - }, - { - "name": "UpdateMarketFeesParams", - "docs": [ - "Borsh payload for updating market default fees." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "TraderFeesUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TraderFeesUpdatedEvent" + } + } + } + ] + }, { - "name": "defaultTakerFeeMicro", - "type": { - "option": "u32" - } + "name": "MarketClosed", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketClosedEvent" + } + } + } + ] }, { - "name": "defaultMakerFeeMicro", - "type": { - "option": "i32" - } - } - ] - } - }, - { - "name": "UpdateWithdrawRateLimitsParams", - "docs": [ - "Borsh payload for updating withdraw throttle rate limits." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "MarketDeleted", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketDeletedEvent" + } + } + } + ] + }, { - "name": "maxBudget", - "type": { - "option": "u64" - } + "name": "EscrowAccountCreated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "EscrowAccountCreatedEvent" + } + } + } + ] }, { - "name": "replenishAmountPerSlot", - "type": { - "option": "u64" - } - } - ] - } - }, - { - "name": "UpdateWithdrawParametersParams", - "docs": [ - "Borsh payload for updating withdraw cooldown and fee parameters." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "EscrowRequestCreated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "EscrowRequestCreatedEvent" + } + } + } + ] + }, { - "name": "depositCooldownPeriodInSlots", - "type": { - "option": "u64" - } + "name": "EscrowRequestAccepted", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "EscrowRequestAcceptedEvent" + } + } + } + ] }, { - "name": "withdrawalFee", - "type": { - "option": "u64" - } + "name": "EscrowRequestCancelled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "EscrowRequestCancelledEvent" + } + } + } + ] }, { - "name": "enqueueingFee", - "type": { - "option": "u64" - } - } - ] - } - }, - { - "name": "UpdateAuthoritiesParams", - "docs": [ - "Borsh payload for root authority updates to subordinate authorities." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "SplinePriceUpdatedWithOrdering", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplinePriceUpdatedWithOrderingEvent" + } + } + } + ] + }, { - "name": "newRiskAuthority", - "type": { - "option": "pubkey" - } + "name": "SplineParametersUpdatedWithOrdering", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplineParametersUpdatedWithOrderingEvent" + } + } + } + ] }, { - "name": "newMarketAuthority", - "type": { - "option": "pubkey" - } + "name": "TriggerOrderPlaced", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TriggerOrderPlacedEvent" + } + } + } + ] }, { - "name": "newOracleAuthority", - "type": { - "option": "pubkey" - } + "name": "TriggerOrderCancelled", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TriggerOrderCancelledEvent" + } + } + } + ] }, { - "name": "newCancelAuthority", - "type": { - "option": "pubkey" - } + "name": "TriggerOrderExecuted", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "TriggerOrderExecutedEvent" + } + } + } + ] }, { - "name": "newBackstopAuthority", - "type": { - "option": "pubkey" - } + "name": "PingInvalidated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "PingInvalidatedEvent" + } + } + } + ] }, { - "name": "newAdlAuthority", - "type": { - "option": "pubkey" - } - } - ] - } - }, - { - "name": "ArenaType", - "docs": [ - "Arena account family to initialize." - ], - "type": { - "kind": "enum", - "variants": [ + "name": "PingActivated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "PingActivatedEvent" + } + } + } + ] + }, { - "name": "GlobalTraderIndex" + "name": "SplinePositionLimitsConfigUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SplinePositionLimitsConfigUpdatedEvent" + } + } + } + ] }, { - "name": "ActiveTraderBuffer" - } - ] - } - }, - { - "name": "InitializeArenaParams", - "docs": [ - "Borsh payload for initializing a new arena account." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "MarketTombstoned", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "MarketTombstonedEvent" + } + } + } + ] + }, { - "name": "arenaType", - "type": { - "defined": { - "name": "ArenaType" + "name": "ShutdownClosePositions", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "ShutdownClosePositionsEvent" + } + } } - } - } - ] - } - }, - { - "name": "RemoveOracleInstruction", - "docs": [ - "Borsh payload for removing oracle public keys from the perp asset map." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "oracleKeys", - "type": { - "vec": "pubkey" - } - } - ] - } - }, - { - "name": "RemoveAllOraclesInstruction", - "docs": [ - "Borsh payload for removing all oracle keys for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + "name": "OrderResidualDiscarded", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "OrderResidualDiscardedEvent" + } + } + } + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "SpotCollateralDeposited", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SpotCollateralDepositedEvent" + } + } } - } - } - ] - } - }, - { - "name": "UpdateFundingParametersInstruction", - "docs": [ - "Borsh payload for updating funding parameters for a perp asset." - ], - "type": { - "kind": "struct", - "fields": [ + ] + }, { - "name": "perpAssetSymbol", - "type": { - "defined": { - "name": "Symbol" + "name": "SpotCollateralWithdrawn", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SpotCollateralWithdrawnEvent" + } + } } - } + ] }, { - "name": "fundingIntervalSeconds", - "type": { - "option": { - "defined": { - "name": "FundingRateUnitInSeconds" + "name": "SpotCollateralLiquidated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SpotCollateralLiquidatedEvent" + } } } - } + ] }, { - "name": "fundingPeriodSeconds", - "type": { - "option": { - "defined": { - "name": "FundingRateUnitInSeconds" + "name": "RestartAcknowledged", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "RestartAcknowledgedEvent" + } } } - } + ] }, { - "name": "maxFundingRate", - "type": { - "option": { - "defined": { - "name": "SignedQuoteLotsPerBaseLot" + "name": "AdditionalFeeAccrued", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "AdditionalFeeAccruedEvent" + } } } - } + ] + }, + { + "name": "SpotAssetPriceUpdated", + "fields": [ + { + "name": "value", + "type": { + "defined": { + "name": "SpotAssetPriceUpdatedEvent" + } + } + } + ] } ] } }, { - "name": "UpdateSplinePositionLimitsConfigParams", - "docs": [ - "Borsh payload for updating spline position-size limits and leverage decrease configuration." - ], + "name": "SlotContextEvent", "type": { "kind": "struct", "fields": [ { - "name": "maxPositionSize", - "type": { - "option": { - "defined": { - "name": "PositionSizeLimit" - } - } - } + "name": "timestamp", + "type": "u64" }, { - "name": "leverageDecreaseInBps", - "type": { - "option": "u32" - } + "name": "slot", + "type": "u64" } ] } }, { - "name": "TraderFundsWithdrawnEnqueuedEvent", - "docs": [ - "Viewer-safe alias for MarketEvent::TraderFundsWithdrawnEnqueued.", - "Uses the same payload layout as TraderFundsWithdrawnEvent." - ], + "name": "MarketEventHeader", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" + "name": "sequenceNumber", + "type": "u64" }, { - "name": "amount", + "name": "prevSequenceNumberSlot", "type": "u64" }, { - "name": "traderSequenceNumber", - "type": "u64" + "name": "assetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } }, { - "name": "traderPrevSequenceNumberSlot", - "type": "u64" + "name": "assetId", + "type": "u32" }, { - "name": "postWithdrawalBudget", - "type": "u64" + "name": "tickSize", + "type": "u32" }, { - "name": "postQueueSize", - "type": "u64" + "name": "baseLotDecimals", + "type": "i8" }, { - "name": "totalQueuedAmount", - "type": "u64" + "name": "quoteLotDecimals", + "type": "u8" }, { - "name": "withdrawQueueSequenceNumber", - "type": "u64" + "name": "signer", + "type": "pubkey" }, { - "name": "withdrawQueuePrevSequenceNumberSlot", - "type": "u64" + "name": "traderAccount", + "type": "pubkey" } ] } }, { - "name": "TraderFundsWithdrawnDroppedEvent", - "docs": [ - "Viewer-safe alias for MarketEvent::TraderFundsWithdrawnDropped.", - "Uses the same payload layout as TraderFundsWithdrawnEvent." - ], + "name": "OrderPlacedEvent", "type": { "kind": "struct", "fields": [ { - "name": "trader", - "type": "pubkey" - }, - { - "name": "authority", - "type": "pubkey" + "name": "orderId", + "type": "u32" }, { - "name": "amount", - "type": "u64" + "name": "orderFlags", + "type": { + "defined": { + "name": "OrderFlags" + } + } }, { - "name": "traderSequenceNumber", + "name": "orderSequenceNumber", "type": "u64" }, { - "name": "traderPrevSequenceNumberSlot", + "name": "prevOrderSequenceNumberSlot", "type": "u64" }, { - "name": "postWithdrawalBudget", - "type": "u64" + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } }, { - "name": "postQueueSize", - "type": "u64" + "name": "price", + "type": { + "defined": { + "name": "Ticks" + } + } }, { - "name": "totalQueuedAmount", - "type": "u64" + "name": "quantity", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } }, { - "name": "withdrawQueueSequenceNumber", - "type": "u64" + "name": "lastValidSlot", + "type": { + "defined": { + "name": "OptionalNonZeroU64" + } + } }, { - "name": "withdrawQueuePrevSequenceNumberSlot", + "name": "initialSlot", "type": "u64" } ] } }, { - "name": "SetSpotAssetPriceInstruction", - "docs": [ - "Borsh payload for recording a spot-collateral price. The exponent must equal the", - "asset's configured price exponent; the symbol is cross-checked against the entry", - "at the asset index." - ], + "name": "OrderPacketEvent", "type": { "kind": "struct", "fields": [ { - "name": "symbol", + "name": "orderPacket", "type": { "defined": { - "name": "Symbol" + "name": "OrderPacket" } } }, { - "name": "assetIndex", - "type": "u32" + "name": "trader", + "type": "pubkey" }, { - "name": "price", - "type": { - "defined": { - "name": "Price" - } - } + "name": "nextOrderSequenceNumber", + "type": "u64" } ] } }, { - "name": "ChangeSpotAssetParamsInstruction", - "docs": [ - "Borsh payload updating selected fields in a spot asset's oracle-price configuration;", - "Some(default pubkey) disables proxy price writes and None leaves that field unchanged." - ], + "name": "OrderRejectedEvent", "type": { "kind": "struct", "fields": [ { - "name": "assetIndex", + "name": "orderIndex", "type": "u32" }, { - "name": "proxyAuthority", + "name": "clientOrderId", "type": { - "option": "pubkey" + "array": [ + "u8", + 16 + ] } }, { - "name": "priceExpo", + "name": "price", "type": { - "option": "u8" + "defined": { + "name": "Ticks" + } } }, { - "name": "staleThresholdSlots", + "name": "side", "type": { - "option": "u64" + "defined": { + "name": "Side" + } } - } - ] - } - }, - { - "name": "TransferNativeSolInstruction", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - } - ] - } - }, - { - "name": "LiquidateNativeSolParams", - "type": { - "kind": "struct", - "fields": [ - { - "name": "maxNativeSolAmount", - "type": "u64" }, { - "name": "numTradersToCheck", - "type": "u64" + "name": "numBaseLots", + "type": { + "defined": { + "name": "BaseLots" + } + } }, { - "name": "packedInstructions", + "name": "reason", "type": { - "vec": { - "defined": { - "name": "PackedInstruction" - } - } + "array": [ + "u8", + 32 + ] } } ] } }, { - "name": "SwapNativeInstruction", + "name": "OrderResidualDiscardedEvent", "type": { "kind": "struct", "fields": [ { - "name": "direction", + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } + }, + { + "name": "price", "type": { "defined": { - "name": "SwapDirection" + "name": "Ticks" } } }, { - "name": "amountIn", - "type": "u64" + "name": "side", + "type": { + "defined": { + "name": "Side" + } + } }, { - "name": "minAmountOut", - "type": "u64" + "name": "baseLotsDiscarded", + "type": { + "defined": { + "name": "BaseLots" + } + } }, { - "name": "packedInstructions", + "name": "reason", "type": { - "vec": { - "defined": { - "name": "PackedInstruction" - } + "defined": { + "name": "OrderResidualDiscardReason" } } } @@ -9999,465 +28013,5714 @@ } }, { - "name": "ConfigureNativeSolParams", + "name": "OrderFilledEvent", "type": { "kind": "struct", "fields": [ { - "name": "maxPerTraderBalance", + "name": "orderSequenceNumber", + "type": "u64" + }, + { + "name": "side", "type": { - "option": "u64" + "defined": { + "name": "Side" + } } }, { - "name": "maxGlobalBalance", + "name": "price", "type": { - "option": "u64" + "defined": { + "name": "Ticks" + } } }, { - "name": "minMarginDiscount", + "name": "baseLotsFilled", "type": { - "option": "u32" + "defined": { + "name": "BaseLots" + } } }, { - "name": "maxMarginDiscount", + "name": "quoteLotsFilled", "type": { - "option": "u32" + "defined": { + "name": "QuoteLots" + } } }, { - "name": "maxLiquidationDiscount", + "name": "quantityRemaining", "type": { - "option": "u32" + "defined": { + "name": "BaseLots" + } } }, { - "name": "minLiquidationSlippage", + "name": "maker", + "type": "pubkey" + }, + { + "name": "makerFeeRate", "type": { - "option": "u32" + "defined": { + "name": "SignedFeeRateMicro" + } } }, { - "name": "maxLiquidationSize", + "name": "makerBaseLotPosition", "type": { - "option": "u64" + "defined": { + "name": "SignedBaseLots" + } } }, { - "name": "postLiquidationBuffer", + "name": "makerVirtualQuoteLotPosition", "type": { - "option": "u64" + "defined": { + "name": "SignedQuoteLots" + } } }, { - "name": "quoteLotCollateralShortfallBuffer", + "name": "makerQuoteLotCollateral", "type": { - "option": "u64" + "defined": { + "name": "SignedQuoteLots" + } } }, { - "name": "disablePositionAuthoritySwap", + "name": "makerCumulativeFundingSnapshot", "type": { - "option": "bool" + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } } } ] } }, { - "name": "PackedAccountMeta", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": "u8" - } - ] - } - }, - { - "name": "PackedInstruction", + "name": "SplineFilledEvent", "type": { "kind": "struct", "fields": [ { - "name": "programIdIndex", - "type": "u8" + "name": "splineSequenceNumber", + "type": "u64" }, { - "name": "data", + "name": "side", "type": { - "vec": "u8" + "defined": { + "name": "Side" + } } }, { - "name": "accountMetas", + "name": "price", "type": { - "vec": { - "defined": { - "name": "PackedAccountMeta" - } + "defined": { + "name": "Ticks" } } - } - ] - } - }, - { - "name": "SwapDirection", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Sell" }, { - "name": "Buy" - } - ] - } - }, - { - "name": "WithdrawNativeSolAction", - "type": { - "kind": "enum", - "variants": [ - { - "name": "AllExcess" + "name": "baseLotsFilled", + "type": { + "defined": { + "name": "BaseLots" + } + } }, { - "name": "WithExcess", - "fields": [ - { - "name": "amount", - "type": "u64" + "name": "quoteLotsFilled", + "type": { + "defined": { + "name": "QuoteLots" } - ] + } }, { - "name": "WithoutExcess", - "fields": [ - { - "name": "amount", - "type": "u64" - } - ] - } - ] - } - }, - { - "name": "RebirthParams", - "type": { - "kind": "struct", - "fields": [ + "name": "maker", + "type": "pubkey" + }, { - "name": "globalTraderIndexNumNodesPerArena", - "type": "u32" + "name": "makerFeeRate", + "type": { + "defined": { + "name": "SignedFeeRateMicro" + } + } }, { - "name": "activeTraderBufferNumNodesPerArena", - "type": "u32" + "name": "makerBaseLotPosition", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } }, { - "name": "multiArenaNumAdditionalNodes", - "type": "u32" + "name": "makerVirtualQuoteLotPosition", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } }, { - "name": "withdrawThrottleMaxBudget", - "type": "u64" + "name": "makerQuoteLotCollateral", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } }, { - "name": "withdrawThrottleReplenishAmountPerSlot", - "type": "u64" + "name": "makerCumulativeFundingSnapshot", + "type": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } } ] } }, { - "name": "AddMarketParams", + "name": "OrderModifiedEvent", "type": { "kind": "struct", "fields": [ { - "name": "perpAssetSymbol", + "name": "orderSequenceNumber", + "type": "u64" + }, + { + "name": "price", "type": { "defined": { - "name": "Symbol" + "name": "Ticks" } } }, { - "name": "maxLeverageTiers", + "name": "baseLotsReleased", "type": { "defined": { - "name": "LeverageTiers" + "name": "SignedBaseLots" } } }, { - "name": "isolatedOnly", - "type": "u8" - }, - { - "name": "isCommodity", - "type": "bool" - }, - { - "name": "tickSizeInQuoteLotsPerBaseLot", - "type": "u64" + "name": "quoteLotsReleased", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } }, { - "name": "baseLotDecimals", - "type": "i8" + "name": "baseLotsRemaining", + "type": { + "defined": { + "name": "BaseLots" + } + } }, { - "name": "defaultTakerFeeMicro", - "type": "u32" - }, + "name": "reason", + "type": { + "defined": { + "name": "OrderModificationReason" + } + } + } + ] + } + }, + { + "name": "TradeSummaryEvent", + "type": { + "kind": "struct", + "fields": [ { - "name": "defaultMakerFeeMicro", - "type": "i32" + "name": "trader", + "type": "pubkey" }, { - "name": "upnlRiskFactor", + "name": "tradeSequenceNumber", "type": "u64" }, { - "name": "upnlRiskFactorForWithdrawals", + "name": "prevTradeSequenceNumberSlot", "type": "u64" }, { - "name": "riskFactors", + "name": "side", "type": { - "array": [ - "u16", - 3 - ] + "defined": { + "name": "Side" + } } }, { - "name": "cancelOrderRiskFactor", - "type": "u16" - }, - { - "name": "maxLiquidationSize", - "type": "u64" + "name": "baseLotsFilled", + "type": { + "defined": { + "name": "BaseLots" + } + } }, { - "name": "transferFeeTiers", + "name": "quoteLotsFilled", "type": { - "array": [ - { - "defined": { - "name": "TransferFeeTier" - } - }, - 4 - ] + "defined": { + "name": "QuoteLots" + } } }, { - "name": "openInterestCap", - "type": "u64" + "name": "feeInQuoteLots", + "type": { + "defined": { + "name": "QuoteLots" + } + } }, { - "name": "emaPeriodSlots", - "type": "u64" + "name": "baseLotPosition", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } }, { - "name": "emaDiffRadius", - "type": "u64" + "name": "virtualQuoteLotPosition", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } }, { - "name": "bookPriceRadius", - "type": "u64" + "name": "quoteLotCollateral", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } }, { - "name": "adjustedExchangeSpotPriceWeight", - "type": "u64" - }, + "name": "cumulativeFundingSnapshot", + "type": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + } + ] + } + }, + { + "name": "MarketSummaryEvent", + "type": { + "kind": "struct", + "fields": [ { - "name": "bookPriceWeight", - "type": "u64" + "name": "assetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } }, { - "name": "exchangePerpPriceWeight", - "type": "u64" + "name": "assetId", + "type": "u32" }, { - "name": "oracles", + "name": "openInterest", "type": { - "vec": "pubkey" + "defined": { + "name": "BaseLots" + } } }, { - "name": "oracleDivergenceRadius", - "type": "u16" + "name": "totalMakerQuoteLotFees", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLots" + } + } + } }, { - "name": "minOracleResponses", - "type": "u8" + "name": "totalTakerQuoteLotFees", + "type": { + "option": { + "defined": { + "name": "QuoteLots" + } + } + } }, { - "name": "bookHardStaleMultiplier", - "type": "u8" - }, - { - "name": "oracleHardStaleMultiplier", - "type": "u8" + "name": "markPrice", + "type": { + "defined": { + "name": "Ticks" + } + } }, { - "name": "riskActionPriceValidityRules", + "name": "spotPrice", "type": { - "option": { - "array": [ - { - "array": [ - { - "array": [ - { - "defined": { - "name": "ValidationRule" - } - }, - 8 - ] - }, - 4 - ] - }, - 8 - ] + "defined": { + "name": "Ticks" } } + } + ] + } + }, + { + "name": "TraderRegisteredEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "traderSequenceNumber", + "type": "u64" }, { - "name": "postOnlyMarketPriceRadiusPercentage", + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "maxPositions", + "type": "u32" + }, + { + "name": "traderPreferenceBits", + "type": "u32" + }, + { + "name": "traderPdaIndex", "type": "u8" }, { - "name": "fundingPeriodSeconds", - "type": { - "option": "u64" - } + "name": "traderSubaccountIndex", + "type": "u8" + } + ] + } + }, + { + "name": "TraderDelegatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" }, { - "name": "fundingIntervalSeconds", - "type": { - "option": "u64" - } + "name": "authority", + "type": "pubkey" }, { - "name": "maxFundingRate", - "type": { - "option": "i64" - } + "name": "oldPositionAuthority", + "type": "pubkey" }, { - "name": "afterHoursRadiusBps", - "type": "u64" + "name": "newPositionAuthority", + "type": "pubkey" } ] } }, { - "name": "ClaimAuthorityInstruction", + "name": "TraderFeesUpdatedEvent", "type": { "kind": "struct", "fields": [ { - "name": "authorityType", - "type": { - "defined": { - "name": "AuthorityType" - } - } + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "previousMakerFeeOverrideMultiplier", + "type": "i8" + }, + { + "name": "newMakerFeeOverrideMultiplier", + "type": "i8" + }, + { + "name": "previousTakerFeeOverrideMultiplier", + "type": "i8" + }, + { + "name": "newTakerFeeOverrideMultiplier", + "type": "i8" + }, + { + "name": "isHotTrader", + "type": "bool" } ] } }, { - "name": "NameSuccessorInstruction", + "name": "TraderCapabilitiesEnabledEvent", "type": { "kind": "struct", "fields": [ { - "name": "newAuthority", + "name": "trader", "type": "pubkey" }, { - "name": "authorityType", + "name": "authority", + "type": "pubkey" + }, + { + "name": "previousFlags", "type": { "defined": { - "name": "AuthorityType" + "name": "TraderCapabilityFlags" + } + } + }, + { + "name": "newFlags", + "type": { + "defined": { + "name": "TraderCapabilityFlags" } } + }, + { + "name": "globalTraderIndex", + "type": "u32" } ] } }, { - "name": "ClaimFeesInstruction", + "name": "TraderActivatedEvent", "type": { "kind": "struct", "fields": [ { - "name": "amount", - "type": "u64" + "name": "globalTraderIndex", + "type": "u32" + }, + { + "name": "authority", + "type": "pubkey" } ] } }, { - "name": "SetMultiArenaAdditionalNodesWatermarkParams", + "name": "TraderDeactivatedEvent", "type": { "kind": "struct", "fields": [ { - "name": "numAdditionalNodes", + "name": "prevGlobalTraderIndex", "type": "u32" + }, + { + "name": "authority", + "type": "pubkey" } ] } }, { - "name": "EnableFeatureParams", + "name": "TraderFundsDepositedEvent", "type": { "kind": "struct", "fields": [ { - "name": "feature", + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "amount", "type": { "defined": { - "name": "Feature" + "name": "QuoteLots" + } + } + }, + { + "name": "traderFlags", + "type": { + "defined": { + "name": "TraderCapabilityFlags" + } + } + }, + { + "name": "newCollateralBalance", + "type": { + "defined": { + "name": "SignedQuoteLots" } } + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" } ] } }, { - "name": "Feature", + "name": "TraderFundsWithdrawnEvent", "type": { - "kind": "enum", - "variants": [ + "kind": "struct", + "fields": [ { - "name": "LazilyReplenishWithdrawThrottle" + "name": "trader", + "type": "pubkey" }, { - "name": "NativeSolCollaterals" + "name": "authority", + "type": "pubkey" }, { - "name": "PauseOnClusterRestart" + "name": "amount", + "type": "u64" + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "traderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "postWithdrawalBudget", + "type": "u64" + }, + { + "name": "postQueueSize", + "type": "u64" + }, + { + "name": "totalQueuedAmount", + "type": "u64" + }, + { + "name": "withdrawQueueSequenceNumber", + "type": "u64" + }, + { + "name": "withdrawQueuePrevSequenceNumberSlot", + "type": "u64" } ] } + }, + { + "name": "TraderFundsWithdrawnFeePaymentEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "fee", + "type": "u64" + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "traderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "withdrawQueueSequenceNumber", + "type": "u64" + }, + { + "name": "withdrawQueuePrevSequenceNumberSlot", + "type": "u64" + } + ] + } + }, + { + "name": "TraderWithdrawCancelledEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "traderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "withdrawQueueSequenceNumber", + "type": "u64" + }, + { + "name": "withdrawQueuePrevSequenceNumberSlot", + "type": "u64" + } + ] + } + }, + { + "name": "TraderCollateralTransferredEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "amount", + "type": { + "defined": { + "name": "QuoteLots" + } + } + }, + { + "name": "srcTrader", + "type": "pubkey" + }, + { + "name": "srcTraderSequenceNumber", + "type": "u64" + }, + { + "name": "srcTraderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "srcTraderNewCollateralBalance", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "dstTrader", + "type": "pubkey" + }, + { + "name": "dstTraderSequenceNumber", + "type": "u64" + }, + { + "name": "dstTraderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "dstTraderNewCollateralBalance", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + } + ] + } + }, + { + "name": "TraderFundingSettledEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "assetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "assetId", + "type": "u32" + }, + { + "name": "fundingPayment", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "cumulativeFundingSnapshot", + "type": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + }, + { + "name": "newCollateralBalance", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + } + ] + } + }, + { + "name": "SplineRegisteredEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + } + ] + } + }, + { + "name": "SplineActivatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "midPrice", + "type": "u64" + } + ] + } + }, + { + "name": "SplineDeactivatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + } + ] + } + }, + { + "name": "SplinePositionLimitsConfigUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "maxPositionSize", + "type": { + "option": { + "defined": { + "name": "PositionSizeLimit" + } + } + } + }, + { + "name": "prevMaxPositionSize", + "type": { + "defined": { + "name": "PositionSizeLimit" + } + } + }, + { + "name": "leverageDecreaseInBps", + "type": { + "option": "u32" + } + }, + { + "name": "prevLeverageDecreaseInBps", + "type": "u32" + } + ] + } + }, + { + "name": "SplinePriceUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "priceInTicks", + "type": "u64" + }, + { + "name": "userUpdateSlot", + "type": "u64" + }, + { + "name": "refreshRegions", + "type": "bool" + } + ] + } + }, + { + "name": "SplineParametersUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "midPrice", + "type": "u64" + }, + { + "name": "bidRegions", + "type": { + "array": [ + { + "defined": { + "name": "TickRegion" + } + }, + 10 + ] + } + }, + { + "name": "askRegions", + "type": { + "array": [ + { + "defined": { + "name": "TickRegion" + } + }, + 10 + ] + } + } + ] + } + }, + { + "name": "SplinePriceUpdatedWithOrderingEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "priceInTicks", + "type": "u64" + }, + { + "name": "userUpdateSlot", + "type": "u64" + }, + { + "name": "refreshRegions", + "type": "bool" + }, + { + "name": "userSequenceNumber", + "type": "u64" + }, + { + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } + } + ] + } + }, + { + "name": "SplineParametersUpdatedWithOrderingEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "market", + "type": "pubkey" + }, + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "midPrice", + "type": "u64" + }, + { + "name": "bidRegions", + "type": { + "array": [ + { + "defined": { + "name": "TickRegion" + } + }, + 10 + ] + } + }, + { + "name": "askRegions", + "type": { + "array": [ + { + "defined": { + "name": "TickRegion" + } + }, + 10 + ] + } + }, + { + "name": "userSequenceNumber", + "type": "u64" + }, + { + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } + } + ] + } + }, + { + "name": "StopLossPlacedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "triggerPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "executionPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "tradeSize", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "tradeSide", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "executionDirection", + "type": { + "defined": { + "name": "Direction" + } + } + }, + { + "name": "positionSequenceNumber", + "type": "u8" + }, + { + "name": "placeSlot", + "type": "u64" + }, + { + "name": "fundingKey", + "type": "pubkey" + }, + { + "name": "orderKind", + "type": { + "defined": { + "name": "StopLossOrderKind" + } + } + } + ] + } + }, + { + "name": "StopLossCancelledEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "executionDirection", + "type": { + "defined": { + "name": "Direction" + } + } + } + ] + } + }, + { + "name": "StopLossExecutedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "executionDirection", + "type": { + "defined": { + "name": "Direction" + } + } + }, + { + "name": "orderSequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "TriggerOrderPlacedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "conditionalOrderIndex", + "type": "u8" + }, + { + "name": "triggerPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "executionPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "tradeSide", + "type": { + "defined": { + "name": "Side" + } + } + }, + { + "name": "triggerDirection", + "type": { + "defined": { + "name": "Direction" + } + } + }, + { + "name": "orderKind", + "type": { + "defined": { + "name": "StopLossOrderKind" + } + } + }, + { + "name": "maxSize", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "fillableSize", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "filledSize", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "usePercent", + "type": "bool" + }, + { + "name": "percent", + "type": "u8" + }, + { + "name": "positionSequenceNumber", + "type": "u8" + }, + { + "name": "attachedOrderId", + "type": { + "defined": { + "name": "OptionalFIFOOrderId" + } + } + } + ] + } + }, + { + "name": "TriggerOrderCancelledEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "conditionalOrderIndex", + "type": "u8" + }, + { + "name": "triggerDirection", + "type": { + "defined": { + "name": "Direction" + } + } + } + ] + } + }, + { + "name": "TriggerOrderExecutedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "conditionalOrderIndex", + "type": "u8" + }, + { + "name": "triggerDirection", + "type": { + "defined": { + "name": "Direction" + } + } + }, + { + "name": "orderSequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "PingInvalidatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "conditionalOrderIndex", + "type": "u8" + }, + { + "name": "pre", + "type": { + "defined": { + "name": "ConditionalOrderPingSnapshot" + } + } + }, + { + "name": "post", + "type": { + "defined": { + "name": "ConditionalOrderPingSnapshot" + } + } + }, + { + "name": "currentPositionSequenceNumber", + "type": "u8" + } + ] + } + }, + { + "name": "PingActivatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "prevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "conditionalOrderIndex", + "type": "u8" + }, + { + "name": "pre", + "type": { + "defined": { + "name": "ConditionalOrderPingSnapshot" + } + } + }, + { + "name": "post", + "type": { + "defined": { + "name": "ConditionalOrderPingSnapshot" + } + } + }, + { + "name": "bookOrderRemainingBaseLots", + "type": { + "option": { + "defined": { + "name": "BaseLots" + } + } + } + } + ] + } + }, + { + "name": "MarketAddedEvent", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "MarketStatusChangedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "previousMarketStatus", + "type": { + "defined": { + "name": "MarketStatus" + } + } + }, + { + "name": "newMarketStatus", + "type": { + "defined": { + "name": "MarketStatus" + } + } + } + ] + } + }, + { + "name": "MarketClosedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "previousMarketStatus", + "type": { + "defined": { + "name": "MarketStatus" + } + } + }, + { + "name": "finalizedMarkPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + } + ] + } + }, + { + "name": "MarketTombstonedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "previousMarketStatus", + "type": { + "defined": { + "name": "MarketStatus" + } + } + }, + { + "name": "finalSequenceNumber", + "type": "u64" + }, + { + "name": "finalTradeSequenceNumber", + "type": "u64" + }, + { + "name": "finalOrderSequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "ShutdownClosePositionsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "longTrader", + "type": "pubkey" + }, + { + "name": "shortTrader", + "type": "pubkey" + }, + { + "name": "assetId", + "type": "u32" + }, + { + "name": "baseLotsClosed", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "settlementPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "tradeSequenceNumber", + "type": "u64" + }, + { + "name": "sequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "MarketDeletedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "assetId", + "type": "u32" + }, + { + "name": "lamportsReclaimed", + "type": "u64" + } + ] + } + }, + { + "name": "ExchangeStatusChangedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "previousBits", + "type": "u8" + }, + { + "name": "newBits", + "type": "u8" + }, + { + "name": "authority", + "type": "pubkey" + } + ] + } + }, + { + "name": "MarketParametersUpdatedEvent", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "FundingParametersUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "newFundingIntervalSeconds", + "type": { + "option": { + "defined": { + "name": "FundingRateUnitInSeconds" + } + } + } + }, + { + "name": "newFundingPeriodSeconds", + "type": { + "option": { + "defined": { + "name": "FundingRateUnitInSeconds" + } + } + } + }, + { + "name": "newMaxFundingRate", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + } + } + ] + } + }, + { + "name": "FeesClaimedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "SetPermissionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "previousPermission", + "type": "u64" + }, + { + "name": "newPermission", + "type": "u64" + }, + { + "name": "previousExpiresAtTimestamp", + "type": "i64" + }, + { + "name": "newExpiresAtTimestamp", + "type": "i64" + }, + { + "name": "previousNumSignerActionsRemaining", + "type": "i64" + }, + { + "name": "newNumSignerActionsRemaining", + "type": "i64" + }, + { + "name": "created", + "type": "bool" + } + ] + } + }, + { + "name": "PricesUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracleSigner", + "type": { + "option": "pubkey" + } + }, + { + "name": "assetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "assetId", + "type": "u32" + }, + { + "name": "newBestBid", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "newBestAsk", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "newLastTrade", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "newExchangeSpotPrice", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "newExchangePerpPrice", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "newMidSpotDiffEmaTicks", + "type": { + "option": { + "defined": { + "name": "SignedTicks" + } + } + } + }, + { + "name": "newMarkPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "cumulativeFundingRate", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + } + }, + { + "name": "settledContribution", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + } + }, + { + "name": "intervalAccumulator", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLotsPerBaseLotUpcasted" + } + } + } + }, + { + "name": "assetSequenceNumber", + "type": "u64" + }, + { + "name": "prevAssetSequenceNumberSlot", + "type": "u64" + } + ] + } + }, + { + "name": "LiquidationEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "liquidator", + "type": "pubkey" + }, + { + "name": "liquidatedTrader", + "type": "pubkey" + }, + { + "name": "assetId", + "type": "u32" + }, + { + "name": "liquidationSize", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "markPrice", + "type": { + "defined": { + "name": "Ticks" + } + } + }, + { + "name": "baseLotsFilled", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "quoteLotsFilled", + "type": { + "defined": { + "name": "QuoteLots" + } + } + }, + { + "name": "positionClosed", + "type": "bool" + } + ] + } + }, + { + "name": "LiquidationTransferSummaryEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "liquidatee", + "type": "pubkey" + }, + { + "name": "liquidator", + "type": "pubkey" + }, + { + "name": "totalTransfers", + "type": "u32" + }, + { + "name": "liquidateeCollateralChange", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "liquidatorCollateralChange", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "haircutCollected", + "type": { + "defined": { + "name": "QuoteLots" + } + } + } + ] + } + }, + { + "name": "LiquidationTransferEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "liquidatee", + "type": "pubkey" + }, + { + "name": "liquidator", + "type": "pubkey" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "baseLotsTransferred", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } + }, + { + "name": "virtualQuoteLotsTransferred", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "haircutRate", + "type": "u16" + }, + { + "name": "liquidateeCollateralChange", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "liquidatorCollateralChange", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + } + ] + } + }, + { + "name": "CloseMatchedPositionsEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "caller", + "type": "pubkey" + }, + { + "name": "closedShort", + "type": "pubkey" + }, + { + "name": "closedLong", + "type": "pubkey" + }, + { + "name": "inProfitAccount", + "type": "pubkey" + }, + { + "name": "assetId", + "type": "u64" + }, + { + "name": "baseLotsClosed", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } + }, + { + "name": "atLossCloseValue", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "inProfitCloseValue", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "atLossCollateralChange", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "inProfitCollateralChange", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + } + ] + } + }, + { + "name": "NameSuccessorEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "newAuthority", + "type": "pubkey" + } + ] + } + }, + { + "name": "ClaimAuthorityEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "previousAuthority", + "type": "pubkey" + }, + { + "name": "newAuthority", + "type": "pubkey" + } + ] + } + }, + { + "name": "AuthorityChangedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "previousAuthority", + "type": "pubkey" + }, + { + "name": "newAuthority", + "type": "pubkey" + }, + { + "name": "authorityType", + "type": { + "defined": { + "name": "AuthorityType" + } + } + } + ] + } + }, + { + "name": "WithdrawStateTransitionEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "amount", + "type": { + "defined": { + "name": "QuoteLots" + } + } + }, + { + "name": "fromState", + "type": "u8" + }, + { + "name": "toState", + "type": "u8" + }, + { + "name": "reason", + "type": "u8" + }, + { + "name": "transitionCount", + "type": "u16" + }, + { + "name": "nodeIndex", + "type": { + "defined": { + "name": "NodePointer" + } + } + } + ] + } + }, + { + "name": "PnLEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "assetId", + "type": "u32" + }, + { + "name": "assetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "realizedPnl", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "fundingPayment", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "baseLotsBefore", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } + }, + { + "name": "baseLotsAfter", + "type": { + "defined": { + "name": "SignedBaseLots" + } + } + }, + { + "name": "virtualQuoteLotsBefore", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + }, + { + "name": "virtualQuoteLotsAfter", + "type": { + "defined": { + "name": "SignedQuoteLots" + } + } + } + ] + } + }, + { + "name": "AdminParameterUpdatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "assetSymbol", + "type": { + "option": { + "defined": { + "name": "Symbol" + } + } + } + }, + { + "name": "assetId", + "type": { + "option": "u32" + } + }, + { + "name": "updateKind", + "type": { + "defined": { + "name": "AdminParameterUpdateKind" + } + } + } + ] + } + }, + { + "name": "EscrowAccountCreatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "capacity", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowRequestCreatedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiverAuthority", + "type": "pubkey" + }, + { + "name": "senderAuthority", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "senderPdaIndex", + "type": "u8" + }, + { + "name": "senderSubaccountIndex", + "type": "u8" + }, + { + "name": "receiverPdaIndex", + "type": "u8" + }, + { + "name": "receiverSubaccountIndex", + "type": "u8" + }, + { + "name": "expirationOffset", + "type": "u32" + }, + { + "name": "initialSlot", + "type": "u64" + }, + { + "name": "actions", + "type": { + "array": [ + { + "defined": { + "name": "EscrowAction" + } + }, + 4 + ] + } + } + ] + } + }, + { + "name": "EscrowRequestAcceptedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiverAuthority", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowRequestCancelledEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiverAuthority", + "type": "pubkey" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "reason", + "type": { + "defined": { + "name": "EscrowRequestCancelReason" + } + } + } + ] + } + }, + { + "name": "ChangeMarketStatusParams", + "docs": [ + "Borsh payload for changing an orderbook market status." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "nextMarketStatus", + "type": { + "defined": { + "name": "MarketStatus" + } + } + } + ] + } + }, + { + "name": "ChangeExchangeStatusParams", + "docs": [ + "Borsh payload patching the exchange-wide status bits; `None` preserves a bit." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "active", + "type": { + "option": "bool" + } + }, + { + "name": "gated", + "type": { + "option": "bool" + } + }, + { + "name": "maintenance", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "AuthorizedForceCancelParams", + "docs": [ + "Borsh payload for authority-driven force cancellation by remaining side size." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "remainingBidBaseLots", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "remainingAskBaseLots", + "type": { + "defined": { + "name": "BaseLots" + } + } + } + ] + } + }, + { + "name": "AuthorizedForceCancelByIdParams", + "docs": [ + "Borsh payload for authority-driven force cancellation by order id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bidOrderIds", + "type": { + "vec": { + "defined": { + "name": "CancelId" + } + } + } + }, + { + "name": "askOrderIds", + "type": { + "vec": { + "defined": { + "name": "CancelId" + } + } + } + } + ] + } + }, + { + "name": "CreateEscrowAccountParams", + "docs": [ + "Borsh payload for creating an escrow account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "capacity", + "type": "u64" + } + ] + } + }, + { + "name": "EscrowParticipantMetadata", + "docs": [ + "Trader PDA/subaccount metadata stored with escrow requests." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "senderPdaIndex", + "type": "u8" + }, + { + "name": "senderSubaccountIndex", + "type": "u8" + }, + { + "name": "receiverPdaIndex", + "type": "u8" + }, + { + "name": "receiverSubaccountIndex", + "type": "u8" + } + ] + } + }, + { + "name": "CreateEscrowRequestParams", + "docs": [ + "Borsh payload for creating an escrow request." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "participantMetadata", + "type": { + "defined": { + "name": "EscrowParticipantMetadata" + } + } + }, + { + "name": "lastValidSlot", + "type": { + "option": "u64" + } + }, + { + "name": "actions", + "type": { + "vec": { + "defined": { + "name": "EscrowAction" + } + } + } + } + ] + } + }, + { + "name": "CancelEscrowRequestParams", + "docs": [ + "Borsh payload for cancelling an escrow request by sequence number." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "sequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "UpsertEscrowRequestParams", + "docs": [ + "Borsh payload for upserting an escrow request." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "participantMetadata", + "type": { + "defined": { + "name": "EscrowParticipantMetadata" + } + } + }, + { + "name": "action", + "type": { + "defined": { + "name": "EscrowAction" + } + } + } + ] + } + }, + { + "name": "RegisterSplineParams", + "docs": [ + "Empty Borsh payload for registering a spline trader." + ], + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "UpdateCommodityMarketStateInstruction", + "docs": [ + "Borsh payload for updating commodity market state and mark price inputs." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "marketState", + "type": { + "defined": { + "name": "CommodityMarketState" + } + } + }, + { + "name": "lastKnownIndexPrice", + "type": { + "option": { + "defined": { + "name": "Ticks" + } + } + } + }, + { + "name": "lastIndexExpiryTimestamp", + "type": "u64" + }, + { + "name": "emaPeriodSlots", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "UpdatePerpIsolatedOnlyInstruction", + "docs": [ + "Borsh payload for updating isolated-only metadata for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "isolatedOnly", + "type": "u8" + } + ] + } + }, + { + "name": "UpdatePerpLeverageTiersInstruction", + "docs": [ + "Borsh payload for updating leverage tiers for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "leverageTiers", + "type": { + "defined": { + "name": "LeverageTiers" + } + } + } + ] + } + }, + { + "name": "UpdatePerpOpenInterestCapInstruction", + "docs": [ + "Borsh payload for updating open interest cap for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "openInterestCap", + "type": { + "defined": { + "name": "BaseLots" + } + } + } + ] + } + }, + { + "name": "UPnlRiskFactor", + "docs": [ + "uPnL risk factor encoded as basis points." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "inner", + "type": "u64" + } + ] + } + }, + { + "name": "UpdatePerpMaxLiquidationSizeInstruction", + "docs": [ + "Borsh payload for updating max liquidation size for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "maxLiquidationSize", + "type": { + "defined": { + "name": "BaseLots" + } + } + } + ] + } + }, + { + "name": "UpdatePerpRiskFactorsInstruction", + "docs": [ + "Borsh payload for updating risk factors for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "maintenanceRiskFactor", + "type": { + "defined": { + "name": "BasisPointsU16" + } + } + }, + { + "name": "backstopRiskFactor", + "type": { + "defined": { + "name": "BasisPointsU16" + } + } + }, + { + "name": "highRiskRiskFactor", + "type": { + "defined": { + "name": "BasisPointsU16" + } + } + } + ] + } + }, + { + "name": "UpdatePerpTransferFeeTiersInstruction", + "docs": [ + "Borsh payload for updating transfer fee tiers for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "transferFeeTiers", + "type": { + "array": [ + { + "defined": { + "name": "TransferFeeTier" + } + }, + 4 + ] + } + } + ] + } + }, + { + "name": "TransferFeeTier", + "docs": [ + "Liquidation-transfer haircut tier keyed by position size." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "positionSizeLimit", + "type": { + "defined": { + "name": "BaseLots" + } + } + }, + { + "name": "feeRate", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "UpdatePerpUPnlRiskFactorInstruction", + "docs": [ + "Borsh payload for updating uPnL risk factor for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "upnlRiskFactor", + "type": { + "defined": { + "name": "UPnlRiskFactor" + } + } + } + ] + } + }, + { + "name": "UpdatePerpUPnlRiskFactorForWithdrawalsInstruction", + "docs": [ + "Borsh payload for updating withdrawal uPnL risk factor for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "upnlRiskFactorForWithdrawals", + "type": { + "defined": { + "name": "UPnlRiskFactor" + } + } + } + ] + } + }, + { + "name": "UpdatePerpCancelRiskFactorInstruction", + "docs": [ + "Borsh payload for updating cancel-order risk factor for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "cancelOrderRiskFactor", + "type": "u16" + } + ] + } + }, + { + "name": "UpdatePerpMarkPriceParametersInstruction", + "docs": [ + "Borsh payload for updating mark price calculation parameters for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "emaPeriodSlots", + "type": { + "option": "u64" + } + }, + { + "name": "emaDiffRadius", + "type": { + "option": "u64" + } + }, + { + "name": "bookPriceRadius", + "type": { + "option": "u64" + } + }, + { + "name": "commoditiesAfterHoursRadiusBps", + "type": { + "option": "u64" + } + }, + { + "name": "adjustedExchangeSpotPriceWeight", + "type": { + "option": "u64" + } + }, + { + "name": "bookPriceWeight", + "type": { + "option": "u64" + } + }, + { + "name": "exchangePerpPriceWeight", + "type": { + "option": "u64" + } + }, + { + "name": "spotPriceStaleThreshold", + "type": { + "option": "u64" + } + }, + { + "name": "bookPriceStaleThreshold", + "type": { + "option": "u64" + } + }, + { + "name": "perpPriceStaleThreshold", + "type": { + "option": "u64" + } + }, + { + "name": "riskActionPriceValidityRules", + "type": { + "option": { + "array": [ + { + "array": [ + { + "array": [ + { + "defined": { + "name": "ValidationRule" + } + }, + 8 + ] + }, + 4 + ] + }, + 8 + ] + } + } + }, + { + "name": "oracleDivergenceRadius", + "type": { + "option": "u16" + } + }, + { + "name": "minOracleResponses", + "type": { + "option": "u8" + } + } + ] + } + }, + { + "name": "UpdatePerpParametersDelegatedInstruction", + "docs": [ + "Borsh payload for maintaining price-dependent parameters for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "emaDiffRadius", + "type": { + "option": "u64" + } + }, + { + "name": "bookPriceRadius", + "type": { + "option": "u64" + } + }, + { + "name": "maxFundingRate", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + } + } + ] + } + }, + { + "name": "UpdateTraderFeesParams", + "docs": [ + "Borsh payload for updating trader fee override multipliers." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "makerFeeOverrideMultiplier", + "type": "i8" + }, + { + "name": "takerFeeOverrideMultiplier", + "type": "i8" + } + ] + } + }, + { + "name": "UpdateMarketFeesParams", + "docs": [ + "Borsh payload for updating market default fees." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "defaultTakerFeeMicro", + "type": { + "option": "u32" + } + }, + { + "name": "defaultMakerFeeMicro", + "type": { + "option": "i32" + } + } + ] + } + }, + { + "name": "UpdateWithdrawRateLimitsParams", + "docs": [ + "Borsh payload for updating withdraw throttle rate limits." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxBudget", + "type": { + "option": "u64" + } + }, + { + "name": "replenishAmountPerSlot", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "UpdateWithdrawParametersParams", + "docs": [ + "Borsh payload for updating withdraw cooldown and fee parameters." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "depositCooldownPeriodInSlots", + "type": { + "option": "u64" + } + }, + { + "name": "withdrawalFee", + "type": { + "option": "u64" + } + }, + { + "name": "enqueueingFee", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "UpdateAuthoritiesParams", + "docs": [ + "Borsh payload for root authority updates to subordinate authorities." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "newRiskAuthority", + "type": { + "option": "pubkey" + } + }, + { + "name": "newMarketAuthority", + "type": { + "option": "pubkey" + } + }, + { + "name": "newOracleAuthority", + "type": { + "option": "pubkey" + } + }, + { + "name": "newCancelAuthority", + "type": { + "option": "pubkey" + } + }, + { + "name": "newBackstopAuthority", + "type": { + "option": "pubkey" + } + }, + { + "name": "newAdlAuthority", + "type": { + "option": "pubkey" + } + } + ] + } + }, + { + "name": "ArenaType", + "docs": [ + "Arena account family to initialize." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "GlobalTraderIndex" + }, + { + "name": "ActiveTraderBuffer" + } + ] + } + }, + { + "name": "InitializeArenaParams", + "docs": [ + "Borsh payload for initializing a new arena account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "arenaType", + "type": { + "defined": { + "name": "ArenaType" + } + } + } + ] + } + }, + { + "name": "RemoveOracleInstruction", + "docs": [ + "Borsh payload for removing oracle public keys from the perp asset map." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracleKeys", + "type": { + "vec": "pubkey" + } + } + ] + } + }, + { + "name": "RemoveAllOraclesInstruction", + "docs": [ + "Borsh payload for removing all oracle keys for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + } + ] + } + }, + { + "name": "UpdateFundingParametersInstruction", + "docs": [ + "Borsh payload for updating funding parameters for a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "fundingIntervalSeconds", + "type": { + "option": { + "defined": { + "name": "FundingRateUnitInSeconds" + } + } + } + }, + { + "name": "fundingPeriodSeconds", + "type": { + "option": { + "defined": { + "name": "FundingRateUnitInSeconds" + } + } + } + }, + { + "name": "maxFundingRate", + "type": { + "option": { + "defined": { + "name": "SignedQuoteLotsPerBaseLot" + } + } + } + } + ] + } + }, + { + "name": "UpdatePerpDisplayNameInstruction", + "docs": [ + "Borsh payload for setting or clearing the display name of a perp asset." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "displayName", + "type": { + "defined": { + "name": "DisplayName" + } + } + } + ] + } + }, + { + "name": "UpdateSplinePositionLimitsConfigParams", + "docs": [ + "Borsh payload for updating spline position-size limits and leverage decrease configuration." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxPositionSize", + "type": { + "option": { + "defined": { + "name": "PositionSizeLimit" + } + } + } + }, + { + "name": "leverageDecreaseInBps", + "type": { + "option": "u32" + } + } + ] + } + }, + { + "name": "TraderFundsWithdrawnEnqueuedEvent", + "docs": [ + "Viewer-safe alias for MarketEvent::TraderFundsWithdrawnEnqueued.", + "Uses the same payload layout as TraderFundsWithdrawnEvent." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "traderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "postWithdrawalBudget", + "type": "u64" + }, + { + "name": "postQueueSize", + "type": "u64" + }, + { + "name": "totalQueuedAmount", + "type": "u64" + }, + { + "name": "withdrawQueueSequenceNumber", + "type": "u64" + }, + { + "name": "withdrawQueuePrevSequenceNumberSlot", + "type": "u64" + } + ] + } + }, + { + "name": "TraderFundsWithdrawnDroppedEvent", + "docs": [ + "Viewer-safe alias for MarketEvent::TraderFundsWithdrawnDropped.", + "Uses the same payload layout as TraderFundsWithdrawnEvent." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "traderSequenceNumber", + "type": "u64" + }, + { + "name": "traderPrevSequenceNumberSlot", + "type": "u64" + }, + { + "name": "postWithdrawalBudget", + "type": "u64" + }, + { + "name": "postQueueSize", + "type": "u64" + }, + { + "name": "totalQueuedAmount", + "type": "u64" + }, + { + "name": "withdrawQueueSequenceNumber", + "type": "u64" + }, + { + "name": "withdrawQueuePrevSequenceNumberSlot", + "type": "u64" + } + ] + } + }, + { + "name": "AddSpotCollateralParams", + "docs": [ + "Parameters for registering a new SPL spot collateral asset; registration requires a complete, valid configuration up front and the entry starts inactive." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "mintAddress", + "type": "pubkey" + }, + { + "name": "perpAssetSymbol", + "type": { + "option": { + "defined": { + "name": "Symbol" + } + } + } + }, + { + "name": "config", + "type": { + "defined": { + "name": "SpotAssetConfig" + } + } + } + ] + } + }, + { + "name": "ConfigureSpotCollateralParams", + "docs": [ + "Parameters for updating a registered spot collateral asset; None fields are left unchanged. isActive = true activates the asset, requiring the SpotCollaterals feature and a configured linked-perp or standalone oracle price source. isActive = false is rejected." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "spotAssetIndex", + "type": "u32" + }, + { + "name": "update", + "type": { + "defined": { + "name": "SpotAssetConfigUpdate" + } + } + }, + { + "name": "isActive", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "SpotAssetConfig", + "docs": [ + "Risk parameters for a spot collateral asset. Basis-point fields wrap u32; quote-lot fields wrap u64." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxPerTraderBalance", + "type": "u64" + }, + { + "name": "maxGlobalBalance", + "type": "u64" + }, + { + "name": "minMarginDiscount", + "type": "u32" + }, + { + "name": "maxMarginDiscount", + "type": "u32" + }, + { + "name": "maxLiquidationDiscount", + "type": "u32" + }, + { + "name": "minLiquidationSlippage", + "type": "u32" + }, + { + "name": "maxLiquidationSize", + "type": "u64" + }, + { + "name": "postLiquidationBuffer", + "type": "u64" + }, + { + "name": "quoteLotCollateralShortfallBuffer", + "type": "u64" + } + ] + } + }, + { + "name": "SpotAssetConfigUpdate", + "docs": [ + "Partial (Option-wrapped) counterpart of SpotAssetConfig; None fields are left unchanged." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxPerTraderBalance", + "type": { + "option": "u64" + } + }, + { + "name": "maxGlobalBalance", + "type": { + "option": "u64" + } + }, + { + "name": "minMarginDiscount", + "type": { + "option": "u32" + } + }, + { + "name": "maxMarginDiscount", + "type": { + "option": "u32" + } + }, + { + "name": "maxLiquidationDiscount", + "type": { + "option": "u32" + } + }, + { + "name": "minLiquidationSlippage", + "type": { + "option": "u32" + } + }, + { + "name": "maxLiquidationSize", + "type": { + "option": "u64" + } + }, + { + "name": "postLiquidationBuffer", + "type": { + "option": "u64" + } + }, + { + "name": "quoteLotCollateralShortfallBuffer", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "TransferSpotParams", + "docs": [ + "Amount is in the token's native units." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawSpotParams", + "docs": [ + "Action selects how the withdrawal splits between accounted collateral and uncounted custody excess." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "action", + "type": { + "defined": { + "name": "WithdrawCollateralAction" + } + } + } + ] + } + }, + { + "name": "WithdrawCollateralAction", + "docs": [ + "Shared by WithdrawNativeSol and WithdrawSpot. AllExcess withdraws every uncounted unit without debiting collateral; WithExcess withdraws exactly amount, consuming uncounted excess before accounted collateral; WithoutExcess withdraws exactly amount from accounted collateral, leaving excess in place. Amounts are in the token's native units." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "AllExcess" + }, + { + "name": "WithExcess", + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "WithoutExcess", + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ] + } + }, + { + "name": "SwapSpotWithSpotInstruction", + "docs": [ + "AmountIn is in the source token's native units, minAmountOut is the slippage bound in the destination token's native units (0 disables the check), and packedInstructions are executed against the trailing remaining accounts." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "amountIn", + "type": "u64" + }, + { + "name": "minAmountOut", + "type": "u64" + }, + { + "name": "packedInstructions", + "type": { + "vec": { + "defined": { + "name": "PackedInstruction" + } + } + } + } + ] + } + }, + { + "name": "SwapSpotWithUsdcInstruction", + "docs": [ + "Direction (Sell: spot to USDC, Buy: USDC to spot), amountIn (spot native units for Sell, USDC quote lots for Buy), minAmountOut in the output asset's units (0 disables), and the packed external venue instructions." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "direction", + "type": { + "defined": { + "name": "SwapDirection" + } + } + }, + { + "name": "amountIn", + "type": "u64" + }, + { + "name": "minAmountOut", + "type": "u64" + }, + { + "name": "packedInstructions", + "type": { + "vec": { + "defined": { + "name": "PackedInstruction" + } + } + } + } + ] + } + }, + { + "name": "SwapSpotWithSolInstruction", + "docs": [ + "Direction (Sell: spot to SOL, Buy: SOL to spot), amountIn (spot native units for Sell, lamports for Buy), minAmountOut in the output asset's units (0 disables), and the packed external venue instructions." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "direction", + "type": { + "defined": { + "name": "SwapDirection" + } + } + }, + { + "name": "amountIn", + "type": "u64" + }, + { + "name": "minAmountOut", + "type": "u64" + }, + { + "name": "packedInstructions", + "type": { + "vec": { + "defined": { + "name": "PackedInstruction" + } + } + } + } + ] + } + }, + { + "name": "SwapDirection", + "docs": [ + "Which side of a two-legged swap the non-quote asset is on. Sell debits the non-quote leg and credits the quote-side leg; Buy the reverse." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Sell" + }, + { + "name": "Buy" + } + ] + } + }, + { + "name": "LiquidateSpotParams", + "docs": [ + "MaxSpotAmount is the amount of spot collateral, in the token's native units, the liquidator takes over; numTradersToCheck counts the leading remaining accounts used as shortfall evidence; packedInstructions are venue instructions executed via CPI against the trailing venue accounts." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxSpotAmount", + "type": "u64" + }, + { + "name": "numTradersToCheck", + "type": "u64" + }, + { + "name": "packedInstructions", + "type": { + "vec": { + "defined": { + "name": "PackedInstruction" + } + } + } + } + ] + } + }, + { + "name": "PackedInstruction", + "docs": [ + "Caller-provided CPI instruction whose accounts index into the trailing venue accounts." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "programIdIndex", + "type": "u8" + }, + { + "name": "data", + "type": { + "vec": "u8" + } + }, + { + "name": "accountMetas", + "type": { + "vec": { + "defined": { + "name": "PackedAccountMeta" + } + } + } + } + ] + } + }, + { + "name": "PackedAccountMeta", + "docs": [ + "Single-byte packed account meta: bits 7..2 = account index into the trailing venue accounts, bit 1 = is_signer, bit 0 = is_writable." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "state", + "type": "u8" + } + ] + } + }, + { + "name": "SetSpotAssetPriceInstruction", + "docs": [ + "Borsh payload for recording a spot-collateral price. The exponent must equal the", + "asset's configured price exponent; the symbol is cross-checked against the entry", + "at the asset index." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "symbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "assetIndex", + "type": "u32" + }, + { + "name": "price", + "type": { + "defined": { + "name": "Price" + } + } + } + ] + } + }, + { + "name": "ChangeSpotAssetParamsInstruction", + "docs": [ + "Borsh payload updating selected fields in a spot asset's oracle-price configuration;", + "Some(default pubkey) disables proxy price writes and None leaves that field unchanged." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "mintAddress", + "type": "pubkey" + }, + { + "name": "proxyAuthority", + "type": { + "option": "pubkey" + } + }, + { + "name": "priceExpo", + "type": { + "option": "u8" + } + }, + { + "name": "staleThresholdSlots", + "type": { + "option": "u64" + } + }, + { + "name": "hardStaleMultiplier", + "type": { + "option": "u8" + } + } + ] + } + }, + { + "name": "TransferNativeSolInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateNativeSolParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxNativeSolAmount", + "type": "u64" + }, + { + "name": "numTradersToCheck", + "type": "u64" + }, + { + "name": "packedInstructions", + "type": { + "vec": { + "defined": { + "name": "PackedInstruction" + } + } + } + } + ] + } + }, + { + "name": "SwapNativeInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "direction", + "type": { + "defined": { + "name": "SwapDirection" + } + } + }, + { + "name": "amountIn", + "type": "u64" + }, + { + "name": "minAmountOut", + "type": "u64" + }, + { + "name": "packedInstructions", + "type": { + "vec": { + "defined": { + "name": "PackedInstruction" + } + } + } + } + ] + } + }, + { + "name": "ConfigureNativeSolParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxPerTraderBalance", + "type": { + "option": "u64" + } + }, + { + "name": "maxGlobalBalance", + "type": { + "option": "u64" + } + }, + { + "name": "minMarginDiscount", + "type": { + "option": "u32" + } + }, + { + "name": "maxMarginDiscount", + "type": { + "option": "u32" + } + }, + { + "name": "maxLiquidationDiscount", + "type": { + "option": "u32" + } + }, + { + "name": "minLiquidationSlippage", + "type": { + "option": "u32" + } + }, + { + "name": "maxLiquidationSize", + "type": { + "option": "u64" + } + }, + { + "name": "postLiquidationBuffer", + "type": { + "option": "u64" + } + }, + { + "name": "quoteLotCollateralShortfallBuffer", + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "WithdrawNativeSolAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "AllExcess" + }, + { + "name": "WithExcess", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "WithoutExcess", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + ] + } + }, + { + "name": "RebirthParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "globalTraderIndexNumNodesPerArena", + "type": "u32" + }, + { + "name": "activeTraderBufferNumNodesPerArena", + "type": "u32" + }, + { + "name": "multiArenaNumAdditionalNodes", + "type": "u32" + }, + { + "name": "withdrawThrottleMaxBudget", + "type": "u64" + }, + { + "name": "withdrawThrottleReplenishAmountPerSlot", + "type": "u64" + } + ] + } + }, + { + "name": "AddMarketParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpAssetSymbol", + "type": { + "defined": { + "name": "Symbol" + } + } + }, + { + "name": "maxLeverageTiers", + "type": { + "defined": { + "name": "LeverageTiers" + } + } + }, + { + "name": "isolatedOnly", + "type": "u8" + }, + { + "name": "isCommodity", + "type": "bool" + }, + { + "name": "tickSizeInQuoteLotsPerBaseLot", + "type": "u64" + }, + { + "name": "baseLotDecimals", + "type": "i8" + }, + { + "name": "defaultTakerFeeMicro", + "type": "u32" + }, + { + "name": "defaultMakerFeeMicro", + "type": "i32" + }, + { + "name": "upnlRiskFactor", + "type": "u64" + }, + { + "name": "upnlRiskFactorForWithdrawals", + "type": "u64" + }, + { + "name": "riskFactors", + "type": { + "array": [ + "u16", + 3 + ] + } + }, + { + "name": "cancelOrderRiskFactor", + "type": "u16" + }, + { + "name": "maxLiquidationSize", + "type": "u64" + }, + { + "name": "transferFeeTiers", + "type": { + "array": [ + { + "defined": { + "name": "TransferFeeTier" + } + }, + 4 + ] + } + }, + { + "name": "openInterestCap", + "type": "u64" + }, + { + "name": "emaPeriodSlots", + "type": "u64" + }, + { + "name": "emaDiffRadius", + "type": "u64" + }, + { + "name": "bookPriceRadius", + "type": "u64" + }, + { + "name": "adjustedExchangeSpotPriceWeight", + "type": "u64" + }, + { + "name": "bookPriceWeight", + "type": "u64" + }, + { + "name": "exchangePerpPriceWeight", + "type": "u64" + }, + { + "name": "oracles", + "type": { + "vec": "pubkey" + } + }, + { + "name": "oracleDivergenceRadius", + "type": "u16" + }, + { + "name": "minOracleResponses", + "type": "u8" + }, + { + "name": "bookHardStaleMultiplier", + "type": "u8" + }, + { + "name": "oracleHardStaleMultiplier", + "type": "u8" + }, + { + "name": "riskActionPriceValidityRules", + "type": { + "option": { + "array": [ + { + "array": [ + { + "array": [ + { + "defined": { + "name": "ValidationRule" + } + }, + 8 + ] + }, + 4 + ] + }, + 8 + ] + } + } + }, + { + "name": "postOnlyMarketPriceRadiusPercentage", + "type": "u8" + }, + { + "name": "fundingPeriodSeconds", + "type": { + "option": "u64" + } + }, + { + "name": "fundingIntervalSeconds", + "type": { + "option": "u64" + } + }, + { + "name": "maxFundingRate", + "type": { + "option": "i64" + } + }, + { + "name": "afterHoursRadiusBps", + "type": "u64" + } + ] + } + }, + { + "name": "ClaimAuthorityInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authorityType", + "type": { + "defined": { + "name": "AuthorityType" + } + } + } + ] + } + }, + { + "name": "NameSuccessorInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newAuthority", + "type": "pubkey" + }, + { + "name": "authorityType", + "type": { + "defined": { + "name": "AuthorityType" + } + } + } + ] + } + }, + { + "name": "ClaimFeesInstruction", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "EnableFeatureParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feature", + "type": { + "defined": { + "name": "Feature" + } + } + } + ] + } + }, + { + "name": "Feature", + "type": { + "kind": "enum", + "variants": [ + { + "name": "LazilyReplenishWithdrawThrottle" + }, + { + "name": "NativeSolCollaterals" + }, + { + "name": "PauseOnClusterRestart" + }, + { + "name": "SpotCollaterals" + } + ] + } + }, + { + "name": "AdditionalFeeAccruedEvent", + "docs": [ + "Rates are in tenths of a basis point. Rounded combined fee minus rounded base fee. Maker amounts are per fill with an order id; taker amounts aggregate the incoming order without an order id." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "trader", + "type": "pubkey" + }, + { + "name": "orderSequenceNumber", + "type": { + "option": "u64" + } + }, + { + "name": "additionalFeeTenthsBps", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + }, + { + "name": "quoteLotsFilled", + "type": { + "defined": { + "name": "QuoteLots" + } + } + }, + { + "name": "feeInQuoteLots", + "type": { + "defined": { + "name": "QuoteLots" + } + } + } + ] + } + }, + { + "name": "AdditionalFees", + "docs": [ + "Additional fees in tenths of a basis point for taker and maker fills." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "taker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + }, + { + "name": "maker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + } + ] + } + }, + { + "name": "AdditionalFeeTenthsBps", + "docs": [ + "Additional fee rate encoded as one raw u8 in tenths of a basis point.", + "1 means 0.1 bps; 100 means 10 bps; 255 means 25.5 bps. Zero means no additional fee. There is no Option tag." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "inner", + "type": "u8" + } + ] + } + }, + { + "name": "AdditionalFeeConfig", + "docs": [ + "Optional instruction suffix; Borsh enum with u8 rates in tenths of a basis point. Omit the suffix for zero fees." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "MakerAndTaker", + "fields": [ + { + "name": "maker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + }, + { + "name": "taker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + } + ] + }, + { + "name": "MakerOnly", + "fields": [ + { + "name": "maker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + } + ] + }, + { + "name": "MakerTakerAndConditionalTaker", + "fields": [ + { + "name": "maker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + }, + { + "name": "taker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + }, + { + "name": "conditionalTaker", + "type": { + "defined": { + "name": "AdditionalFeeTenthsBps" + } + } + } + ] + } + ] + } + }, + { + "name": "UpdateSplinePriceWithMetadataParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newMidPrice", + "type": "u32" + }, + { + "name": "flags", + "type": "u32" + }, + { + "name": "userSequenceNumber", + "type": "u64" + }, + { + "name": "updateSlot", + "type": "u64" + }, + { + "name": "bidWidth", + "type": "u32" + }, + { + "name": "askWidth", + "type": "u32" + }, + { + "name": "clientOrderId", + "type": { + "array": [ + "u8", + 16 + ] + } + } + ] + } + } + ], + "events": [ + { + "name": "SlotContextEvent", + "docs": [ + "MarketEvent::SlotContext Borsh variant 0.", + "Payload type: SlotContextEvent." + ], + "discriminator": [ + 0 + ] + }, + { + "name": "MarketEventHeader", + "docs": [ + "MarketEvent::Header Borsh variant 1.", + "Payload type: MarketEventHeader." + ], + "discriminator": [ + 1 + ] + }, + { + "name": "OrderPlacedEvent", + "docs": [ + "MarketEvent::OrderPlaced Borsh variant 2.", + "Payload type: OrderPlacedEvent." + ], + "discriminator": [ + 2 + ] + }, + { + "name": "OrderFilledEvent", + "docs": [ + "MarketEvent::OrderFilled Borsh variant 3.", + "Payload type: OrderFilledEvent." + ], + "discriminator": [ + 3 + ] + }, + { + "name": "OrderRejectedEvent", + "docs": [ + "MarketEvent::OrderRejected Borsh variant 4.", + "Payload type: OrderRejectedEvent." + ], + "discriminator": [ + 4 + ] + }, + { + "name": "SplineFilledEvent", + "docs": [ + "MarketEvent::SplineFilled Borsh variant 5.", + "Payload type: SplineFilledEvent." + ], + "discriminator": [ + 5 + ] + }, + { + "name": "TradeSummaryEvent", + "docs": [ + "MarketEvent::TradeSummary Borsh variant 6.", + "Payload type: TradeSummaryEvent." + ], + "discriminator": [ + 6 + ] + }, + { + "name": "OrderModifiedEvent", + "docs": [ + "MarketEvent::OrderModified Borsh variant 7.", + "Payload type: OrderModifiedEvent." + ], + "discriminator": [ + 7 + ] + }, + { + "name": "MarketSummaryEvent", + "docs": [ + "MarketEvent::MarketSummary Borsh variant 8.", + "Payload type: MarketSummaryEvent." + ], + "discriminator": [ + 8 + ] + }, + { + "name": "TraderRegisteredEvent", + "docs": [ + "MarketEvent::TraderRegistered Borsh variant 9.", + "Payload type: TraderRegisteredEvent." + ], + "discriminator": [ + 9 + ] + }, + { + "name": "TraderCollateralTransferredEvent", + "docs": [ + "MarketEvent::TraderCollateralTransferred Borsh variant 10.", + "Payload type: TraderCollateralTransferredEvent." + ], + "discriminator": [ + 10 + ] + }, + { + "name": "TraderActivatedEvent", + "docs": [ + "MarketEvent::TraderActivated Borsh variant 11.", + "Payload type: TraderActivatedEvent." + ], + "discriminator": [ + 11 + ] + }, + { + "name": "TraderDeactivatedEvent", + "docs": [ + "MarketEvent::TraderDeactivated Borsh variant 12.", + "Payload type: TraderDeactivatedEvent." + ], + "discriminator": [ + 12 + ] + }, + { + "name": "TraderFundsDepositedEvent", + "docs": [ + "MarketEvent::TraderFundsDeposited Borsh variant 13.", + "Payload type: TraderFundsDepositedEvent." + ], + "discriminator": [ + 13 + ] + }, + { + "name": "TraderFundsWithdrawnEvent", + "docs": [ + "MarketEvent::TraderFundsWithdrawn Borsh variant 14.", + "Payload type: TraderFundsWithdrawnEvent." + ], + "discriminator": [ + 14 + ] + }, + { + "name": "TraderFundsWithdrawnEnqueuedEvent", + "docs": [ + "MarketEvent::TraderFundsWithdrawnEnqueued Borsh variant 15.", + "Payload type: TraderFundsWithdrawnEvent." + ], + "discriminator": [ + 15 + ] + }, + { + "name": "TraderFundsWithdrawnDroppedEvent", + "docs": [ + "MarketEvent::TraderFundsWithdrawnDropped Borsh variant 16.", + "Payload type: TraderFundsWithdrawnEvent." + ], + "discriminator": [ + 16 + ] + }, + { + "name": "TraderWithdrawCancelledEvent", + "docs": [ + "MarketEvent::TraderWithdrawCancelled Borsh variant 17.", + "Payload type: TraderWithdrawCancelledEvent." + ], + "discriminator": [ + 17 + ] + }, + { + "name": "TraderFundingSettledEvent", + "docs": [ + "MarketEvent::TraderFundingSettled Borsh variant 18.", + "Payload type: TraderFundingSettledEvent." + ], + "discriminator": [ + 18 + ] + }, + { + "name": "SplineRegisteredEvent", + "docs": [ + "MarketEvent::SplineRegistered Borsh variant 19.", + "Payload type: SplineRegisteredEvent." + ], + "discriminator": [ + 19 + ] + }, + { + "name": "SplineActivatedEvent", + "docs": [ + "MarketEvent::SplineActivated Borsh variant 20.", + "Payload type: SplineActivatedEvent." + ], + "discriminator": [ + 20 + ] + }, + { + "name": "SplineDeactivatedEvent", + "docs": [ + "MarketEvent::SplineDeactivated Borsh variant 21.", + "Payload type: SplineDeactivatedEvent." + ], + "discriminator": [ + 21 + ] + }, + { + "name": "SplinePriceUpdatedEvent", + "docs": [ + "MarketEvent::SplinePriceUpdated Borsh variant 22.", + "Payload type: SplinePriceUpdatedEvent." + ], + "discriminator": [ + 22 + ] + }, + { + "name": "SplineParametersUpdatedEvent", + "docs": [ + "MarketEvent::SplineParametersUpdated Borsh variant 23.", + "Payload type: SplineParametersUpdatedEvent." + ], + "discriminator": [ + 23 + ] + }, + { + "name": "MarketAddedEvent", + "docs": [ + "MarketEvent::MarketAdded Borsh variant 24.", + "Payload type: MarketAddedEvent." + ], + "discriminator": [ + 24 + ] + }, + { + "name": "MarketStatusChangedEvent", + "docs": [ + "MarketEvent::MarketStatusChanged Borsh variant 25.", + "Payload type: MarketStatusChangedEvent." + ], + "discriminator": [ + 25 + ] + }, + { + "name": "MarketParametersUpdatedEvent", + "docs": [ + "MarketEvent::MarketParametersUpdated Borsh variant 26.", + "Payload type: MarketParametersUpdatedEvent." + ], + "discriminator": [ + 26 + ] + }, + { + "name": "FundingParametersUpdatedEvent", + "docs": [ + "MarketEvent::FundingParametersUpdated Borsh variant 27.", + "Payload type: FundingParametersUpdatedEvent." + ], + "discriminator": [ + 27 + ] + }, + { + "name": "FeesClaimedEvent", + "docs": [ + "MarketEvent::FeesClaimed Borsh variant 28.", + "Payload type: FeesClaimedEvent." + ], + "discriminator": [ + 28 + ] + }, + { + "name": "PricesUpdatedEvent", + "docs": [ + "MarketEvent::PricesUpdated Borsh variant 29.", + "Payload type: PricesUpdatedEvent." + ], + "discriminator": [ + 29 + ] + }, + { + "name": "LiquidationTransferSummaryEvent", + "docs": [ + "MarketEvent::LiquidationTransferSummary Borsh variant 30.", + "Payload type: LiquidationTransferSummaryEvent." + ], + "discriminator": [ + 30 + ] + }, + { + "name": "LiquidationTransferEvent", + "docs": [ + "MarketEvent::LiquidationTransfer Borsh variant 31.", + "Payload type: LiquidationTransferEvent." + ], + "discriminator": [ + 31 + ] + }, + { + "name": "LiquidationEvent", + "docs": [ + "MarketEvent::Liquidation Borsh variant 32.", + "Payload type: LiquidationEvent." + ], + "discriminator": [ + 32 + ] + }, + { + "name": "CloseMatchedPositionsEvent", + "docs": [ + "MarketEvent::CloseMatchedPositions Borsh variant 33.", + "Payload type: CloseMatchedPositionsEvent." + ], + "discriminator": [ + 33 + ] + }, + { + "name": "NameSuccessorEvent", + "docs": [ + "MarketEvent::NameSuccessor Borsh variant 34.", + "Payload type: NameSuccessorEvent." + ], + "discriminator": [ + 34 + ] + }, + { + "name": "ClaimAuthorityEvent", + "docs": [ + "MarketEvent::ClaimAuthority Borsh variant 35.", + "Payload type: ClaimAuthorityEvent." + ], + "discriminator": [ + 35 + ] + }, + { + "name": "WithdrawStateTransitionEvent", + "docs": [ + "MarketEvent::WithdrawStateTransition Borsh variant 36.", + "Payload type: WithdrawStateTransitionEvent." + ], + "discriminator": [ + 36 + ] + }, + { + "name": "PnLEvent", + "docs": [ + "MarketEvent::PnL Borsh variant 37.", + "Payload type: PnLEvent." + ], + "discriminator": [ + 37 + ] + }, + { + "name": "StopLossPlacedEvent", + "docs": [ + "MarketEvent::StopLossPlaced Borsh variant 38.", + "Payload type: StopLossPlacedEvent." + ], + "discriminator": [ + 38 + ] + }, + { + "name": "StopLossCancelledEvent", + "docs": [ + "MarketEvent::StopLossCancelled Borsh variant 39.", + "Payload type: StopLossCancelledEvent." + ], + "discriminator": [ + 39 + ] + }, + { + "name": "StopLossExecutedEvent", + "docs": [ + "MarketEvent::StopLossExecuted Borsh variant 40.", + "Payload type: StopLossExecutedEvent." + ], + "discriminator": [ + 40 + ] + }, + { + "name": "ExchangeStatusChangedEvent", + "docs": [ + "MarketEvent::ExchangeStatusChanged Borsh variant 41.", + "Payload type: ExchangeStatusChangedEvent." + ], + "discriminator": [ + 41 + ] + }, + { + "name": "TraderCapabilitiesEnabledEvent", + "docs": [ + "MarketEvent::TraderCapabilitiesEnabled Borsh variant 42.", + "Payload type: TraderCapabilitiesEnabledEvent." + ], + "discriminator": [ + 42 + ] + }, + { + "name": "TraderFundsWithdrawnFeePaymentEvent", + "docs": [ + "MarketEvent::TraderFundsWithdrawnFeePayment Borsh variant 43.", + "Payload type: TraderFundsWithdrawnFeePaymentEvent." + ], + "discriminator": [ + 43 + ] + }, + { + "name": "OrderPacketEvent", + "docs": [ + "MarketEvent::OrderPacket Borsh variant 44.", + "Payload type: OrderPacketEvent." + ], + "discriminator": [ + 44 + ] + }, + { + "name": "SetPermissionEvent", + "docs": [ + "MarketEvent::SetPermission Borsh variant 45.", + "Payload type: SetPermissionEvent." + ], + "discriminator": [ + 45 + ] + }, + { + "name": "AuthorityChangedEvent", + "docs": [ + "MarketEvent::AuthorityChanged Borsh variant 46.", + "Payload type: AuthorityChangedEvent." + ], + "discriminator": [ + 46 + ] + }, + { + "name": "TraderDelegatedEvent", + "docs": [ + "MarketEvent::TraderDelegated Borsh variant 47.", + "Payload type: TraderDelegatedEvent." + ], + "discriminator": [ + 47 + ] + }, + { + "name": "AdminParameterUpdatedEvent", + "docs": [ + "MarketEvent::AdminParameterUpdated Borsh variant 48.", + "Payload type: AdminParameterUpdatedEvent." + ], + "discriminator": [ + 48 + ] + }, + { + "name": "TraderFeesUpdatedEvent", + "docs": [ + "MarketEvent::TraderFeesUpdated Borsh variant 49.", + "Payload type: TraderFeesUpdatedEvent." + ], + "discriminator": [ + 49 + ] + }, + { + "name": "MarketClosedEvent", + "docs": [ + "MarketEvent::MarketClosed Borsh variant 50.", + "Payload type: MarketClosedEvent." + ], + "discriminator": [ + 50 + ] + }, + { + "name": "MarketDeletedEvent", + "docs": [ + "MarketEvent::MarketDeleted Borsh variant 51.", + "Payload type: MarketDeletedEvent." + ], + "discriminator": [ + 51 + ] + }, + { + "name": "EscrowAccountCreatedEvent", + "docs": [ + "MarketEvent::EscrowAccountCreated Borsh variant 52.", + "Payload type: EscrowAccountCreatedEvent." + ], + "discriminator": [ + 52 + ] + }, + { + "name": "EscrowRequestCreatedEvent", + "docs": [ + "MarketEvent::EscrowRequestCreated Borsh variant 53.", + "Payload type: EscrowRequestCreatedEvent." + ], + "discriminator": [ + 53 + ] + }, + { + "name": "EscrowRequestAcceptedEvent", + "docs": [ + "MarketEvent::EscrowRequestAccepted Borsh variant 54.", + "Payload type: EscrowRequestAcceptedEvent." + ], + "discriminator": [ + 54 + ] + }, + { + "name": "EscrowRequestCancelledEvent", + "docs": [ + "MarketEvent::EscrowRequestCancelled Borsh variant 55.", + "Payload type: EscrowRequestCancelledEvent." + ], + "discriminator": [ + 55 + ] + }, + { + "name": "SplinePriceUpdatedWithOrderingEvent", + "docs": [ + "MarketEvent::SplinePriceUpdatedWithOrdering Borsh variant 56.", + "Payload type: SplinePriceUpdatedWithOrderingEvent." + ], + "discriminator": [ + 56 + ] + }, + { + "name": "SplineParametersUpdatedWithOrderingEvent", + "docs": [ + "MarketEvent::SplineParametersUpdatedWithOrdering Borsh variant 57.", + "Payload type: SplineParametersUpdatedWithOrderingEvent." + ], + "discriminator": [ + 57 + ] + }, + { + "name": "TriggerOrderPlacedEvent", + "docs": [ + "MarketEvent::TriggerOrderPlaced Borsh variant 58.", + "Payload type: TriggerOrderPlacedEvent." + ], + "discriminator": [ + 58 + ] + }, + { + "name": "TriggerOrderCancelledEvent", + "docs": [ + "MarketEvent::TriggerOrderCancelled Borsh variant 59.", + "Payload type: TriggerOrderCancelledEvent." + ], + "discriminator": [ + 59 + ] + }, + { + "name": "TriggerOrderExecutedEvent", + "docs": [ + "MarketEvent::TriggerOrderExecuted Borsh variant 60.", + "Payload type: TriggerOrderExecutedEvent." + ], + "discriminator": [ + 60 + ] + }, + { + "name": "PingInvalidatedEvent", + "docs": [ + "MarketEvent::PingInvalidated Borsh variant 61.", + "Payload type: PingInvalidatedEvent." + ], + "discriminator": [ + 61 + ] + }, + { + "name": "PingActivatedEvent", + "docs": [ + "MarketEvent::PingActivated Borsh variant 62.", + "Payload type: PingActivatedEvent." + ], + "discriminator": [ + 62 + ] + }, + { + "name": "SplinePositionLimitsConfigUpdatedEvent", + "docs": [ + "MarketEvent::SplinePositionLimitsConfigUpdated Borsh variant 63.", + "Payload type: SplinePositionLimitsConfigUpdatedEvent." + ], + "discriminator": [ + 63 + ] + }, + { + "name": "MarketTombstonedEvent", + "docs": [ + "MarketEvent::MarketTombstoned Borsh variant 64.", + "Payload type: MarketTombstonedEvent." + ], + "discriminator": [ + 64 + ] + }, + { + "name": "ShutdownClosePositionsEvent", + "docs": [ + "MarketEvent::ShutdownClosePositions Borsh variant 65.", + "Payload type: ShutdownClosePositionsEvent." + ], + "discriminator": [ + 65 + ] + }, + { + "name": "OrderResidualDiscardedEvent", + "docs": [ + "MarketEvent::OrderResidualDiscarded Borsh variant 66.", + "Payload type: OrderResidualDiscardedEvent." + ], + "discriminator": [ + 66 + ] + }, + { + "name": "SpotCollateralDepositedEvent", + "docs": [ + "MarketEvent::SpotCollateralDeposited Borsh variant 67.", + "Payload type: SpotCollateralDepositedEvent." + ], + "discriminator": [ + 67 + ] + }, + { + "name": "SpotCollateralWithdrawnEvent", + "docs": [ + "MarketEvent::SpotCollateralWithdrawn Borsh variant 68.", + "Payload type: SpotCollateralWithdrawnEvent." + ], + "discriminator": [ + 68 + ] + }, + { + "name": "SpotCollateralLiquidatedEvent", + "docs": [ + "MarketEvent::SpotCollateralLiquidated Borsh variant 69.", + "Payload type: SpotCollateralLiquidatedEvent." + ], + "discriminator": [ + 69 + ] + }, + { + "name": "RestartAcknowledgedEvent", + "docs": [ + "MarketEvent::RestartAcknowledged Borsh variant 70.", + "Payload type: RestartAcknowledgedEvent." + ], + "discriminator": [ + 70 + ] + }, + { + "name": "AdditionalFeeAccruedEvent", + "docs": [ + "MarketEvent::AdditionalFeeAccrued Borsh payload." + ], + "discriminator": [ + 71 + ] + }, + { + "name": "SpotAssetPriceUpdatedEvent", + "docs": [ + "MarketEvent::SpotAssetPriceUpdated Borsh variant 72.", + "Payload type: SpotAssetPriceUpdatedEvent." + ], + "discriminator": [ + 72 + ] } ], - "events": [], "errors": [] -} +} \ No newline at end of file diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/market.rs b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/market.rs new file mode 100644 index 000000000..784d13f43 --- /dev/null +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/market.rs @@ -0,0 +1,617 @@ +use core::fmt::Display; + +use phoenix_rise_accounts::{ + PhoenixAccountDecodeError, + orderbook::{ORDERBOOK_CAPACITY, Orderbook}, + pda::{derive_permission_address, derive_spline_collection_address}, + perp_asset_map::{PerpAssetMap, PerpAssetMetadataEntry}, + spline_collection::SplineCollection, + trader::TraderHeader, +}; +use solana_account::Account; +use solana_clock::Clock; +use solana_commitment_config::CommitmentConfig; +use solana_instruction::Instruction; +use solana_pubkey::Pubkey; + +use super::{ + state_builder::{ + Exchange, PHOENIX_GLOBAL_CONFIG, PHOENIX_PROGRAM_ID, Sandbox, hydrate, local_account, + log_authority, phoenix_instruction, symbol_bytes, + }, + trader::{BboView, HAWKEYE_PROGRAM_ID, bbo}, +}; +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + surfnet::{remote::SurfnetRemoteClient, svm::SurfnetSvm}, +}; + +/// Crossed resting orders the uncross crank matches in one call: about 12,000 compute units each, +/// so 64 stay well inside a transaction's 1.4 million. +const UNCROSS_MATCH_LIMIT: u64 = 64; +/// Each side holds up to `ORDERBOOK_CAPACITY` resting orders and a match between two removes at +/// least one; spline fills make this a budget, not a guarantee. +const MAX_UNCROSS_CRANKS: usize = 2 * ORDERBOOK_CAPACITY / UNCROSS_MATCH_LIMIT as usize; + +pub(crate) fn invalid_perp_asset_map( + perp_asset_map: &Pubkey, + reason: impl Display, +) -> SurfpoolError { + SurfpoolError::invalid_account_data( + perp_asset_map, + "Expected the Phoenix Eternal PerpAssetMap", + Some(reason), + ) +} + +/// Every market the PerpAssetMap lists, in storage order. +pub(crate) fn map_entries( + perp_asset_map: &Pubkey, + data: &[u8], +) -> SurfpoolResult> { + let invalid = |e: PhoenixAccountDecodeError| invalid_perp_asset_map(perp_asset_map, e); + PerpAssetMap::try_from_account_bytes(data) + .map_err(invalid)? + .iter() + .collect::>() + .map_err(invalid) +} + +/// Every market the PerpAssetMap lists, by symbol. +pub fn phoenix_markets(perp_asset_map: Pubkey, account: &Account) -> SurfpoolResult> { + if account.owner != PHOENIX_PROGRAM_ID { + return Err(SurfpoolError::invalid_account_owner( + perp_asset_map, + None::, + )); + } + let mut markets: Vec = map_entries(&perp_asset_map, &account.data)? + .iter() + .map(Market::from_entry) + .collect(); + markets.sort_unstable_by(|left, right| left.symbol.cmp(&right.symbol)); + + Ok(markets) +} + +/// One market as the PerpAssetMap describes it. +#[derive(Clone, Debug)] +pub struct Market { + pub symbol: String, + pub asset_id: u64, + pub orderbook: Pubkey, + pub splines: Pubkey, + pub tick_size: u64, + pub base_lot_decimals: i8, + pub mark_ticks: u64, + /// The most base lots one liquidation may close. + pub max_liquidation_size: u64, + /// Maintenance, backstop and high-risk factors, in basis points of the initial margin. + pub risk_factors: [u16; 3], + /// The keys that report this market's oracle prices. + pub oracle_keys: Vec, + pub latest_oracle_timestamp: u64, +} + +impl Market { + fn from_entry(entry: &PerpAssetMetadataEntry) -> Self { + let params = entry.metadata.static_market_params(); + let orderbook = Pubkey::new_from_array(params.market_account); + let mark = entry.metadata.oracle_price().mark_price; + Self { + symbol: entry.symbol.as_str().to_string(), + asset_id: u64::from(params.asset_id()), + orderbook, + splines: derive_spline_collection_address(&PHOENIX_PROGRAM_ID, &orderbook), + tick_size: params.tick_size.as_inner(), + base_lot_decimals: params.base_lot_decimals, + mark_ticks: mark.price.ticks.as_inner(), + max_liquidation_size: entry.metadata.risk_params().max_liquidation_size.as_inner(), + risk_factors: entry.metadata.risk_params().risk_factors, + oracle_keys: mark + .oracle_data + .iter() + .map(|oracle| Pubkey::new_from_array(oracle.oracle_pubkey)) + .filter(|key| *key != Pubkey::default()) + .collect(), + latest_oracle_timestamp: mark + .oracle_last_updated_timestamps + .iter() + .copied() + .max() + .unwrap_or_default(), + } + } + + pub fn find(perp_asset_map: &Pubkey, data: &[u8], symbol: &str) -> SurfpoolResult { + let entry = map_entries(perp_asset_map, data)? + .into_iter() + .find(|entry| entry.symbol.matches(symbol)) + .ok_or_else(|| SurfpoolError::internal(format!("Phoenix lists no market {symbol}")))?; + Ok(Self::from_entry(&entry)) + } + + /// The oracle price for `ticks`, as a value and a decimal exponent: USD per base unit is + /// ticks × tick size × 10^(base lot decimals − 6). + pub fn oracle_price(&self, ticks: u64) -> SurfpoolResult<(u64, u8)> { + let overflow = + || SurfpoolError::internal(format!("{ticks} ticks overflow {}'s price", self.symbol)); + let value = ticks.checked_mul(self.tick_size).ok_or_else(overflow)?; + let decimals = 6 - i32::from(self.base_lot_decimals); + if decimals >= 0 { + Ok((value, decimals as u8)) + } else { + let scale = 10_u64 + .checked_pow(decimals.unsigned_abs()) + .ok_or_else(overflow)?; + Ok((value.checked_mul(scale).ok_or_else(overflow)?, 0)) + } + } +} + +/// A market with everything a move needs in the local VM: its accounts, the keys that report its +/// oracle prices with their permissions, and the makers quoting it. +pub struct MarketContext { + pub exchange: Exchange, + pub market: Market, + oracle_permissions: Vec, + makers: Vec, + next_timestamp: u64, +} + +/// An enabled maker on the market's spline collection. +struct Maker { + trader: Pubkey, + /// The wallet that signs for the Trader. + authority: Pubkey, + /// The last price sequence number it used. + sequence: u64, + /// The slot its updates report. + update_slot: u64, +} + +impl MarketContext { + pub async fn load( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + symbol: &str, + ) -> SurfpoolResult { + let mut contexts = Self::load_all(svm, remote_ctx, &[symbol]).await?; + Ok(contexts.remove(0)) + } + + /// The markets of `symbols`, in that order. Their accounts come in two batched fetches, the + /// books and spline collections first and then what those name, rather than market by market. + pub async fn load_all( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + symbols: &[&str], + ) -> SurfpoolResult> { + let exchange = Exchange::load(svm, remote_ctx).await?; + hydrate( + svm, + remote_ctx, + &[ + exchange.perp_asset_map, + exchange.global_trader_index, + exchange.active_trader_buffer, + ], + ) + .await?; + let map = local_account(svm, &exchange.perp_asset_map)?; + let markets = symbols + .iter() + .map(|symbol| { + let market = Market::find(&exchange.perp_asset_map, &map.data, symbol)?; + if market.oracle_keys.is_empty() { + return Err(SurfpoolError::internal(format!( + "Phoenix market {symbol} has no oracle that reports its price" + ))); + } + Ok(market) + }) + .collect::>>()?; + let books: Vec = markets + .iter() + .flat_map(|market| [market.orderbook, market.splines]) + .collect(); + hydrate(svm, remote_ctx, &books).await?; + + // A spline refuses an update slot below its last one, and a running surfnet's slot trails + // the mainnet slot that last update came from. + let slot = svm.inner.get_sysvar::().slot; + let oracle_authority = Pubkey::new_from_array(exchange.config.oracle_authority()); + let mut loaded = Vec::with_capacity(markets.len()); + let mut dependencies = Vec::new(); + for market in markets { + let splines = local_account(svm, &market.splines)?; + let collection = + SplineCollection::try_from_account_bytes(&splines.data).map_err(|e| { + SurfpoolError::invalid_account_data( + market.splines, + "Expected a Phoenix spline collection", + Some(e), + ) + })?; + let enabled: Vec<(Pubkey, u64, u64)> = collection + .splines() + .filter_map(Result::ok) + .filter(|spline| spline.is_enabled()) + .map(|spline| { + ( + Pubkey::new_from_array(spline.trader()), + spline.user_price_sequence_number(), + spline.user_update_slot().max(slot), + ) + }) + .collect(); + let oracle_permissions: Vec = market + .oracle_keys + .iter() + .map(|key| derive_permission_address(&PHOENIX_PROGRAM_ID, &oracle_authority, key)) + .collect(); + dependencies.extend(enabled.iter().map(|(trader, ..)| *trader)); + dependencies.extend(&oracle_permissions); + loaded.push((market, enabled, oracle_permissions)); + } + hydrate(svm, remote_ctx, &dependencies).await?; + + // An oracle refuses a report older than its last one. + let clock_ms = (svm.inner.get_sysvar::().unix_timestamp.max(0) as u64) * 1000; + let config = local_account(svm, &PHOENIX_GLOBAL_CONFIG)?; + loaded + .into_iter() + .map(|(market, enabled, oracle_permissions)| { + let mut makers = Vec::with_capacity(enabled.len()); + for (trader, sequence, update_slot) in enabled { + let account = local_account(svm, &trader)?; + let header = + TraderHeader::try_read_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data( + trader, + "Expected a Phoenix Trader", + Some(e), + ) + })?; + makers.push(Maker { + trader, + authority: Pubkey::new_from_array(header.authority), + sequence, + update_slot, + }); + } + Ok(Self { + next_timestamp: clock_ms.max(market.latest_oracle_timestamp) + 1, + exchange: Exchange::from_config(&config)?, + market, + oracle_permissions, + makers, + }) + }) + .collect() + } + + /// Every oracle key reporting `ticks`, which moves the mark there. Each call reports later + /// than the previous one. + pub fn oracle_reports(&mut self, ticks: u64) -> SurfpoolResult> { + checked_ticks(ticks)?; + let (value, expo) = self.market.oracle_price(ticks)?; + let price = serde_json::json!({ "value": value, "expo": expo }); + let symbol = symbol_bytes(&self.market.symbol); + let timestamp = self.next_timestamp; + self.next_timestamp += 1; + self.market + .oracle_keys + .iter() + .zip(&self.oracle_permissions) + .map(|(key, permission)| { + phoenix_instruction( + "UpdateOraclePricesWithOrdering", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("authority", *key), + ("permissionAccount", *permission), + ("perpAssetMap", self.exchange.perp_asset_map), + ], + &serde_json::json!({ + "params": { + "updateTimestamp": timestamp, + "updates": [{ + "perpAssetId": { "symbolBytes": symbol }, + "newExchangePerpPrice": price, + "newExchangeSpotPrice": price, + }], + "flags": { "flags": 0 }, + } + }), + ) + }) + .collect() + } + + /// Every enabled maker re-centring its spline on `ticks`. + pub fn spline_moves(&mut self, ticks: u64) -> SurfpoolResult> { + checked_ticks(ticks)?; + let mut instructions = Vec::with_capacity(self.makers.len()); + for maker in &mut self.makers { + maker.sequence += 1; + let client_order_id = [0_u8; 16]; + instructions.push(phoenix_instruction( + "UpdateSplinePrice", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("signerAccount", maker.authority), + ("traderAccount", maker.trader), + ("splineAccount", self.market.splines), + ("orderbook", self.market.orderbook), + ], + &serde_json::json!({ + "params": { + "newMidPrice": ticks, + "userUpdateSlot": maker.update_slot, + "refreshRegions": true, + "userSequenceNumber": maker.sequence, + "clientOrderId": client_order_id, + "overrideSequenceNumber": false, + } + }), + )?); + } + Ok(instructions) + } + + /// The crank that matches resting orders a move left crossed. + pub fn uncross(&self) -> SurfpoolResult { + phoenix_instruction( + "UncrossCrank", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("perpAssetMap", self.exchange.perp_asset_map), + ("globalTraderIndex", self.exchange.global_trader_index), + ("activeTraderBuffer", self.exchange.active_trader_buffer), + ("orderbook", self.market.orderbook), + ("splines", self.market.splines), + ], + &serde_json::json!({ "params": { "matchLimit": UNCROSS_MATCH_LIMIT } }), + ) + } + + /// The move to `ticks` in `sandbox`: oracle reports and spline moves. Resting orders the moved + /// splines cross stay until [`MarketContext::uncross_in`]. + pub fn move_in(&mut self, sandbox: &mut Sandbox, ticks: u64) -> SurfpoolResult<()> { + let mut instructions = self.oracle_reports(ticks)?; + instructions.extend(self.spline_moves(ticks)?); + sandbox.run(&instructions) + } + + /// The uncross crank in `sandbox` until [`uncross_done`]. Hawkeye must be in the local VM. + pub fn uncross_in(&mut self, sandbox: &mut Sandbox) -> SurfpoolResult<()> { + let uncross = self.uncross()?; + let mut before = self.book_top(sandbox)?; + for _ in 0..MAX_UNCROSS_CRANKS { + sandbox.run(std::slice::from_ref(&uncross))?; + let after = self.book_top(sandbox)?; + if uncross_done(&before, &after) { + return Ok(()); + } + before = after; + } + Err(SurfpoolError::internal(format!( + "the {} book stayed crossed after {MAX_UNCROSS_CRANKS} uncross cranks", + self.market.symbol + ))) + } + + /// Hawkeye's best bid and ask in `sandbox`, with the number of resting orders in the book. + fn book_top(&self, sandbox: &mut Sandbox) -> SurfpoolResult<(BboView, usize)> { + let account = sandbox.account(&self.market.orderbook)?; + let book = Orderbook::try_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data( + self.market.orderbook, + "Expected a Phoenix orderbook", + Some(e), + ) + })?; + let resting = book.num_bids() + book.num_asks(); + Ok((bbo(sandbox, &self.exchange, &self.market)?, resting)) + } +} + +/// Whether a crank that took the book from `before` to `after` was the last: the book is uncrossed, +/// or the crank matched nothing because it leaves that cross alone. Every match moves a price or +/// removes a resting order; prices alone miss fills that all sit at the best price. +fn uncross_done(before: &(BboView, usize), after: &(BboView, usize)) -> bool { + let ((before, before_orders), (after, after_orders)) = (before, after); + !after.crossed() + || (after.best_bid_ticks, after.best_ask_ticks, after_orders) + == (before.best_bid_ticks, before.best_ask_ticks, before_orders) +} + +/// The writes that move one market to `target_ticks` the way mainnet does: oracle reports and +/// makers' splines re-centred. Resting orders the new price crosses stay until [`uncross_market`]. +pub async fn move_market( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + symbol: &str, + target_ticks: u64, +) -> SurfpoolResult> { + let mut context = MarketContext::load(svm, remote_ctx, symbol).await?; + let mut sandbox = Sandbox::new(svm); + context.move_in(&mut sandbox, target_ticks)?; + sandbox.writes() +} + +/// The writes of Phoenix's uncross crank matching one market's crossed resting orders, which anyone +/// can send after a move. +pub async fn uncross_market( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + symbol: &str, +) -> SurfpoolResult> { + let mut context = MarketContext::load(svm, remote_ctx, symbol).await?; + hydrate(svm, remote_ctx, &[HAWKEYE_PROGRAM_ID]).await?; + let mut sandbox = Sandbox::new(svm); + context + .uncross_in(&mut sandbox) + .map_err(explain_move_failure)?; + sandbox.writes() +} + +/// Phoenix's uncross crank breaks on books that do not match the local trader index, which +/// happens when surfnet fetched the two at different times; the raw program panic says nothing. +pub(crate) fn explain_move_failure(error: SurfpoolError) -> SurfpoolError { + let text = error.to_string(); + if !text.contains("Uncross Crank") { + return error; + } + SurfpoolError::internal(format!( + "Phoenix's uncross crank failed, which happens when the local order books and Phoenix's \ + trader index were fetched at different times; restart the surfnet (or reset the Phoenix \ + accounts) and play again. {text}" + )) +} + +fn checked_ticks(ticks: u64) -> SurfpoolResult<()> { + if ticks == 0 || ticks > u64::from(u32::MAX) { + return Err(SurfpoolError::internal(format!( + "{ticks} ticks are outside Phoenix's price range 1..={}", + u32::MAX + ))); + } + Ok(()) +} + +#[cfg(test)] +pub(crate) mod tests { + use base64::{Engine, prelude::BASE64_STANDARD}; + use phoenix_rise_accounts::perp_asset_map::PriceComponent; + + use super::{super::state_builder::PHOENIX_PERP_ASSET_MAP, *}; + + const PERP_ASSET_MAP_LEN: usize = 1_622_064; + const SOL_PERP_ASSET_MAP_PREFIX_B64: &str = "jjZz33zvbCYBAAAAAAAAAF+FshYAAAAALAAAAAAAAAAtAAAAAQAAAAAEAAAAAAAAU09MAAAAAAAAAAAAAAAAAJIjVQMAAAAAK+yGGQAAAAAr7IYZAAAAABccAAAAAAAAK+yGGQAAAAAXHAAAAAAAACXshhkAAAAAFxwAAAAAAAAk7IYZAAAAABccAAAAAAAAIuyGGQAAAAAWHAAAAAAAACnshhkAAAAAGBwAAAAAAABkAAAAAAAAABkAAAAAAAAAK+yGGQAAAAAAAAAAAAAAAHUAAAAAAAAAdwEAAAAAAAByAQAAAAAAACvshhkAAAAAERwAAAAAAAAl7IYZAAAAABEcAAAAAAAAJOyGGQAAAAASHAAAAAAAACLshhkAAAAAERwAAAAAAAAp7IYZAAAAABEcAAAAAAAAZAAAAAAAAAAZAAAAAAAAACvshhkAAAAAGRwAAAAAAABkAAAAAAAAAGQAAAAAAAAAcgEAAAAAAAAk7IYZAAAAABkcAAAAAAAAJOyGGQAAAAAaHAAAAAAAAPjrhhkAAAAAFBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAQAAAAAAAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgIBAAAAAAACAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgEAAAAAAAICAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAQAAAAAAAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgIBAAAAAAACAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2d/uHkzTMEI+nE0Ymaus9KEPf4oJXVEtWcWP29rQOywAAAAAAAAAAPBv/oFQVyUzn/BwmYuYTfvalmqearF4UMH8Xu10jPi1AAAAAAAAAAC9eIxYdxEuqtIqoFaGCUmDIS3Ki2887zwxOIzii37LZgAAAAAAAAAAp5Qc5gqxc5w9o5gk0/YHpMTClPTT8zaXjCVPspfSfxwAAAAAAAAAAIj2IrJxxvwcSeH0Zi3/xWcn5icVCYuh/OncuwHqSRBjAAAAAAAAAAD0AQEAAAAAACvshhkAAAAAnI6GGQAAAAAAAAAAAAAAAFRyhhkAAAAA5wAF8p4BAAAh9gTyngEAAFj1BPKeAQAAxu8E8p4BAAC6/ATyngEAAAAAAAAAAAAAAAAAAAAAAABZQzBUxbJLqOqoIX/f+QNvuZxLwZEZqXGqSDPsYEwjH2QAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATMvqAQAAAAAPAAAAAAAAABAnAAAAAAAATcvqAQAAAAABAAAAAAAAABAnAAAAAAAATsvqAQAAAAABAAAAAAAAABAnAAAAAAAAT8vqAQAAAAABAAAAAAAAABAnAAAAAAAAECcAAAAAAABQwwAAAAAAAKCGAQAAAAAAZAAAAAAAAAAgoQcAAAAAAMgAAAAAAAAAQEIPAAAAAAAsAQAAAAAAAICWmAAAAAAAkAEAAAAAAACIE9AH6ANMHWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCQAAAAAAAJDaOWoAAAAAatw5agAAAAAQDgAAAAAAAIBRAQAAAAAAogYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnuYhAAAAAABMy+oBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQlRDAAAAAAAAAAAAAAAAAA=="; + + pub(crate) fn perp_asset_map_fixture() -> Vec { + let prefix = BASE64_STANDARD + .decode(SOL_PERP_ASSET_MAP_PREFIX_B64) + .unwrap(); + let mut data = vec![0_u8; PERP_ASSET_MAP_LEN]; + data[..prefix.len()].copy_from_slice(&prefix); + data[24..26].copy_from_slice(&1_u16.to_le_bytes()); + data[32..36].copy_from_slice(&1_u32.to_le_bytes()); + data[36..40].copy_from_slice(&0_u32.to_le_bytes()); + data + } + + pub(crate) fn perp_asset_map_account() -> Account { + Account { + lamports: 1, + data: perp_asset_map_fixture(), + owner: PHOENIX_PROGRAM_ID, + executable: false, + rent_epoch: 0, + } + } + + fn market(tick_size: u64, base_lot_decimals: i8) -> Market { + Market { + symbol: "SOL".to_string(), + asset_id: 0, + orderbook: Pubkey::new_unique(), + splines: Pubkey::new_unique(), + tick_size, + base_lot_decimals, + mark_ticks: 1, + max_liquidation_size: 1, + risk_factors: [5_000, 2_000, 1_000], + oracle_keys: vec![Pubkey::new_unique()], + latest_oracle_timestamp: 0, + } + } + + #[test] + fn converts_ticks_to_the_oracle_price() { + // SOL: tick size 100, base lot decimals 2, so 10730 ticks are 107.30 USD. + assert_eq!(market(100, 2).oracle_price(10_730).unwrap(), (1_073_000, 4)); + assert_eq!(market(1, -3).oracle_price(5).unwrap(), (5, 9)); + assert_eq!(market(3, 8).oracle_price(7).unwrap(), (2_100, 0)); + assert!(market(u64::MAX, 2).oracle_price(2).is_err()); + } + + #[test] + fn lists_the_markets_a_caller_can_name() { + let markets = phoenix_markets(PHOENIX_PERP_ASSET_MAP, &perp_asset_map_account()).unwrap(); + let [sol] = markets.as_slice() else { + panic!("the fixture lists one market: {markets:?}"); + }; + assert_eq!( + ( + sol.symbol.as_str(), + sol.orderbook, + sol.tick_size, + sol.base_lot_decimals, + &sol.risk_factors[..2], + ), + ( + "SOL", + Pubkey::from_str_const("71Si24E4uc3oCaPbPZTozC1ptSNNqygjjebxSmErSsC2"), + 100, + 2, + &[5_000, 2_000][..], + ) + ); + let foreign = Account { + owner: Pubkey::new_unique(), + ..perp_asset_map_account() + }; + assert!(phoenix_markets(PHOENIX_PERP_ASSET_MAP, &foreign).is_err()); + } + + #[test] + fn configuration_finds_a_market_without_oracles() { + let mut data = perp_asset_map_fixture(); + // The fixture's only market starts after the 48-byte header with its 16-byte symbol. + let range = 64..64 + size_of::(); + let mut price: PriceComponent = bytemuck::pod_read_unaligned(&data[range.clone()]); + for oracle in &mut price.mark_price.oracle_data { + oracle.oracle_pubkey = [0; 32]; + } + data[range].copy_from_slice(bytemuck::bytes_of(&price)); + let market = Market::find(&PHOENIX_PERP_ASSET_MAP, &data, "SOL").unwrap(); + assert!(market.oracle_keys.is_empty()); + } + + fn book_at(bid: u64, ask: u64, orders: usize) -> (BboView, usize) { + let mut view: BboView = bytemuck::Zeroable::zeroed(); + view.best_bid_ticks = bid; + view.best_ask_ticks = ask; + (view, orders) + } + + #[test] + fn uncross_cranks_stop_once_the_book_uncrosses_or_a_crank_changes_nothing() { + let crossed = book_at(120, 100, 200); + assert!( + !uncross_done(&crossed, &book_at(110, 100, 200)), + "bid moved" + ); + assert!( + !uncross_done(&crossed, &book_at(120, 105, 200)), + "ask moved" + ); + assert!( + !uncross_done(&crossed, &book_at(120, 100, 136)), + "filled at the best price" + ); + assert!(uncross_done(&crossed, &book_at(99, 100, 200)), "uncrossed"); + assert!(uncross_done(&crossed, &book_at(100, 100, 200)), "locked"); + assert!(uncross_done(&crossed, &book_at(0, 100, 200)), "no bids"); + assert!(uncross_done(&crossed, &book_at(120, 0, 200)), "no asks"); + assert!(uncross_done(&crossed, &crossed), "no progress"); + } +} diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/mod.rs b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/mod.rs index 75dcf65a6..3731450a3 100644 --- a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/mod.rs +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/mod.rs @@ -1,2 +1,3 @@ -pub mod collateral; +pub mod market; pub mod state_builder; +pub mod trader; diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/overrides.yaml b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/overrides.yaml index c0f6ec721..9a7d3a51f 100644 --- a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/overrides.yaml +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/overrides.yaml @@ -9,33 +9,51 @@ tags: - risk templates: - - id: phoenix-trader-collateral-stress - name: Phoenix Trader Collateral Stress + - id: phoenix-oracle-price + name: Phoenix Oracle Price description: >- - Set exact signed quote-lot collateral on a Phoenix Trader, and on its record in the local - GlobalTraderIndex when the index lists it. - idl_account_name: Trader + Set one Phoenix Eternal market's oracle price, in ticks or as a percentage change. Only the + oracles move: the order book and the makers' quotes stay where they are. + idl_account_name: PerpAssetMap properties: - - path: traderState.quoteLotCollateral - label: Target Collateral in Quote Lots - description: Exact signed quote-lot collateral encoded as a decimal string + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + - path: target_ticks + type: input + value_type: string + label: Oracle Price (ticks) + description: The new price in ticks, at least 1; leave empty to give a percentage instead + - path: change_percent + type: input + value_type: string + label: Change (%) + description: >- + Change from the current mark in percent, for example -5 or 2.5; leave + empty to give ticks instead llm_context: | - This prepares collateral stress state. It does not execute liquidation. - Only lower collateral; raising it needs a real deposit into the global vault. - The target is quoteLotCollateral, which excludes the unrealized PnL and funding Phoenix adds - for effective collateral: lowering it by N quote lots lowers effective collateral by N. - For a liquidation cascade, put this override at slot 0 and phoenix-direct-mark-risk-shock at - slot 1 in one create_scenario call. - Set fetchBeforeUse: true so the Trader is fetched from the upstream datasource first; an - override on a Trader that is not in the local VM yet is skipped. - Keep override labels short. Pass traderState.quoteLotCollateral as a decimal string. + Every oracle key that reports the market sends the new price through Phoenix's own oracle + update, and Phoenix recomputes the mark from it. Nothing else moves: makers keep their + quotes and resting orders keep their prices, so a trade or liquidation right after fills + against the old book. Use phoenix-market-move and phoenix-market-uncross when the book + should follow the price. + Give exactly one of target_ticks and change_percent, as a decimal string. change_percent is + applied to the market's mark when the override plays, so moves at later slots add up. + USD per base unit = ticks * tickSize * 10^(baseLotDecimals - 6), all from + list_phoenix_markets. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. address: type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC - - id: phoenix-direct-mark-risk-shock - name: Phoenix Direct Mark Risk Shock + - id: phoenix-market-move + name: Phoenix Market Move description: >- - Set exact mark-price ticks for one market in the Phoenix Eternal PerpAssetMap. + Move one Phoenix Eternal market to a new price the way mainnet moves it: oracle reports and + maker liquidity. Resting orders the new price crosses stay until phoenix-market-uncross. idl_account_name: PerpAssetMap properties: - path: symbol @@ -46,30 +64,175 @@ templates: - path: target_ticks type: input value_type: string - label: Target Mark Ticks - description: Exact mark-price ticks, at least 1, encoded as a decimal string + label: Target Price (ticks) + description: The new price in ticks, at least 1, encoded as a decimal string + llm_context: | + This runs Phoenix's own instructions: every oracle key that reports the market sends the new + price and every active market maker re-centres its spline liquidity on it. Resting orders on + the other side of the new price stay crossed, and trades and liquidations fill against them + at their old prices, until phoenix-market-uncross matches them: add it right after this + override, in the same slot. + USD per base unit = ticks * tickSize * 10^(baseLotDecimals - 6), all from + list_phoenix_markets; compute a relative move from markTicks there. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-market-uncross + name: Phoenix Market Uncross + description: >- + Match the resting orders a price move left crossed in one Phoenix Eternal market with + Phoenix's uncross crank, which anyone can send. + idl_account_name: PerpAssetMap + properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + llm_context: | + Runs Phoenix's uncross crank on the market until the book is uncrossed or a crank matches + nothing more. After phoenix-market-move the makers quote around the new price while resting + orders on the other side of it stay crossed; this matches them, so the best bid and ask + end up around the target. Add it right after phoenix-market-move, in the same slot. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-trader-cancel-orders + name: Phoenix Trader Cancel Orders + description: >- + Cancel all resting orders of a Phoenix Eternal trader, as the trader, on every market, on one + market or on several. On every market it takes the trader out of Phoenix's active trader + index. Positions stay open. + idl_account_name: PerpAssetMap + properties: + - path: trader + type: input + value_type: string + label: Trader Account + description: The trader's Trader account address, as list_phoenix_trader_positions takes it + - path: symbol + type: input + value_type: string + label: Market Symbols + description: Empty for every market, one symbol such as SOL, or several separated by commas such as SOL, BTC, ETH; symbols from list_phoenix_markets + llm_context: | + Runs Phoenix's CancelAll as the trader on every market where it holds a position or resting + orders when symbol is empty, or only on the markets symbol names: one, such as SOL, or + several separated by commas, such as SOL, BTC, ETH. Canceled on every market, a trader the + active trader index lists (hot: true in list_phoenix_trader_positions) leaves it, and + Phoenix reads its state from the Trader account again; canceled on some markets, it stays + in the index while it has orders elsewhere. A cold trader has no resting orders and nothing + changes. A keeper cannot liquidate a trader with risk-increasing resting orders on any + market until it force-cancels them, which Phoenix allows any signer while the trader is + liquidatable. With phoenix-trader-collateral for the same trader, put this first, in the + same slot. Before a price move that should make a trader liquidatable, leave symbol empty, + so the move cannot fill the trader's orders and change its positions. + A cancel freezes the mark for risk in its slot: play phoenix-market-move at a later slot. + A trader Phoenix still lists after a cancel on every market, such as a spline market maker, + is refused. An unknown symbol refuses the whole override, so nothing is canceled. + Set trader to the trader's Trader account; the override's own account is the PerpAssetMap, + as for the market templates. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-trader-collateral + name: Phoenix Trader Collateral + description: >- + Set one Phoenix Eternal trader's effective collateral in USDC, which sets its distance from + liquidation, with or without resting orders. + idl_account_name: Trader + properties: + - path: effective_collateral_usdc + type: input + value_type: string + label: Effective Collateral (USDC) + description: The effective collateral when the override plays, such as 750.5; it can be negative + llm_context: | + Brings the trader's effective collateral, which Phoenix measures margin against, to + effective_collateral_usdc when the override plays: it reads Hawkeye's effective collateral + then and moves the collateral field by the difference, where Phoenix reads it, the Trader + account or, for a trader Phoenix's active trader index lists (hot: true), its record there. + Effective collateral moves one for one with that field, so the value lands even with + funding pending or resting orders. list_phoenix_trader_positions reports quote lots, and 1 + USDC is 1000000 quote lots: effective_collateral_usdc = target / 1000000. + Take the target from the tool's margin, the trader with its orders canceled, in the middle + of a range, as the state can drift a little before the keeper acts: between backstopMargin + and maintenanceMargin, one keeper liquidation makes the account healthy again; between + highRiskMargin and backstopMargin, a keeper can liquidate every position. Below + highRiskMargin a liquidation can leave the trader underwater and fail, and below 0 Phoenix + refuses it. A keeper force-cancels a trader's risk-increasing orders before it liquidates. + A scenario that also cancels the trader's orders puts phoenix-trader-cancel-orders before + this override, in the same slot: a cancel moves the effective collateral a little, and this + override lands on its target after it. + To have a trader cross on a phoenix-market-move and phoenix-market-uncross at a later slot, + call the tool with moves, put phoenix-trader-cancel-orders before this override in the same + slot, and set effective_collateral_usdc to wanted / 1000000, where wanted = + margin.effectiveCollateral - marginAfterMoves.effectiveCollateral + target in quote lots, + with target in a range of marginAfterMoves; wanted must stay above margin.maintenanceMargin + so the trader is healthy until the move. + A spline market maker is refused. A collateral above the current one is not backed by the + global vault, so a later withdrawal could take funds the vault does not count. + The account is the trader's Trader account. + Leave fetchBeforeUse false: a refetch drops earlier Phoenix changes to this trader. Call + list_phoenix_trader_positions first; it loads the trader into the surfnet. + address: + type: pubkey + + - id: phoenix-market-risk-factors + name: Phoenix Market Risk Factors + description: >- + Set one market's maintenance, backstop and high-risk factors, which decide its liquidation tiers. + idl_account_name: PerpAssetMap + properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + - path: maintenanceRiskFactor + type: input + value_type: string + label: Maintenance factor (bps) + description: >- + Basis points of the initial margin, at most the cancel order factor (7500 on mainnet); + mainnet markets use 5000 + - path: backstopRiskFactor + type: input + value_type: string + label: Backstop factor (bps) + description: >- + Basis points of the initial margin, below the maintenance factor; mainnet uses 2000 + - path: highRiskRiskFactor + type: input + value_type: string + label: High-risk factor (bps) + description: >- + Basis points of the initial margin, below the backstop factor; mainnet uses 1000 llm_context: | - This directly changes Phoenix risk state, setting the mark ticks and stamping the mark slot - with the current slot. The market's spot and perp oracle readings get the same ticks and - slot. The orderbook and spline liquidity are not moved: trades still fill at the book's real - prices, and a mark Phoenix rebuilds after a trade can move back toward the book; - list_phoenix_markets shows the mark in the local VM. - Pass target_ticks as a decimal string. - The account is the singleton PerpAssetMap, so this template carries its address. - Resolve the market with list_phoenix_markets: a market named by its orderbook address takes - the symbol listed with that address, and a relative move (such as a 40% drop) is computed - from its markTicks there. - Set fetchBeforeUse: true so the asset map is fetched from the upstream datasource first; use - false only for a later override that builds on state an earlier one prepared in the same - scenario. + The three factors set the Liquidatable, BackstopLiquidatable and HighRisk tiers; keep them in + that descending order. Phoenix also refuses a maintenance factor above the market's cancel + order risk factor (7500 on mainnet); raise that first with phoenix-market-cancel-risk-factor. + list_phoenix_markets shows the current maintenance and backstop factors. Pass every factor + as a decimal string. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. address: type: pubkey value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC - - id: phoenix-maintenance-margin-stress - name: Phoenix Maintenance Margin Stress + - id: phoenix-market-cancel-risk-factor + name: Phoenix Cancel Order Risk Factor description: >- - Set the maintenance margin risk factor for one market in the Phoenix Eternal PerpAssetMap. + Set the risk factor of one market's resting orders, the ceiling for its maintenance factor. idl_account_name: PerpAssetMap properties: - path: symbol @@ -77,29 +240,104 @@ templates: label: Market Symbol source: list_phoenix_markets description: Exact Phoenix market symbol - - path: maintenance_risk_factor_bps + - path: cancelOrderRiskFactor type: input value_type: string - label: Maintenance Risk Factor (bps) + label: Cancel order factor (bps) description: >- - Maintenance margin risk factor in basis points, above the market's backstop factor and at - most 10000, encoded as a decimal string + Basis points; mainnet markets use 7500. Example: 10000 llm_context: | - This changes how much collateral positions in one market need to stay healthy. - It does not change prices, collateral or positions, and does not execute liquidation. - The maintenance margin scales linearly with the factor. - Mainnet markets use 5000, so 10000 doubles the requirement, the most it can be raised: Phoenix - reads a factor above 10000 as 10000. - A factor at or below the market's backstop factor is refused, since it would leave no - Liquidatable tier. - Pass the factor as a decimal string. - The account is the singleton PerpAssetMap, so this template carries its address. - Resolve the market with list_phoenix_markets: a market named by its orderbook address takes - the symbol listed with that address, and its current and backstop factors are - maintenanceRiskFactorBps and backstopRiskFactorBps there. - Set fetchBeforeUse: true so the asset map is fetched from the upstream datasource first; use - false only for a later override that builds on state an earlier one prepared in the same - scenario. + Phoenix keeps every maintenance factor at or below this one, so raise it before setting a + maintenance factor above 7500. Pass the factor as a decimal string. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-market-max-liquidation-size + name: Phoenix Max Liquidation Size + description: >- + Set the most base lots one liquidation may close in a market. + idl_account_name: PerpAssetMap + properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + - path: maxLiquidationSize + type: input + value_type: string + label: Max liquidation size (base lots) + description: >- + Base lots one liquidate_via_market_order may close; mainnet SOL allows 500000 + llm_context: | + A keeper's liquidation asking for more is refused, so a small value forces a large position to + be liquidated in several steps. Pass the size as a decimal string. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-market-open-interest-cap + name: Phoenix Open Interest Cap + description: >- + Set the most open interest a market accepts, in base lots. + idl_account_name: PerpAssetMap + properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + - path: openInterestCap + type: input + value_type: string + label: Open interest cap (base lots) + description: >- + The most open interest the market accepts + llm_context: | + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-market-funding + name: Phoenix Market Funding + description: >- + Set one market's funding interval, funding period and maximum funding rate. + idl_account_name: PerpAssetMap + properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + - path: fundingIntervalSeconds + type: input + value_type: string + label: Funding interval (seconds) + description: >- + Leave empty to keep the current value + - path: fundingPeriodSeconds + type: input + value_type: string + label: Funding period (seconds) + description: >- + Leave empty to keep the current value + - path: maxFundingRate + type: input + value_type: string + label: Max funding rate (quote lots per base lot) + description: >- + Signed; leave empty to keep the current value + llm_context: | + Every field is optional: one left out keeps its current value. Pass values as decimal strings. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. address: type: pubkey value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC @@ -107,105 +345,229 @@ templates: - id: phoenix-market-fees name: Phoenix Market Fees description: >- - Set one market's default taker and maker fees on its Phoenix Eternal orderbook. - idl_account_name: OrderbookHeader + Set one market's default taker and maker fees. + idl_account_name: PerpAssetMap properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol - path: defaultTakerFeeMicro + type: input + value_type: string label: Taker fee (micro) description: >- - Taker fee in millionths of the filled notional. Mainnet markets use 350 (0.035%, 3.5 - bps). - Example: 1000 + Millionths of the filled notional; mainnet markets use 350 (0.035%). Example: 1000 - path: defaultMakerFeeMicro + type: input + value_type: string label: Maker fee (micro) description: >- - Maker fee in millionths of the filled notional; negative is a rebate. Mainnet markets use - 50 (0.5 bps). Example: -50 + Millionths of the filled notional, negative for a rebate; mainnet uses 50. Example: -50 llm_context: | - This changes the fees a market charges on every fill, for example a fee hike or a maker - rebate. It does not change per-trader fee overrides, prices or orders. - The account is the market's orderbook: resolve the market with list_phoenix_markets and put - its orderbook address in account.pubkey. - Pass both fees as JSON numbers in millionths of the notional: 350 is 0.035%, 1000 is 0.1%. - defaultMakerFeeMicro may be negative (a rebate); keep it below the taker fee. - Set fetchBeforeUse: true so the orderbook is fetched from the upstream datasource first. + This changes the fees a market charges on every fill. It does not change per-trader fee + overrides. A fee left out keeps its current value. Keep the maker fee below the taker fee. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. address: type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-market-status + name: Phoenix Market Status + description: >- + Pause a market, make it post-only, or activate it again. + idl_account_name: PerpAssetMap + properties: + - path: symbol + type: dynamic_ref + label: Market Symbol + source: list_phoenix_markets + description: Exact Phoenix market symbol + - path: nextMarketStatus + type: input + value_type: string + label: Market status + description: >- + Active, PostOnly, Paused or Closed + llm_context: | + Paused stops all trading in the market, PostOnly accepts only orders that rest on the book, + and Active trades normally. Closed and Tombstoned are one-way steps toward removing a market. + Leave fetchBeforeUse false: a refetch puts every market's prices and factors back to + mainnet's while their order books keep earlier moves. list_phoenix_markets loads the map. + address: + type: pubkey + value: 2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC + + - id: phoenix-exchange-status + name: Phoenix Exchange Status + description: >- + Switch the exchange-wide active, gated and maintenance flags. + idl_account_name: GlobalConfiguration + properties: + - path: active + type: input + value_type: string + label: Active + description: >- + true or false; leave empty to keep it + - path: gated + type: input + value_type: string + label: Gated + description: >- + true or false; leave empty to keep it + - path: maintenance + type: input + value_type: string + label: Maintenance + description: >- + true or false; leave empty to keep it + llm_context: | + Each flag left out keeps its current value. An inactive exchange refuses trading for everyone. + Use fetchBeforeUse: true the first time a scenario changes this account on a surfnet, false + after that. It stands alone, so a refetch only drops earlier changes to it. + address: + type: pubkey + value: 2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ - id: phoenix-withdraw-limits name: Phoenix Withdraw Limits description: >- - Set the exchange-wide withdrawal budget and the withdrawal and queueing fees. + Set the exchange-wide withdrawal budget and how fast it refills. idl_account_name: WithdrawQueueHeader properties: - - path: withdrawThrottle.maxBudget.inner + - path: maxBudget + type: input + value_type: string label: Max withdraw budget description: >- - Most quote lots withdrawable before requests queue (1000000 = 1 USDC). - Mainnet: 2000000000000. Example: 1000000000 - - path: withdrawThrottle.remainingBudget.inner - label: Remaining withdraw budget - description: >- - Quote lots still withdrawable right now; 0 sends every withdrawal to the queue. - Example: 0 - - path: withdrawThrottle.replenishAmountPerSlot.inner + Quote lots withdrawable before requests queue (1000000 = 1 USDC). Mainnet: 2000000000000 + - path: replenishAmountPerSlot + type: input + value_type: string label: Budget refill per slot description: >- - Quote lots added back to the budget each slot. Mainnet: 450000000 (450 USDC). Example: 0 - - path: withdrawalFee.inner + Quote lots added back each slot. Mainnet: 450000000 (450 USDC) + llm_context: | + This throttles withdrawals for every trader, for example a bank run where withdrawals queue + instead of paying out. A withdrawal is paid at once only when the queue is empty and it fits + the remaining budget; otherwise it waits for the permissionless ConsumeWithdrawQueue. + A field left out keeps its current value. Amounts are quote lots of Phoenix USDC with + 6 decimals, passed as decimal strings. + Use fetchBeforeUse: true the first time a scenario changes this account on a surfnet, false + after that. It stands alone, so a refetch only drops earlier changes to it. + address: + type: pubkey + value: 3c3NTwpg7yW91FxijkHBXwVH1xUifun3Z8TC5eW5Si3K + + - id: phoenix-withdraw-parameters + name: Phoenix Withdraw Parameters + description: >- + Set the deposit cooldown and the withdrawal and queueing fees. + idl_account_name: WithdrawQueueHeader + properties: + - path: depositCooldownPeriodInSlots + type: input + value_type: string + label: Deposit cooldown (slots) + description: >- + Slots after a deposit before the trader may withdraw + - path: withdrawalFee + type: input + value_type: string label: Withdrawal fee description: >- - Quote lots charged per withdrawal. Mainnet: 500000 (0.5 USDC). Example: 2000000 - - path: enqueueingFee.inner + Quote lots charged per withdrawal. Mainnet: 500000 (0.5 USDC) + - path: enqueueingFee + type: input + value_type: string label: Queueing fee description: >- - Quote lots charged when a withdrawal is queued. Mainnet: 500000 (0.5 USDC). - Example: 2000000 + Quote lots charged when a withdrawal is queued. Mainnet: 500000 llm_context: | - This throttles withdrawals for every trader, for example a bank run where withdrawals queue - instead of paying out. It does not move collateral or cancel queued requests. - A withdrawal is paid at once only when the queue is empty and it fits the remaining budget. - Otherwise it is queued, never partially filled, and paid later by the permissionless - ConsumeWithdrawQueue instruction; the budget refills by replenishAmountPerSlot each slot. - SOL withdrawals never queue, so this throttles USDC withdrawals only. - Amounts are quote lots of Phoenix USDC with 6 decimals: 1000000 is 1 USDC. Pass them as JSON - numbers. Keep remainingBudget at or below maxBudget, and set replenishAmountPerSlot to 0 to - keep the budget empty while you test. - The account is the singleton WithdrawQueue, so this template carries its address. - Set fetchBeforeUse: true so the queue is fetched from the upstream datasource first. + A field left out keeps its current value. Pass values as decimal strings. + Use fetchBeforeUse: true the first time a scenario changes this account on a surfnet, false + after that. It stands alone, so a refetch only drops earlier changes to it. address: type: pubkey value: 3c3NTwpg7yW91FxijkHBXwVH1xUifun3Z8TC5eW5Si3K + - id: phoenix-trader-fees + name: Phoenix Trader Fees + description: >- + Set one trader's maker and taker fee override multipliers. + idl_account_name: Trader + properties: + - path: makerFeeOverrideMultiplier + type: input + value_type: string + label: Maker fee multiplier + description: >- + Signed 8-bit multiplier as Phoenix defines it + - path: takerFeeOverrideMultiplier + type: input + value_type: string + label: Taker fee multiplier + description: >- + Signed 8-bit multiplier as Phoenix defines it + llm_context: | + Both multipliers are required. Pass them as decimal strings. + The account is the trader's Trader account. + Leave fetchBeforeUse false: a refetch drops earlier Phoenix changes to this trader. Call + list_phoenix_trader_positions first; it loads the trader into the surfnet. + address: + type: pubkey + - id: phoenix-trader-capabilities name: Phoenix Trader Capabilities description: >- - Set what a Phoenix Trader outside the GlobalTraderIndex may do: activate a fresh trader, make - it reduce-only or freeze it. + Allow or block what one trader may do: orders, risk-increasing trades, deposits, withdrawals. idl_account_name: Trader properties: - - path: traderState.flags - label: Capability bits + - path: PlaceLimitOrder + type: input + value_type: string + label: PlaceLimitOrder + description: >- + true or false; leave empty to keep it + - path: PlaceMarketOrder + type: input + value_type: string + label: PlaceMarketOrder + description: >- + true or false; leave empty to keep it + - path: RiskIncreasingTrade + type: input + value_type: string + label: RiskIncreasingTrade + description: >- + true or false; leave empty to keep it + - path: RiskReducingTrade + type: input + value_type: string + label: RiskReducingTrade + description: >- + true or false; leave empty to keep it + - path: DepositCollateral + type: input + value_type: string + label: DepositCollateral + description: >- + true or false; leave empty to keep it + - path: WithdrawCollateral + type: input + value_type: string + label: WithdrawCollateral description: >- - 1 hot (in the GlobalTraderIndex, cannot be set here), 2 limit, 4 market, 8 risk increase, - 16 deposit, 32 withdraw. 62 grants every capability except hot. Example: 62 + true or false; leave empty to keep it llm_context: | - This changes a trader's capability bits, for example to activate a freshly registered trader - in the local VM so a bot can deposit and trade, or to make a trader reduce-only or frozen. - It does not change collateral or positions. - Bits: 1 hot, 2 place limit, 4 place market, 8 increase risk, 16 deposit, 32 withdraw. - A freshly registered trader is frozen with 6 until Phoenix activates it; 62 activates it - without the hot bit. 54 = 2 + 4 + 16 + 32 is reduce-only (no risk increase) and 6 is frozen. - The hot bit (1) cannot be set: Phoenix would look the trader up in the GlobalTraderIndex, - which does not list it, and reject its transactions. Limit orders need a trader in the - index, so a trader outside it places market orders only. - Only a Trader the local GlobalTraderIndex does not list is supported: a listed Trader keeps - these bits in its index record, so Play skips the override with a warning. - Pass traderState.flags as a JSON number. - The account is the Trader PDA ["trader", authority, [pda index, subaccount index]], usually - [0, 0]: pda index 0, and subaccount 0 is the cross-margin account (1 and up are isolated - accounts). - Set fetchBeforeUse: true so the trader is fetched from the upstream datasource first. + Set a capability to false to block it, true to allow it; one left out keeps its current value. + RiskIncreasingTrade false makes the trader reduce-only; every capability false freezes it. + The account is the trader's Trader account. + Leave fetchBeforeUse false: a refetch drops earlier Phoenix changes to this trader. Call + list_phoenix_trader_positions first; it loads the trader into the surfnet. address: type: pubkey diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/state_builder.rs b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/state_builder.rs index cc7fca682..ae12cf260 100644 --- a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/state_builder.rs +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/state_builder.rs @@ -1,1070 +1,1216 @@ use core::mem::size_of; -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr, sync::OnceLock}; +use anchor_lang_idl::types::{ + IdlDefinedFields, IdlInstruction, IdlInstructionAccountItem, IdlType, IdlTypeDef, IdlTypeDefTy, +}; +use log::warn; use phoenix_rise_accounts::{ - PhoenixAccount, PhoenixAccountDecodeError, - perp_asset_map::{PerpAssetMap, PerpAssetMetadata, PriceComponent}, - trader::TraderHeader, + global_config::GlobalConfig, + perp_asset_map::{FundingAccumulator, PerpAssetMetadata, PriceComponent}, + spline_collection::{ + GTC_LIFESPAN, SPLINE_REGION_CAPACITY, Spline, SplineCollection, TickRegion, + }, }; use solana_account::Account; use solana_clock::Clock; use solana_commitment_config::CommitmentConfig; +use solana_compute_budget_interface::ComputeBudgetInstruction; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; use solana_pubkey::Pubkey; -use surfpool_types::{AccountAddress, OverrideInstance, Scenario}; - -use super::collateral::{ - current_quote_lot_collateral, ensure_collateral_floor, index_trader_state_range, - index_trader_state_ranges, parse_quote_lot_collateral, trader_header, - validate_hot_trader_fields, +use solana_signer::Signer; +use solana_transaction::Transaction; +use surfpool_types::Idl; +use txtx_addon_network_svm::codec::idl::borsh_encode_value_to_idl_type; + +use super::{ + market::{ + Market, MarketContext, invalid_perp_asset_map, map_entries, move_market, phoenix_markets, + uncross_market, + }, + trader::{cancel_order_writes, collateral_writes}, }; use crate::{ error::{SurfpoolError, SurfpoolResult}, + scenarios::registry::PHOENIX_ETERNAL_IDL_CONTENT, surfnet::{ remote::SurfnetRemoteClient, - svm::{AccountUpdatePolicy, SurfnetSvm}, + svm::{AccountUpdatePolicy, SurfnetSvm, json_to_txtx_value_for_idl_type}, }, }; -pub const PHOENIX_ETERNAL_PROGRAM_ID: Pubkey = +pub const PHOENIX_PROGRAM_ID: Pubkey = Pubkey::from_str_const("EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih"); -// Singletons that GlobalConfig points at; the mainnet tests check them against GlobalConfig. +pub const PHOENIX_GLOBAL_CONFIG: Pubkey = + Pubkey::from_str_const("2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ"); +/// GlobalConfig names this map too; the mainnet tests check the two agree. pub const PHOENIX_PERP_ASSET_MAP: Pubkey = Pubkey::from_str_const("2nHGAaEw3D5dd4hVueaUNoygkQFmoeKqRQWnSPqSMFUC"); -pub const PHOENIX_GLOBAL_TRADER_INDEX: Pubkey = - Pubkey::from_str_const("HCrPXLByGqRh2szQi3gj7oRdRVBNi1gccAyn4CQCT3HK"); -fn phoenix_account_kind(data: &[u8]) -> Option { - PhoenixAccount::from_discriminant(data.get(..8)?.try_into().unwrap()) +pub fn log_authority() -> Pubkey { + Pubkey::find_program_address(&[b"log"], &PHOENIX_PROGRAM_ID).0 } -const COLLATERAL_TEMPLATE_ID: &str = "phoenix-trader-collateral-stress"; -const COLLATERAL_FIELD: &str = "traderState.quoteLotCollateral"; -const MARKET_SYMBOL_FIELD: &str = "symbol"; -const DIRECT_MARK_TICKS_FIELD: &str = "target_ticks"; -const MAINTENANCE_FACTOR_FIELD: &str = "maintenance_risk_factor_bps"; -const MAX_RISK_FACTOR_BPS: u16 = 10_000; -const PREPARATION_SLOT: u64 = 0; -/// Phoenix refuses a market once its readings are older than its stale threshold times its `u8` -/// `oracle_hard_stale_multiplier`, or than the threshold alone when that is 0. `u32::MAX` puts the -/// limit beyond any session and keeps within a `u64`. -const RAISED_STALE_THRESHOLD_SLOTS: u64 = u32::MAX as u64; - -/// What a caller can name a market by, and the current values a relative change starts from. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PhoenixMarket { - pub symbol: String, - pub orderbook: Pubkey, - pub mark_ticks: u64, - pub tick_size: u64, - pub base_lot_decimals: i8, - pub maintenance_risk_factor_bps: u16, - pub backstop_risk_factor_bps: u16, +/// The exchange-wide accounts, as GlobalConfig names them. +pub struct Exchange { + pub config: GlobalConfig, + pub perp_asset_map: Pubkey, + pub global_trader_index: Pubkey, + pub active_trader_buffer: Pubkey, } -pub fn phoenix_markets( - perp_asset_map: Pubkey, - account: &Account, -) -> SurfpoolResult> { - if account.owner != PHOENIX_ETERNAL_PROGRAM_ID { - return Err(SurfpoolError::invalid_account_owner( - perp_asset_map, - None::, - )); - } - let invalid = |error: PhoenixAccountDecodeError| { - SurfpoolError::invalid_account_data( - perp_asset_map, - "Expected a valid Phoenix Eternal PerpAssetMap account", - Some(error), +impl Exchange { + /// Reads GlobalConfig from the local VM, after putting it and the program there, and on the + /// first load the index, the buffer and every book ([`Exchange::hydrate_index_with_books`]). + pub async fn load( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + ) -> SurfpoolResult { + hydrate( + svm, + remote_ctx, + &[PHOENIX_PROGRAM_ID, PHOENIX_GLOBAL_CONFIG], ) - }; - let map = PerpAssetMap::try_from_account_bytes(&account.data).map_err(invalid)?; - let mut markets = map - .iter() - .map(|entry| { - entry.map(|entry| PhoenixMarket { - symbol: entry.symbol.as_str().to_string(), - orderbook: Pubkey::new_from_array( - entry.metadata.static_market_params().market_account, - ), - mark_ticks: entry - .metadata - .oracle_price() - .mark_price - .price - .ticks - .as_inner(), - tick_size: entry.metadata.static_market_params().tick_size.as_inner(), - base_lot_decimals: entry.metadata.static_market_params().base_lot_decimals, - maintenance_risk_factor_bps: entry.metadata.risk_params().risk_factors[0], - backstop_risk_factor_bps: entry.metadata.risk_params().risk_factors[1], - }) + .await?; + let exchange = Self::from_config(&local_account(svm, &PHOENIX_GLOBAL_CONFIG)?)?; + exchange.hydrate_index_with_books(svm, remote_ctx).await?; + Ok(exchange) + } + + /// Resting orders and the buffer's positions name traders by their node in the index, so a + /// book fetched after the index can name a node the index has since freed. While the index is + /// missing locally, it comes with the buffer and every book in one read. Past 100 accounts + /// the client splits that read, and the parts can come from different slots. + async fn hydrate_index_with_books( + &self, + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + ) -> SurfpoolResult<()> { + if svm.inner.get_account(&self.global_trader_index)?.is_some() { + return Ok(()); + } + hydrate(svm, remote_ctx, &[self.perp_asset_map]).await?; + let map = local_account(svm, &self.perp_asset_map)?; + let mut group = vec![self.global_trader_index, self.active_trader_buffer]; + for entry in map_entries(&self.perp_asset_map, &map.data)? { + let params = entry.metadata.static_market_params(); + group.push(Pubkey::new_from_array(params.market_account)); + } + hydrate(svm, remote_ctx, &group).await + } + + /// The exchange the GlobalConfig `account` describes. + pub fn from_config(account: &Account) -> SurfpoolResult { + let config = GlobalConfig::try_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data( + PHOENIX_GLOBAL_CONFIG, + "Expected the Phoenix Eternal GlobalConfig", + Some(e), + ) + })?; + Ok(Self { + perp_asset_map: Pubkey::new_from_array(config.perp_asset_map_key()), + global_trader_index: Pubkey::new_from_array(config.global_trader_index_header_key()), + active_trader_buffer: Pubkey::new_from_array(config.active_trader_buffer_header_key()), + config, }) - .collect::, _>>() - .map_err(invalid)?; - markets.sort_unstable_by(|left, right| left.symbol.cmp(&right.symbol)); + } +} - Ok(markets) +pub fn phoenix_idl() -> &'static Idl { + static IDL: OnceLock = OnceLock::new(); + IDL.get_or_init(|| { + serde_json::from_str(PHOENIX_ETERNAL_IDL_CONTENT).expect("the bundled Phoenix IDL parses") + }) } -fn price_patch_error(account_pubkey: &Pubkey, message: impl core::fmt::Display) -> SurfpoolError { - SurfpoolError::invalid_account_data( - account_pubkey, - "Expected a valid Phoenix Eternal PerpAssetMap account", - Some(message), - ) +/// An Eternal instruction encoded from its IDL definition. Accounts are named as in the IDL, and +/// `args` holds one JSON value per IDL argument, shaped like its type. +pub fn phoenix_instruction( + name: &str, + accounts: &[(&str, Pubkey)], + args: &serde_json::Value, +) -> SurfpoolResult { + encode_instruction(name, accounts, args, true) } -fn checked_ticks(ticks: u64) -> SurfpoolResult { - // Margin is the mark times the tick size, so a zero mark would zero the margin of every - // position in the market instead of shocking it. - if ticks == 0 || ticks > u64::from(u32::MAX) { +/// Like [`phoenix_instruction`], picking the accounts the instruction takes out of `known` and +/// ignoring the rest. +pub fn phoenix_instruction_from( + name: &str, + known: &[(&str, Pubkey)], + args: &serde_json::Value, +) -> SurfpoolResult { + encode_instruction(name, known, args, false) +} + +fn instruction_definition(name: &str) -> SurfpoolResult<&'static IdlInstruction> { + phoenix_idl() + .instructions + .iter() + .find(|instruction| instruction.name == name) + .ok_or_else(|| SurfpoolError::internal(format!("the Phoenix IDL has no {name}"))) +} + +fn encode_instruction( + name: &str, + accounts: &[(&str, Pubkey)], + args: &serde_json::Value, + strict: bool, +) -> SurfpoolResult { + let idl = phoenix_idl(); + let program_id = Pubkey::from_str(&idl.address) + .map_err(|e| SurfpoolError::internal(format!("invalid Phoenix IDL address: {e}")))?; + let definition = instruction_definition(name)?; + + let mut data = definition.discriminator.clone(); + for arg in &definition.args { + let value = args.get(&arg.name).ok_or_else(|| { + SurfpoolError::internal(format!("{name} needs the argument {}", arg.name)) + })?; + let value = json_to_txtx_value_for_idl_type(value, &arg.ty, &idl.types)?; + let encoded = borsh_encode_value_to_idl_type(&value, &arg.ty, &idl.types, None) + .map_err(|e| SurfpoolError::internal(format!("{name} {}: {e}", arg.name)))?; + data.extend(encoded); + } + + let mut metas = Vec::with_capacity(definition.accounts.len()); + for item in &definition.accounts { + let IdlInstructionAccountItem::Single(account) = item else { + return Err(SurfpoolError::internal(format!( + "{name} groups its accounts, which the Phoenix IDL does not do" + ))); + }; + let Some((_, pubkey)) = accounts.iter().find(|(given, _)| *given == account.name) else { + return Err(SurfpoolError::internal(format!( + "{name} needs the account {}", + account.name + ))); + }; + metas.push(AccountMeta { + pubkey: *pubkey, + is_signer: account.signer, + is_writable: account.writable, + }); + } + if let Some((unknown, _)) = accounts.iter().filter(|_| strict).find(|(given, _)| { + !definition + .accounts + .iter() + .any(|item| matches!(item, IdlInstructionAccountItem::Single(a) if a.name == *given)) + }) { return Err(SurfpoolError::internal(format!( - "price ticks {ticks} are outside the Phoenix mark range 1..={}", - u32::MAX + "{name} takes no account named {unknown}" ))); } - Ok(ticks) -} -fn patch_direct_mark( - account_pubkey: &Pubkey, - data: &[u8], - symbol: &str, - target_ticks: u64, - mark_slot: u64, -) -> SurfpoolResult> { - let target_ticks = checked_ticks(target_ticks)?; - patch_market_metadata(account_pubkey, data, symbol, |_, bytes| { - let price_len = size_of::(); - let mut price = bytemuck::pod_read_unaligned::(&bytes[..price_len]); - price.mark_price.price.slot = mark_slot; - price.mark_price.price.ticks = bytemuck::cast(target_ticks); - // Phoenix rebuilds the mark from its oracle inputs on every trade, so they carry the - // shock too; the book input is clamped around them and needs no write. - let mark = &mut price.mark_price; - for sample in mark - .spot_price_component - .last_exchange_spot_price - .iter_mut() - .chain( - mark.perp_price_component - .last_exchange_perp_price - .iter_mut(), - ) - { - sample.ticks = mark.price.ticks; - sample.slot = mark_slot; - } - mark.spot_price_component.slot = mark_slot; - bytes[..price_len].copy_from_slice(bytemuck::bytes_of(&price)); - Ok(()) + Ok(Instruction { + program_id, + accounts: metas, + data, }) } -fn patch_maintenance_factor( - account_pubkey: &Pubkey, - data: &[u8], - symbol: &str, - factor: u16, -) -> SurfpoolResult> { - patch_market_metadata(account_pubkey, data, symbol, |metadata, bytes| { - // risk_factors is [maintenance, backstop, high_risk], and the risk tier is checked from - // the backstop up, so a maintenance factor at or below the backstop one leaves no - // Liquidatable tier. - let backstop = metadata.risk_params().risk_factors[1]; - if factor <= backstop || factor > MAX_RISK_FACTOR_BPS { +/// A market symbol as Phoenix stores it: its bytes, zero-padded to 16. +pub fn symbol_bytes(symbol: &str) -> [u8; 16] { + let mut bytes = [0_u8; 16]; + let len = symbol.len().min(bytes.len()); + bytes[..len].copy_from_slice(&symbol.as_bytes()[..len]); + bytes +} + +/// The type `ty` names, when it names one in `types`. +fn defined<'t>(ty: &IdlType, types: &'t [IdlTypeDef]) -> Option<&'t IdlTypeDefTy> { + let IdlType::Defined { name, .. } = ty else { + return None; + }; + types.iter().find(|t| &t.name == name).map(|t| &t.ty) +} + +/// The arguments of `name` built from a template's flat values: every IDL field takes the value +/// named after it, a market symbol fills `perpAssetSymbol`, and an optional field left out is kept. +pub fn instruction_args( + name: &str, + values: &HashMap, + symbol: Option<&str>, +) -> SurfpoolResult { + let idl = phoenix_idl(); + let mut args = serde_json::Map::new(); + for arg in &instruction_definition(name)?.args { + let Some(IdlTypeDefTy::Struct { + fields: Some(IdlDefinedFields::Named(fields)), + }) = defined(&arg.ty, &idl.types) + else { return Err(SurfpoolError::internal(format!( - "{MAINTENANCE_FACTOR_FIELD} {factor} must be above {symbol}'s backstop factor \ - {backstop} and at most {MAX_RISK_FACTOR_BPS}" + "{name} {} is not a struct of named fields", + arg.name ))); + }; + let mut object = serde_json::Map::new(); + for field in fields { + let value = if field.name == "perpAssetSymbol" { + let symbol = symbol.ok_or_else(|| { + SurfpoolError::internal(format!("{name} needs a market symbol")) + })?; + serde_json::json!({ "symbolBytes": symbol_bytes(symbol) }) + } else if let Some(value) = given(values, &field.name) { + shape(value, &field.ty, &idl.types)? + } else if matches!(field.ty, IdlType::Option(_)) { + serde_json::Value::Null + } else { + return Err(SurfpoolError::internal(format!( + "{name} needs {}", + field.name + ))); + }; + object.insert(field.name.clone(), value); } - // The metadata layout type is private to the crate, so the field offset comes from the view. - let offset = metadata.risk_params().risk_factors.as_ptr() as usize - - metadata.as_bytes().as_ptr() as usize; - bytes[offset..offset + 2].copy_from_slice(&factor.to_le_bytes()); - Ok(()) + args.insert(arg.name.clone(), object.into()); + } + Ok(args.into()) +} + +/// A template value in the JSON shape the IDL encoder expects for `ty`: decimal strings become +/// numbers, a bare value fills a one-field wrapper such as `BaseLots`, and a name picks a variant. +fn shape( + value: &serde_json::Value, + ty: &IdlType, + types: &[IdlTypeDef], +) -> SurfpoolResult { + use serde_json::Value; + let invalid = || SurfpoolError::internal(format!("{value} does not fit {ty:?}")); + Ok(match (ty, value) { + (IdlType::Option(_), Value::Null) => Value::Null, + (IdlType::Option(inner), _) => shape(value, inner, types)?, + (IdlType::Bool, Value::String(text)) => Value::Bool(text.parse().map_err(|_| invalid())?), + (IdlType::U8 | IdlType::U16 | IdlType::U32 | IdlType::U64, Value::String(text)) => { + Value::from(text.trim().parse::().map_err(|_| invalid())?) + } + (IdlType::I8 | IdlType::I16 | IdlType::I32 | IdlType::I64, Value::String(text)) => { + Value::from(text.trim().parse::().map_err(|_| invalid())?) + } + (IdlType::Defined { .. }, _) => match (defined(ty, types), value) { + ( + Some(IdlTypeDefTy::Struct { + fields: Some(IdlDefinedFields::Named(fields)), + }), + _, + ) => match fields.as_slice() { + [field] => serde_json::json!({ &field.name: shape(value, &field.ty, types)? }), + _ => return Err(invalid()), + }, + (Some(IdlTypeDefTy::Enum { .. }), Value::String(variant)) => { + serde_json::json!({ variant: null }) + } + _ => value.clone(), + }, + _ => value.clone(), }) } -/// The map with every market's spot and perp oracle stale threshold raised to -/// [`RAISED_STALE_THRESHOLD_SLOTS`], or `None` when they already are. A higher threshold is kept. -fn raise_stale_thresholds(account_pubkey: &Pubkey, data: &[u8]) -> SurfpoolResult>> { - let decode_error = |error: PhoenixAccountDecodeError| { - price_patch_error( - account_pubkey, - format!("invalid Phoenix PerpAssetMap account: {error}"), - ) - }; - let map = PerpAssetMap::try_from_account_bytes(data).map_err(decode_error)?; - let price_len = size_of::(); - let mut patched: Option> = None; - // The decoder walks the entries in storage order, and each market's metadata holds its own - // market account, so it occurs once in the map - let mut cursor = 0; - for entry in map.iter() { - let metadata = entry.map_err(decode_error)?.metadata; - let metadata_bytes = metadata.as_bytes(); - let offset = data[cursor..] - .windows(metadata_bytes.len()) - .position(|window| window == metadata_bytes) - .map(|position| cursor + position) - .ok_or_else(|| { - price_patch_error(account_pubkey, "Phoenix market metadata was not found") - })?; - cursor = offset + metadata_bytes.len(); +const FEE_PAYER_LAMPORTS: u64 = 1_000_000_000; +const COMPUTE_UNIT_LIMIT: u32 = 1_400_000; - // The PriceComponent is the metadata's first field. - let mut price = *metadata.oracle_price(); - let mark = &mut price.mark_price; - let mut raised = false; - for threshold in [ - &mut mark.spot_price_component.stale_threshold, - &mut mark.perp_price_component.stale_threshold, - ] { - if *threshold < RAISED_STALE_THRESHOLD_SLOTS { - *threshold = RAISED_STALE_THRESHOLD_SLOTS; - raised = true; - } +/// Puts every listed account into the local VM, reading only the missing ones from the +/// upstream datasource, the way any account read does. +pub async fn hydrate( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + addresses: &[Pubkey], +) -> SurfpoolResult<()> { + let mut missing = Vec::new(); + for address in addresses { + if svm.inner.get_account(address)?.is_none() && !missing.contains(address) { + missing.push(*address); } - if raised { - patched.get_or_insert_with(|| data.to_vec())[offset..offset + price_len] - .copy_from_slice(bytemuck::bytes_of(&price)); + } + if missing.is_empty() { + return Ok(()); + } + let phoenix_offline = svm + .offline_accounts + .get(&PHOENIX_PROGRAM_ID.to_string())? + .is_some_and(|config| config.include_owned_accounts); + for address in &missing { + if phoenix_offline || svm.offline_accounts.contains_key(&address.to_string())? { + return Err(SurfpoolError::internal(format!( + "{address} is offline and missing locally" + ))); } } - Ok(patched) -} - -fn patch_market_metadata( - account_pubkey: &Pubkey, - data: &[u8], - symbol: &str, - update: impl FnOnce(&PerpAssetMetadata, &mut [u8]) -> SurfpoolResult<()>, -) -> SurfpoolResult> { - let decode_error = |error: PhoenixAccountDecodeError| { - price_patch_error( - account_pubkey, - format!("invalid Phoenix PerpAssetMap account: {error}"), - ) - }; - let map = PerpAssetMap::try_from_account_bytes(data).map_err(decode_error)?; - let entry = map - .find_by_symbol(symbol) - .map_err(decode_error)? - .ok_or_else(|| SurfpoolError::internal(format!("Phoenix market {symbol} was not found")))?; - let metadata_bytes = entry.metadata.as_bytes(); - let metadata_offset = unique_subslice_offset(data, metadata_bytes).ok_or_else(|| { - price_patch_error( - account_pubkey, - "selected Phoenix market metadata does not occur exactly once", - ) + let (client, commitment) = remote_ctx.as_ref().ok_or_else(|| { + SurfpoolError::internal(format!( + "{} accounts are missing locally and there is no datasource", + missing.len() + )) })?; - let mut patched = data.to_vec(); - update( - &entry.metadata, - &mut patched[metadata_offset..metadata_offset + metadata_bytes.len()], - )?; - Ok(patched) + for fetched in client.get_multiple_accounts(&missing, *commitment).await? { + svm.apply_account_update(fetched, AccountUpdatePolicy::HydrateIfAbsent)?; + } + Ok(()) } -fn unique_subslice_offset(data: &[u8], needle: &[u8]) -> Option { - let mut matches = data - .windows(needle.len()) - .enumerate() - .filter(|(_, window)| *window == needle) - .map(|(offset, _)| offset); - let offset = matches.next()?; - matches.next().is_none().then_some(offset) +pub(crate) fn local_account(svm: &SurfnetSvm, address: &Pubkey) -> SurfpoolResult { + svm.inner + .get_account(address)? + .ok_or_else(|| SurfpoolError::internal(format!("{address} is missing locally"))) } -fn forge_phoenix_override( - account_pubkey: &Pubkey, - account: &Account, - account_values: &HashMap, - mark_slot: u64, -) -> SurfpoolResult> { - // Only the codec's inputs are read: other keys, such as PerpAssetMap fields a client copied - // from the decoded account, cannot be written through this codec. - let symbol = || { - account_values - .get(MARKET_SYMBOL_FIELD) - .and_then(serde_json::Value::as_str) - .filter(|value| !value.is_empty()) - .ok_or_else(|| SurfpoolError::internal("symbol must be a non-empty string")) - }; - match ( - account_values.get(DIRECT_MARK_TICKS_FIELD), - account_values.get(MAINTENANCE_FACTOR_FIELD), - ) { - (Some(ticks), None) => patch_direct_mark( - account_pubkey, - &account.data, - symbol()?, - parse_decimal(ticks, DIRECT_MARK_TICKS_FIELD, "an unsigned 64-bit integer")?, - mark_slot, - ), - (None, Some(factor)) => patch_maintenance_factor( - account_pubkey, - &account.data, - symbol()?, - parse_decimal(factor, MAINTENANCE_FACTOR_FIELD, "basis points")?, - ), - _ => Err(SurfpoolError::internal( - "Phoenix map overrides take symbol plus exactly one of target_ticks or \ - maintenance_risk_factor_bps", - )), - } +/// A copy of the local VM without signature checks, so instructions run as the signers they name. +/// Nothing reaches the local VM until the caller writes back what [`Sandbox::writes`] returns. +pub struct Sandbox<'a> { + live: &'a SurfnetSvm, + svm: SurfnetSvm, + written: Vec, } -/// The writes a Phoenix override needs, or `None` when the account takes the generic IDL path. -/// Every Phoenix override also leaves the local PerpAssetMap's markets usable for the rest of the -/// session; see `keep_oracle_readings_usable`. `target_slot` is the slot being materialized, which -/// the override's writes are published at. -pub async fn prepare_phoenix_override( - svm: &mut SurfnetSvm, - account_pubkey: &Pubkey, - account: &Account, - values: &HashMap, - remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, - fetch_before_use: bool, - target_slot: u64, -) -> SurfpoolResult>> { - if account.owner != PHOENIX_ETERNAL_PROGRAM_ID { - return Ok(None); +impl<'a> Sandbox<'a> { + pub fn new(live: &'a SurfnetSvm) -> Self { + let mut svm = live.clone_for_profiling(); + svm.inner.set_sigverify(false); + Self { + live, + svm, + written: Vec::new(), + } } - match phoenix_account_kind(&account.data) { - Some(PhoenixAccount::PerpAssetMap) => { - let mark_slot = svm.inner.get_sysvar::().slot; - let data = forge_phoenix_override(account_pubkey, account, values, mark_slot)?; - // This override writes the map Phoenix reads, so the thresholds go in the same write. - let data = match raise_stale_thresholds(account_pubkey, &data) { - Ok(raised) => raised.unwrap_or(data), - Err(e) => { - warn!( - "Could not raise the Phoenix PerpAssetMap's oracle stale thresholds: {e}" - ); - data + + /// Runs the instructions in order; the first failing one fails the run. + pub fn run(&mut self, instructions: &[Instruction]) -> SurfpoolResult<()> { + for (index, instruction) in instructions.iter().enumerate() { + self.send(instruction) + .map_err(|e| SurfpoolError::internal(format!("instruction {index} failed: {e}")))?; + for meta in &instruction.accounts { + if meta.is_writable && !self.written.contains(&meta.pubkey) { + self.written.push(meta.pubkey); } - }; - Ok(Some(vec![( - *account_pubkey, - Account { - data, - ..account.clone() - }, - )])) - } - kind => { - keep_oracle_readings_usable(svm, remote_ctx, target_slot).await; - match kind { - Some(PhoenixAccount::Trader) => prepare_trader_override( - svm, - account_pubkey, - account, - values, - remote_ctx, - fetch_before_use, - ) - .await - .map(Some), - _ => Ok(None), } } + Ok(()) } -} -/// Oracle updates keep every market's readings fresh, and Phoenix refuses a market whose readings -/// are too old (see [`RAISED_STALE_THRESHOLD_SLOTS`]). Nothing refreshes them locally, so -/// this raises the thresholds of the local PerpAssetMap, fetching it first when it is not local -/// yet. Prices and reading slots stay as they were, and the markets stay usable however far the -/// local Clock moves. A failure leaves the map as it was and never fails the override. It runs on -/// every Phoenix override, before that override's own checks, since the map is needed whether or -/// not the override applies; each run reads the whole map. -async fn keep_oracle_readings_usable( - svm: &mut SurfnetSvm, - remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, - target_slot: u64, -) { - let kept = async { - let map = phoenix_dependency(svm, &PHOENIX_PERP_ASSET_MAP, remote_ctx).await?; - let Some(data) = raise_stale_thresholds(&PHOENIX_PERP_ASSET_MAP, &map.data)? else { - return Ok(()); - }; - svm.set_scenario_override_account( - &PHOENIX_PERP_ASSET_MAP, - Account { data, ..map }, - target_slot, - ) + /// The return data of a read-only instruction, such as a Hawkeye view. + pub fn view(&mut self, instruction: &Instruction) -> SurfpoolResult> { + self.send(instruction).map_err(SurfpoolError::internal) } - .await; - if let Err(e) = kept { - warn!("Could not raise the Phoenix PerpAssetMap's oracle stale thresholds: {e}"); - } -} -async fn prepare_trader_override( - svm: &mut SurfnetSvm, - trader: &Pubkey, - account: &Account, - values: &HashMap, - remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, - fetch_before_use: bool, -) -> SurfpoolResult> { - let header = trader_header(trader, account)?; - let hot = header.trader_state.is_hot(); - // Ahead of every check, so a refused override still leaves the fresh record behind. - if fetch_before_use { - refresh_index_record(svm, &header, remote_ctx).await?; + /// An account as the instructions run so far left it. + pub fn account(&self, address: &Pubkey) -> SurfpoolResult { + local_account(&self.svm, address) } - let listed = !hot - && svm - .inner - .get_account(&PHOENIX_GLOBAL_TRADER_INDEX)? - .is_some_and(|index| index_trader_state_range(&index, &header.key).is_ok()); - if hot || listed { - validate_hot_trader_fields(values)?; + + /// Moves the copy's Clock one slot on. Phoenix fixes the mark it uses for risk for the rest of + /// a slot once a risk action, such as a cancel, has read it. + pub fn next_slot(&mut self) { + let mut clock = self.svm.inner.get_sysvar::(); + clock.slot += 1; + self.svm.inner.set_sysvar(&clock); } - let target = values - .get(COLLATERAL_FIELD) - .map(parse_quote_lot_collateral) - .transpose()?; - // A hot Trader's collateral is read from its GlobalTraderIndex record, so the record is what - // the target is checked against and patched in. - let index = match target { - Some(_) if hot => { - Some(phoenix_dependency(svm, &PHOENIX_GLOBAL_TRADER_INDEX, remote_ctx).await?) + + /// Every account the instructions run so far changed, as it now is. + pub fn writes(self) -> SurfpoolResult> { + let mut changed = Vec::new(); + for pubkey in self.written { + let Some(after) = self.svm.inner.get_account(&pubkey)? else { + continue; + }; + if self.live.inner.get_account(&pubkey)?.as_ref() != Some(&after) { + changed.push((pubkey, after)); + } } - // The local GlobalTraderIndex can predate the Trader leaving the hot set, and Phoenix - // keeps reading a Trader the index lists from its record. - Some(_) => svm + Ok(changed) + } + + fn send(&mut self, instruction: &Instruction) -> Result, String> { + // A fresh payer gives every transaction its own signature. + let payer = Keypair::new(); + self.svm .inner - .get_account(&PHOENIX_GLOBAL_TRADER_INDEX)? - .filter(|index| index_trader_state_range(index, &header.key).is_ok()), - _ => None, - }; - let mut values = values.clone(); - if let Some(target) = target { - ensure_collateral_is_lowered( - current_quote_lot_collateral(&header, index.as_ref())?, - target, - )?; - values.insert( - COLLATERAL_FIELD.to_string(), - serde_json::Value::from(target), + .airdrop(&payer.pubkey(), FEE_PAYER_LAMPORTS) + .map_err(|failed| format!("funding the fee payer failed: {:?}", failed.err))?; + let mut transaction = Transaction::new_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(COMPUTE_UNIT_LIMIT), + instruction.clone(), + ], + Some(&payer.pubkey()), ); + transaction.partial_sign(&[&payer], self.svm.inner.svm.latest_blockhash()); + self.svm + .inner + .send_transaction(transaction) + .map(|meta| meta.return_data.data) + .map_err(|failed| format!("{:?}; logs: {:?}", failed.err, failed.meta.logs)) } - let idl_versions = svm - .registered_idls - .get(&PHOENIX_ETERNAL_PROGRAM_ID.to_string())? - .unwrap_or_default(); - let idl = &idl_versions - .first() - .ok_or_else(|| SurfpoolError::internal("No IDL registered for Phoenix Eternal"))? - .1; - let data = svm.get_forged_account_data(trader, &account.data, idl, &values)?; - // A listed Trader is capped by its index record above, not by its account copy. - if !hot && !listed { - let forged = TraderHeader::try_read_from_account_bytes(&data).map_err(|error| { - SurfpoolError::invalid_account_data( - trader, - "Expected a valid Phoenix Eternal Trader account", - Some(error), - ) - })?; - // Phoenix looks a hot-flagged Trader up in the GlobalTraderIndex, which does not list - // this one, so every later transaction for it would fail. - if forged.trader_state.is_hot() { - return Err(SurfpoolError::internal( - "Phoenix traderState.flags must keep the HOT bit clear on a Trader the \ - GlobalTraderIndex does not list", - )); - } - let collateral = forged.trader_state.quote_lot_collateral.as_inner(); - ensure_collateral_floor(collateral)?; - ensure_collateral_is_lowered( - header.trader_state.quote_lot_collateral.as_inner(), - collateral, - )?; - } +} - let mut writes = Vec::new(); - if let (Some(mut index), Some(collateral)) = (index, values.get(COLLATERAL_FIELD)) { - let range = index_trader_state_range(&index, &header.key)?; - let encoded = SurfnetSvm::get_forged_idl_type_data( - &index.data[range.clone()], - idl, - "TraderState", - &HashMap::from([("quoteLotCollateral".to_string(), collateral.clone())]), - )?; - if encoded.len() != range.len() { +/// Writes every account of one Phoenix override, or puts back the ones already written when a +/// write fails: Phoenix needs its accounts to agree with each other. Returns the addresses written. +pub(crate) fn write_all_or_none( + svm: &mut SurfnetSvm, + writes: Vec<(Pubkey, Account)>, + slot: u64, +) -> SurfpoolResult> { + // Read before any write: a write can change the local VM and then fail on its indexes. + let befores = writes + .iter() + .map(|(pubkey, _)| svm.inner.get_account(pubkey)) + .collect::>>()?; + let mut written: Vec<(Pubkey, Option)> = Vec::with_capacity(writes.len()); + for ((pubkey, account), before) in writes.into_iter().zip(befores) { + written.push((pubkey, before)); + if let Err(error) = svm.set_scenario_override_account(&pubkey, account, slot) { + let mut unrestored = Vec::new(); + for (pubkey, before) in written.into_iter().rev() { + if matches!(svm.inner.get_account(&pubkey), Ok(now) if now == before) { + continue; + } + let created = before.is_none(); + // An empty account deletes one the write created. + if let Err(e) = + svm.set_scenario_override_account(&pubkey, before.unwrap_or_default(), slot) + { + unrestored.push(format!("{pubkey}: {e}")); + } + // A delete also marks the account offline, but before the write it could still be + // fetched upstream. + if created && let Err(e) = svm.offline_accounts.take(&pubkey.to_string()) { + unrestored.push(format!("{pubkey} stays offline: {e}")); + } + } + if unrestored.is_empty() { + return Err(error); + } return Err(SurfpoolError::internal(format!( - "the re-encoded TraderState is {} bytes, the GlobalTraderIndex record holds {}", - encoded.len(), - range.len() + "{error}; putting the accounts back also failed for {}", + unrestored.join(", ") ))); } - index.data[range].copy_from_slice(&encoded); - writes.push((PHOENIX_GLOBAL_TRADER_INDEX, index)); } - writes.push(( - *trader, - Account { - data, - ..account.clone() - }, - )); - Ok(writes) + Ok(written.into_iter().map(|(pubkey, _)| pubkey).collect()) +} + +/// Runs the instructions in a [`Sandbox`] and returns what they changed. +pub fn run_instructions( + svm: &SurfnetSvm, + instructions: &[Instruction], +) -> SurfpoolResult> { + let mut sandbox = Sandbox::new(svm); + sandbox.run(instructions)?; + sandbox.writes() } -/// Phoenix reads a Trader's collateral from its GlobalTraderIndex record whenever the local index -/// lists it, so fetchBeforeUse refreshes the collateral there too: from the upstream record, or from -/// the refetched Trader account when a cold Trader has left the upstream index. The rest of the -/// record stays as the local VM has it, and so does the whole record if the fetch fails. The -/// refreshed record is stored at once, as core stores the refetched Trader, so later overrides in -/// the same slot are checked against it. -async fn refresh_index_record( +/// Phoenix refuses a market once its readings are older than its stale threshold times its `u8` +/// `oracle_hard_stale_multiplier`, or than the threshold alone when that is 0. `u32::MAX` puts the +/// limit beyond any session and keeps within a `u64`. +const RAISED_STALE_THRESHOLD_SLOTS: u64 = u32::MAX as u64; + +const TRADER_CAPABILITIES_TEMPLATE_ID: &str = "phoenix-trader-capabilities"; +const MARKET_SYMBOL_FIELD: &str = "symbol"; +const TRADER_FIELD: &str = "trader"; +const TARGET_TICKS_FIELD: &str = "target_ticks"; +const CHANGE_PERCENT_FIELD: &str = "change_percent"; +const EFFECTIVE_COLLATERAL_FIELD: &str = "effective_collateral_usdc"; + +/// The GlobalConfig role that signs a configuration instruction on mainnet. +#[derive(Clone, Copy, Debug)] +enum Role { + Root, + Risk, + Market, +} + +/// Templates that run one of Phoenix's own configuration instructions, as the role that signs it +/// on mainnet. Each template's fields are named after the instruction's IDL fields. +const CONFIG_TEMPLATES: [(&str, &str, Role); 12] = [ + ( + "phoenix-market-risk-factors", + "UpdatePerpRiskFactors", + Role::Risk, + ), + ( + "phoenix-market-cancel-risk-factor", + "UpdatePerpCancelRiskFactor", + Role::Risk, + ), + ( + "phoenix-market-max-liquidation-size", + "UpdatePerpMaxLiquidationSize", + Role::Risk, + ), + ( + "phoenix-market-open-interest-cap", + "UpdatePerpOpenInterestCap", + Role::Risk, + ), + ( + "phoenix-market-funding", + "UpdateFundingParameters", + Role::Market, + ), + ("phoenix-market-fees", "UpdateMarketFees", Role::Market), + ("phoenix-market-status", "ChangeMarketStatus", Role::Market), + ( + "phoenix-exchange-status", + "ChangeExchangeStatus", + Role::Root, + ), + ( + "phoenix-withdraw-limits", + "UpdateWithdrawRateLimits", + Role::Root, + ), + ( + "phoenix-withdraw-parameters", + "UpdateWithdrawParameters", + Role::Risk, + ), + ("phoenix-trader-fees", "UpdateTraderFees", Role::Market), + ( + TRADER_CAPABILITIES_TEMPLATE_ID, + "SetTraderCapability", + Role::Risk, + ), +]; + +/// The writes of a configuration template's instruction, signed by its GlobalConfig role, which +/// passes its own key as the permission account, as mainnet's configuration transactions do. +async fn run_config_template( svm: &mut SurfnetSvm, - header: &TraderHeader, remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, -) -> SurfpoolResult<()> { - let Some((client, commitment)) = remote_ctx else { - return Ok(()); - }; - // An index not held locally yet is fetched whole, and so fresh, when the override needs it. - let Some(mut index) = svm.inner.get_account(&PHOENIX_GLOBAL_TRADER_INDEX)? else { - return Ok(()); - }; - let Ok(local) = index_trader_state_range(&index, &header.key) else { - return Ok(()); - }; - let Ok(Ok(remote)) = client - .get_account(&PHOENIX_GLOBAL_TRADER_INDEX, *commitment) - .await - .map(|fetched| fetched.map_account()) - else { - return Ok(()); - }; - let Ok(upstream_records) = index_trader_state_ranges(&remote) else { - return Ok(()); - }; - let trader = Pubkey::new_from_array(header.key); - let account_collateral = header - .trader_state - .quote_lot_collateral - .as_inner() - .to_le_bytes(); - let fresh = match upstream_records.into_iter().find(|(key, _)| *key == trader) { - Some((_, upstream)) if upstream.len() == local.len() => { - &remote.data[upstream.start..upstream.start + 8] + template_id: &str, + instruction: &str, + role: Role, + target: &Pubkey, + values: &HashMap, +) -> SurfpoolResult> { + let exchange = Exchange::load(svm, remote_ctx).await?; + let authority = Pubkey::new_from_array(match role { + Role::Root => exchange.config.root_authority(), + Role::Risk => exchange.config.risk_authority(), + Role::Market => exchange.config.market_authority(), + }); + let withdraw_queue = Pubkey::new_from_array(exchange.config.withdraw_queue_key()); + hydrate( + svm, + remote_ctx, + &[ + exchange.perp_asset_map, + exchange.global_trader_index, + exchange.active_trader_buffer, + withdraw_queue, + *target, + ], + ) + .await?; + let mut known = vec![ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("authority", authority), + ("maybePermissionAccount", authority), + ("perpAssetMap", exchange.perp_asset_map), + ("globalTraderIndex", exchange.global_trader_index), + ("activeTraderBuffer", exchange.active_trader_buffer), + ("withdrawQueueAccount", withdraw_queue), + ("traderAccount", *target), + ]; + let symbol = values + .get(MARKET_SYMBOL_FIELD) + .and_then(serde_json::Value::as_str); + let mut values = values.clone(); + if let Some(symbol) = symbol { + let map = local_account(svm, &exchange.perp_asset_map)?; + let market = Market::find(&exchange.perp_asset_map, &map.data, symbol)?; + hydrate(svm, remote_ctx, &[market.orderbook]).await?; + known.push(("orderbook", market.orderbook)); + known.push(("orderbookAccount", market.orderbook)); + // UpdatePerpRiskFactors takes all three factors; one left out keeps its current value. + if instruction == "UpdatePerpRiskFactors" { + let fields = [ + "maintenanceRiskFactor", + "backstopRiskFactor", + "highRiskRiskFactor", + ]; + for (field, current) in fields.into_iter().zip(market.risk_factors) { + if given(&values, field).is_none() { + values.insert(field.to_string(), current.to_string().into()); + } + } } - // A cold Trader that left the upstream index keeps its collateral in its own account, which - // core just refetched. A hot Trader's account copy is stale, so its record stays. - None if !header.trader_state.is_hot() => &account_collateral[..], - _ => return Ok(()), + } + let args = if template_id == TRADER_CAPABILITIES_TEMPLATE_ID { + capability_toggles(&values)? + } else { + instruction_args(instruction, &values, symbol)? }; - index.data[local.start..local.start + 8].copy_from_slice(fresh); - svm.set_account(&PHOENIX_GLOBAL_TRADER_INDEX, index)?; - Ok(()) + run_instructions( + svm, + &[phoenix_instruction_from(instruction, &known, &args)?], + ) } -async fn phoenix_dependency( - svm: &mut SurfnetSvm, - address: &Pubkey, - remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, -) -> SurfpoolResult { - if let Some(account) = svm.inner.get_account(address)? { - return Ok(account); +/// A template input, unless it was left out, left empty or null. +fn given<'v>( + values: &'v HashMap, + field: &str, +) -> Option<&'v serde_json::Value> { + values + .get(field) + .filter(|value| !value.is_null() && value.as_str() != Some("")) +} + +/// `SetTraderCapability` takes a list of toggles; its template takes one boolean per Phoenix +/// capability, named after it, and toggles only those given. +fn capability_toggles( + values: &HashMap, +) -> SurfpoolResult { + const TARGETS: [&str; 6] = [ + "PlaceLimitOrder", + "PlaceMarketOrder", + "RiskIncreasingTrade", + "RiskReducingTrade", + "DepositCollateral", + "WithdrawCollateral", + ]; + let mut toggles = Vec::new(); + for target in TARGETS { + let enable = match given(values, target) { + None => continue, + Some(serde_json::Value::Bool(enable)) => *enable, + Some(serde_json::Value::String(text)) => text + .parse() + .map_err(|_| SurfpoolError::internal(format!("{target} must be true or false")))?, + Some(other) => { + return Err(SurfpoolError::internal(format!( + "{target} must be true or false, not {other}" + ))); + } + }; + toggles.push(serde_json::json!({ "target": { target: null }, "enable": enable })); } - if svm.offline_accounts.contains_key(&address.to_string())? - || svm - .offline_accounts - .get(&PHOENIX_ETERNAL_PROGRAM_ID.to_string())? - .is_some_and(|config| config.include_owned_accounts) - { + if toggles.is_empty() { return Err(SurfpoolError::internal(format!( - "Phoenix dependency {address} is offline and missing locally" + "set at least one of {}", + TARGETS.join(", ") ))); } - let (client, commitment) = remote_ctx.as_ref().ok_or_else(|| { - SurfpoolError::internal(format!("Phoenix dependency {address} is missing locally")) - })?; - let fetched = client.get_account(address, *commitment).await?; - let account = fetched.clone().map_account()?; - // Fetch the dependency once instead of per override, the way any read from the upstream - // datasource does: the account is also indexed by owner, so getProgramAccounts serves the - // local copy the override then patches. - svm.apply_account_update(fetched, AccountUpdatePolicy::HydrateIfAbsent)?; - Ok(account) + Ok(serde_json::json!({ "params": { "toggles": toggles } })) } -/// Raising collateral needs a real deposit into the global vault. -fn ensure_collateral_is_lowered( - current_quote_lots: i64, - target_quote_lots: i64, -) -> SurfpoolResult<()> { - if target_quote_lots > current_quote_lots { - return Err(SurfpoolError::internal(format!( - "Phoenix collateral stress can only lower collateral: {current_quote_lots} quote lots \ - are backed by the global vault, {target_quote_lots} would not be. Deposit first to \ - raise it." - ))); +/// The writes a Phoenix override needs, or `None` when the account takes the generic IDL path. +/// Each first keeps the markets usable ([`keep_markets_usable`]); `target_slot` is the slot played. +pub async fn prepare_phoenix_override( + svm: &mut SurfnetSvm, + template_id: &str, + account_pubkey: &Pubkey, + account: &Account, + values: &HashMap, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + target_slot: u64, +) -> SurfpoolResult>> { + if !template_id.starts_with("phoenix-") && account.owner != PHOENIX_PROGRAM_ID { + return Ok(None); + } + keep_markets_usable(svm, remote_ctx, target_slot).await; + if let Some((_, instruction, role)) = CONFIG_TEMPLATES + .iter() + .find(|(config_template, ..)| *config_template == template_id) + { + return run_config_template( + svm, + remote_ctx, + template_id, + instruction, + *role, + account_pubkey, + values, + ) + .await + .map(Some); + } + match template_id { + "phoenix-market-move" => { + let symbol = text_input(values, MARKET_SYMBOL_FIELD)?; + let target_ticks = whole_number_input(values, TARGET_TICKS_FIELD)?; + move_market(svm, remote_ctx, symbol, target_ticks) + .await + .map(Some) + } + "phoenix-market-uncross" => { + let symbol = text_input(values, MARKET_SYMBOL_FIELD)?; + uncross_market(svm, remote_ctx, symbol).await.map(Some) + } + "phoenix-oracle-price" => { + let symbol = text_input(values, MARKET_SYMBOL_FIELD)?; + let mut context = MarketContext::load(svm, remote_ctx, symbol).await?; + let target_ticks = oracle_target(values, context.market.mark_ticks)?; + run_instructions(svm, &context.oracle_reports(target_ticks)?).map(Some) + } + "phoenix-trader-cancel-orders" => { + let text = text_input(values, TRADER_FIELD)?; + let trader = Pubkey::from_str(text).map_err(|e| { + SurfpoolError::internal(format!("{TRADER_FIELD} '{text}' is not an address: {e}")) + })?; + let symbols = match values.get(MARKET_SYMBOL_FIELD) { + None | Some(serde_json::Value::Null) => "", + Some(serde_json::Value::String(text)) => text.as_str(), + Some(_) => { + return Err(SurfpoolError::internal(format!( + "{MARKET_SYMBOL_FIELD} must be a string such as SOL, BTC, or empty for \ + every market" + ))); + } + }; + let markets: Vec<&str> = symbols + .split(',') + .map(str::trim) + .filter(|symbol| !symbol.is_empty()) + .collect(); + cancel_order_writes(svm, remote_ctx, trader, &markets) + .await + .map(Some) + } + "phoenix-trader-collateral" => { + let wanted = usdc_input(values, EFFECTIVE_COLLATERAL_FIELD)?; + collateral_writes(svm, remote_ctx, *account_pubkey, account, wanted) + .await + .map(Some) + } + // Phoenix's other accounts take the generic IDL path. + _ => Ok(None), } - Ok(()) } -pub fn build_phoenix_collateral_scenario( - trader: Pubkey, - trader_account: &Account, - target_quote_lots: &str, -) -> SurfpoolResult { - let target_quote_lots = parse_quote_lot_collateral(&serde_json::json!(target_quote_lots))?; - trader_header(&trader, trader_account)?; - - let values = HashMap::from([( - COLLATERAL_FIELD.to_string(), - serde_json::json!(target_quote_lots.to_string()), - )]); - let mut collateral_override = OverrideInstance::new( - COLLATERAL_TEMPLATE_ID.to_string(), - PREPARATION_SLOT, - AccountAddress::Pubkey(trader.to_string()), - ) - .with_values(values) - .with_label("Phoenix Trader collateral stress".to_string()); - collateral_override.fetch_before_use = true; - - let mut scenario = Scenario::new( - "Phoenix Trader Collateral Stress".to_string(), - "Set exact signed quote-lot collateral on a Phoenix Trader, and on its record in the local GlobalTraderIndex when the index lists it." - .to_string(), - ); - scenario.tags = vec![ - "phoenix-eternal".to_string(), - "collateral".to_string(), - "risk".to_string(), - ]; - scenario.add_override(collateral_override); - - Ok(scenario) +fn text_input<'a>( + values: &'a HashMap, + field: &str, +) -> SurfpoolResult<&'a str> { + values + .get(field) + .and_then(serde_json::Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| SurfpoolError::internal(format!("{field} must be a non-empty string"))) } -fn parse_decimal( - value: &serde_json::Value, +fn whole_number_input( + values: &HashMap, field: &str, - expected: &str, ) -> SurfpoolResult { - let parsed = match value { - serde_json::Value::String(text) => text.parse().ok(), - serde_json::Value::Number(number) if number.is_u64() => number.to_string().parse().ok(), + let parsed = match values.get(field) { + Some(serde_json::Value::String(text)) => text.trim().parse().ok(), + Some(serde_json::Value::Number(number)) => number.to_string().parse().ok(), _ => None, }; parsed.ok_or_else(|| { SurfpoolError::internal(format!( - "{field} must be {expected}, as a decimal string or a whole number" + "{field} must be a whole number, as a decimal string or a JSON number" )) }) } -#[cfg(test)] -mod tests { - use base64::{Engine, prelude::BASE64_STANDARD}; - use phoenix_rise_accounts::{PhoenixAccount, trader::TraderHeader}; - use solana_account::Account; +/// A USDC amount such as "-12.5" in Phoenix's quote lots, a millionth of a USDC each, rounded to +/// the nearest lot. +fn usdc_input(values: &HashMap, field: &str) -> SurfpoolResult { + values + .get(field) + .and_then(decimal) + .map(|usdc| (usdc * 1_000_000.0).round()) + .filter(|lots| lots.abs() < i64::MAX as f64) + .map(|lots| lots as i64) + .ok_or_else(|| { + SurfpoolError::internal(format!( + "{field} must be a USDC amount such as -12.5, as a decimal string or a JSON number" + )) + }) +} - use super::*; - use crate::scenarios::TemplateRegistry; - - const TRADER_HEADER_LEN: usize = size_of::(); - const COLLATERAL_BYTE_RANGE: core::ops::Range = 88..96; - const POSITION_MAP_PREFIX_LEN: usize = 16; - const POSITION_ENTRY_LEN: usize = 40; - const PERP_ASSET_MAP_LEN: usize = 1_622_064; - const SOL_PERP_ASSET_MAP_PREFIX_B64: &str = "jjZz33zvbCYBAAAAAAAAAF+FshYAAAAALAAAAAAAAAAtAAAAAQAAAAAEAAAAAAAAU09MAAAAAAAAAAAAAAAAAJIjVQMAAAAAK+yGGQAAAAAr7IYZAAAAABccAAAAAAAAK+yGGQAAAAAXHAAAAAAAACXshhkAAAAAFxwAAAAAAAAk7IYZAAAAABccAAAAAAAAIuyGGQAAAAAWHAAAAAAAACnshhkAAAAAGBwAAAAAAABkAAAAAAAAABkAAAAAAAAAK+yGGQAAAAAAAAAAAAAAAHUAAAAAAAAAdwEAAAAAAAByAQAAAAAAACvshhkAAAAAERwAAAAAAAAl7IYZAAAAABEcAAAAAAAAJOyGGQAAAAASHAAAAAAAACLshhkAAAAAERwAAAAAAAAp7IYZAAAAABEcAAAAAAAAZAAAAAAAAAAZAAAAAAAAACvshhkAAAAAGRwAAAAAAABkAAAAAAAAAGQAAAAAAAAAcgEAAAAAAAAk7IYZAAAAABkcAAAAAAAAJOyGGQAAAAAaHAAAAAAAAPjrhhkAAAAAFBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAQAAAAAAAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgIBAAAAAAACAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAACAgEAAAAAAAICAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAQAAAAAAAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgIBAAAAAAACAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2d/uHkzTMEI+nE0Ymaus9KEPf4oJXVEtWcWP29rQOywAAAAAAAAAAPBv/oFQVyUzn/BwmYuYTfvalmqearF4UMH8Xu10jPi1AAAAAAAAAAC9eIxYdxEuqtIqoFaGCUmDIS3Ki2887zwxOIzii37LZgAAAAAAAAAAp5Qc5gqxc5w9o5gk0/YHpMTClPTT8zaXjCVPspfSfxwAAAAAAAAAAIj2IrJxxvwcSeH0Zi3/xWcn5icVCYuh/OncuwHqSRBjAAAAAAAAAAD0AQEAAAAAACvshhkAAAAAnI6GGQAAAAAAAAAAAAAAAFRyhhkAAAAA5wAF8p4BAAAh9gTyngEAAFj1BPKeAQAAxu8E8p4BAAC6/ATyngEAAAAAAAAAAAAAAAAAAAAAAABZQzBUxbJLqOqoIX/f+QNvuZxLwZEZqXGqSDPsYEwjH2QAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATMvqAQAAAAAPAAAAAAAAABAnAAAAAAAATcvqAQAAAAABAAAAAAAAABAnAAAAAAAATsvqAQAAAAABAAAAAAAAABAnAAAAAAAAT8vqAQAAAAABAAAAAAAAABAnAAAAAAAAECcAAAAAAABQwwAAAAAAAKCGAQAAAAAAZAAAAAAAAAAgoQcAAAAAAMgAAAAAAAAAQEIPAAAAAAAsAQAAAAAAAICWmAAAAAAAkAEAAAAAAACIE9AH6ANMHWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKCQAAAAAAAJDaOWoAAAAAatw5agAAAAAQDgAAAAAAAIBRAQAAAAAAogYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnuYhAAAAAABMy+oBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQlRDAAAAAAAAAAAAAAAAAA=="; - - fn trader_fixture(collateral: i64, len: u64, capacity: u64) -> Vec { - let capacity = usize::try_from(capacity).expect("fixture capacity"); - let mut data = - vec![0_u8; TRADER_HEADER_LEN + POSITION_MAP_PREFIX_LEN + capacity * POSITION_ENTRY_LEN]; - data[..8].copy_from_slice(&PhoenixAccount::Trader.discriminant()); - data[COLLATERAL_BYTE_RANGE].copy_from_slice(&collateral.to_le_bytes()); - data[112..116].copy_from_slice(&(capacity as u32).to_le_bytes()); - data[TRADER_HEADER_LEN..TRADER_HEADER_LEN + 8].copy_from_slice(&len.to_le_bytes()); - data[TRADER_HEADER_LEN + 8..TRADER_HEADER_LEN + 16] - .copy_from_slice(&(capacity as u64).to_le_bytes()); - if len > 0 && capacity > 0 { - data[TRADER_HEADER_LEN + POSITION_MAP_PREFIX_LEN - ..TRADER_HEADER_LEN + POSITION_MAP_PREFIX_LEN + 8] - .copy_from_slice(&42_u64.to_le_bytes()); - } - data +/// `target_ticks` as given, or `change_percent` applied to the market's mark. +fn oracle_target( + values: &HashMap, + mark_ticks: u64, +) -> SurfpoolResult { + match ( + given(values, TARGET_TICKS_FIELD), + given(values, CHANGE_PERCENT_FIELD), + ) { + (Some(_), None) => whole_number_input(values, TARGET_TICKS_FIELD), + (None, Some(percent)) => percent_of(mark_ticks, percent), + _ => Err(SurfpoolError::internal(format!( + "set exactly one of {TARGET_TICKS_FIELD} and {CHANGE_PERCENT_FIELD}" + ))), } +} - fn trader_account() -> Account { - Account { - lamports: 1, - data: trader_fixture(0, 1, 2), - owner: PHOENIX_ETERNAL_PROGRAM_ID, - executable: false, - rent_epoch: 0, - } +/// `mark_ticks` moved by a percentage such as "-5" or "2.5", rounded to the nearest tick. +fn percent_of(mark_ticks: u64, percent: &serde_json::Value) -> SurfpoolResult { + let change = decimal(percent).ok_or_else(|| { + SurfpoolError::internal(format!( + "{CHANGE_PERCENT_FIELD} must be a number such as -5 or 2.5, not {percent}" + )) + })?; + let moved = (mark_ticks as f64 * (1.0 + change / 100.0)).round(); + if moved < 1.0 { + return Err(SurfpoolError::internal(format!( + "{CHANGE_PERCENT_FIELD} {change} leaves no positive price from {mark_ticks} ticks" + ))); } + Ok(moved as u64) +} - fn trader_account_for(trader: Pubkey, collateral: i64) -> Account { - let mut account = Account { - data: trader_fixture(collateral, 1, 2), - ..trader_account() - }; - account.data[24..56].copy_from_slice(trader.as_ref()); - account +/// A decimal string or a JSON number, while it is finite. +fn decimal(value: &serde_json::Value) -> Option { + match value { + serde_json::Value::String(text) => text.trim().parse::().ok(), + serde_json::Value::Number(number) => number.as_f64(), + _ => None, } + .filter(|value| value.is_finite()) +} - fn perp_asset_map_fixture() -> Vec { - let prefix = BASE64_STANDARD - .decode(SOL_PERP_ASSET_MAP_PREFIX_B64) - .unwrap(); - let mut data = vec![0_u8; PERP_ASSET_MAP_LEN]; - data[..prefix.len()].copy_from_slice(&prefix); - data[24..26].copy_from_slice(&1_u16.to_le_bytes()); - data[32..36].copy_from_slice(&1_u32.to_le_bytes()); - data[36..40].copy_from_slice(&0_u32.to_le_bytes()); - data +/// Nothing refreshes oracle readings or makers' quotes locally, so this raises the map's stale +/// thresholds, restamps old readings, rewinds funding ahead of the Clock and keeps every spline +/// region quoting ([`keep_quoting`]). A failure only warns and keeps the map. +pub(crate) async fn keep_markets_usable( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + target_slot: u64, +) { + let kept = async { + hydrate(svm, remote_ctx, &[PHOENIX_PERP_ASSET_MAP]).await?; + let map = local_account(svm, &PHOENIX_PERP_ASSET_MAP)?; + let clock = svm.inner.get_sysvar::(); + let raised = raise_stale_thresholds(&PHOENIX_PERP_ASSET_MAP, &map.data)?; + let now = u64::try_from(clock.unix_timestamp).unwrap_or(0); + let rewound = rewind_funding_timestamps( + &PHOENIX_PERP_ASSET_MAP, + raised.as_deref().unwrap_or(&map.data), + now, + )?; + let patched = rewound.or(raised); + let restamped = restamp_readings( + &PHOENIX_PERP_ASSET_MAP, + patched.as_deref().unwrap_or(&map.data), + clock.slot, + )?; + if let Some(data) = restamped.or(patched) { + svm.set_scenario_override_account( + &PHOENIX_PERP_ASSET_MAP, + Account { + data, + ..map.clone() + }, + target_slot, + )?; + } + let splines: Vec = phoenix_markets(PHOENIX_PERP_ASSET_MAP, &map)? + .iter() + .map(|market| market.splines) + .collect(); + if let Err(e) = hydrate(svm, remote_ctx, &splines).await { + warn!("Could not load every Phoenix spline collection, keeping the local ones quoting: {e}"); + } + // One collection that cannot be kept quoting must not stop the others. + for address in splines { + let kept = match svm.inner.get_account(&address) { + Ok(Some(account)) => match keep_quoting(&address, &account.data) { + Ok(Some(data)) => svm.set_scenario_override_account( + &address, + Account { data, ..account }, + target_slot, + ), + other => other.map(|_| ()), + }, + Ok(None) => Ok(()), + Err(e) => Err(e), + }; + if let Err(e) = kept { + warn!("Could not keep {address} quoting: {e}"); + } + } + Ok::<_, SurfpoolError>(()) } + .await; + if let Err(e) = kept { + warn!("Could not keep the Phoenix markets usable in the local VM: {e}"); + } +} - fn perp_asset_map_account() -> Account { - Account { - lamports: 1, - data: perp_asset_map_fixture(), - owner: PHOENIX_ETERNAL_PROGRAM_ID, - executable: false, - rent_epoch: 0, +/// The collection with every region's lifespan set to [`GTC_LIFESPAN`], or `None` when nothing +/// changes: nobody re-quotes on a fork, so mainnet's lifespans of a few slots would empty the book. +fn keep_quoting(address: &Pubkey, data: &[u8]) -> SurfpoolResult>> { + // The layout of phoenix-rise-accounts' SplineCollection: a 112-byte header, then each spline's + // bid and ask regions back to back from byte 120 of it, with the lifespan last in each region. + const HEADER_LEN: usize = 112; + const REGIONS_START: usize = 120; + const LIFESPAN_START: usize = 40; + let collection = SplineCollection::try_from_account_bytes(data).map_err(|e| { + SurfpoolError::invalid_account_data( + address, + "Expected a Phoenix spline collection", + Some(e), + ) + })?; + let mut patched = data.to_vec(); + for spline in 0..collection.num_splines() as usize { + for region in 0..2 * SPLINE_REGION_CAPACITY { + let start = HEADER_LEN + + spline * size_of::() + + REGIONS_START + + region * size_of::() + + LIFESPAN_START; + patched[start..start + 8].copy_from_slice(>C_LIFESPAN.to_le_bytes()); } } + Ok((patched != data).then_some(patched)) +} - #[test] - fn builds_one_collateral_override() { - let trader = Pubkey::new_unique(); - let funded = trader_account_for(trader, 500); - let registry = TemplateRegistry::new(); +/// Every market's metadata in the map, with the offset it starts at in `data`. +fn markets_in_map( + account_pubkey: &Pubkey, + data: &[u8], +) -> SurfpoolResult> { + let mut markets = Vec::new(); + // The decoder walks the entries in storage order, and each market's metadata holds its own + // market account, so it occurs once in the map + let mut cursor = 0; + for entry in map_entries(account_pubkey, data)? { + let metadata = entry.metadata; + let metadata_bytes = metadata.as_bytes(); + let offset = data[cursor..] + .windows(metadata_bytes.len()) + .position(|window| window == metadata_bytes) + .map(|position| cursor + position) + .ok_or_else(|| { + invalid_perp_asset_map(account_pubkey, "Phoenix market metadata was not found") + })?; + cursor = offset + metadata_bytes.len(); + markets.push((offset, metadata)); + } + Ok(markets) +} - for target in ["500", "-9007199254740993"] { - let preparation = build_phoenix_collateral_scenario(trader, &funded, target).unwrap(); - assert_eq!(preparation.overrides.len(), 1); - let collateral_override = &preparation.overrides[0]; - assert!( - registry.contains(&collateral_override.template_id), - "the override must name a bundled template" - ); - assert_eq!( - collateral_override.account, - AccountAddress::Pubkey(trader.to_string()) - ); - assert_eq!( - collateral_override.values[COLLATERAL_FIELD], - serde_json::json!(target) - ); - assert_eq!(collateral_override.scenario_relative_slot, PREPARATION_SLOT); - assert!(collateral_override.fetch_before_use); +/// The map with every market's spot and perp oracle stale threshold raised to +/// [`RAISED_STALE_THRESHOLD_SLOTS`], or `None` when they already are. A higher threshold is kept. +fn raise_stale_thresholds(account_pubkey: &Pubkey, data: &[u8]) -> SurfpoolResult>> { + let price_len = size_of::(); + let mut patched: Option> = None; + for (offset, metadata) in markets_in_map(account_pubkey, data)? { + // The PriceComponent is the metadata's first field. + let mut price = *metadata.oracle_price(); + let mark = &mut price.mark_price; + let mut raised = false; + for threshold in [ + &mut mark.spot_price_component.stale_threshold, + &mut mark.perp_price_component.stale_threshold, + ] { + if *threshold < RAISED_STALE_THRESHOLD_SLOTS { + *threshold = RAISED_STALE_THRESHOLD_SLOTS; + raised = true; + } + } + if raised { + patched.get_or_insert_with(|| data.to_vec())[offset..offset + price_len] + .copy_from_slice(bytemuck::bytes_of(&price)); } } + Ok(patched) +} - #[test] - fn direct_mark_patches_only_the_selected_mark_ticks_and_slot() { - let account = perp_asset_map_account(); - let values = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - (DIRECT_MARK_TICKS_FIELD.to_string(), serde_json::json!("1")), - ]); - - let patched = - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123).unwrap(); - let after = PerpAssetMap::try_from_account_bytes(&patched) - .unwrap() - .find_by_symbol("SOL") - .unwrap() - .unwrap(); - let mark = after.metadata.oracle_price().mark_price; - assert_eq!((mark.price.ticks.as_inner(), mark.price.slot), (1, 123)); - for sample in mark +/// The map with reading slots before `slot` moved up to it, or `None`; prices stay. On an old map a +/// report's EMA leaves a deep search move no positive price, and Phoenix refuses the margin. +fn restamp_readings( + account_pubkey: &Pubkey, + data: &[u8], + slot: u64, +) -> SurfpoolResult>> { + let price_len = size_of::(); + let mut patched: Option> = None; + for (offset, metadata) in markets_in_map(account_pubkey, data)? { + // The PriceComponent is the metadata's first field. + let mut price = *metadata.oracle_price(); + let mark = &mut price.mark_price; + let samples = mark .spot_price_component .last_exchange_spot_price - .iter() - .chain(mark.perp_price_component.last_exchange_perp_price.iter()) + .iter_mut() + .chain( + mark.perp_price_component + .last_exchange_perp_price + .iter_mut(), + ) + .map(|sample| &mut sample.slot); + let mut stamped = false; + for reading in [&mut mark.price.slot, &mut mark.spot_price_component.slot] + .into_iter() + .chain(samples) { - assert_eq!((sample.ticks.as_inner(), sample.slot), (1, 123)); + // An unused oracle sample stays empty rather than becoming a fresh zero price. + if *reading != 0 && *reading < slot { + *reading = slot; + stamped = true; + } } - assert_eq!(mark.spot_price_component.slot, 123); - assert_eq!(patched.len(), account.data.len()); - - for rejected in ["0", "4294967296"] { - let values = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - ( - DIRECT_MARK_TICKS_FIELD.to_string(), - serde_json::json!(rejected), - ), - ]); - assert!( - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123).is_err(), - "{rejected}" - ); + if stamped { + patched.get_or_insert_with(|| data.to_vec())[offset..offset + price_len] + .copy_from_slice(bytemuck::bytes_of(&price)); } } + Ok(patched) +} - #[tokio::test] - async fn direct_mark_is_stamped_with_the_clock_slot() { - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.inner - .set_account(PHOENIX_PERP_ASSET_MAP, perp_asset_map_account()) - .unwrap(); - let mut clock = svm.inner.get_sysvar::(); - clock.slot = 1_000; - svm.inner.set_sysvar(&clock); - let template = TemplateRegistry::new() - .get("phoenix-direct-mark-risk-shock") - .expect("template") - .clone(); - let mut scenario = Scenario::new("mark".to_string(), "mark".to_string()); - scenario.add_override( - OverrideInstance::new(template.id, 0, template.address).with_values(HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - (DIRECT_MARK_TICKS_FIELD.to_string(), serde_json::json!("1")), - ])), - ); - // Registered for an earlier slot, which surfnet_registerScenario allows, and played at once. - svm.register_scenario(scenario, Some(900)).unwrap(); +/// The map with funding timestamps after `now` (Unix seconds) moved back to it, or `None`. Phoenix +/// fails to update funding, and every price, while the Clock is before the last update. +fn rewind_funding_timestamps( + account_pubkey: &Pubkey, + data: &[u8], + now: u64, +) -> SurfpoolResult>> { + let mut patched: Option> = None; + for (offset, metadata) in markets_in_map(account_pubkey, data)? { + let mut funding = *metadata.funding_accumulator(); + let mut rewound = false; + for timestamp in [ + &mut funding.start_interval_timestamp, + &mut funding.last_funding_update_timestamp, + ] { + if bytemuck::cast::<_, u64>(*timestamp) > now { + *timestamp = bytemuck::cast(now); + rewound = true; + } + } + if rewound { + // The metadata layout is private to the crate, so the field's offset is taken from + // the decoded copy. + let start = offset + + metadata.funding_accumulator() as *const FundingAccumulator as usize + - metadata.as_bytes().as_ptr() as usize; + patched.get_or_insert_with(|| data.to_vec()) + [start..start + size_of::()] + .copy_from_slice(bytemuck::bytes_of(&funding)); + } + } + Ok(patched) +} - svm.materialize_overrides_for_slot(&None, 900) - .await - .unwrap(); +#[cfg(test)] +pub(crate) mod tests { + use std::collections::HashSet; - let account = svm - .inner - .get_account(&PHOENIX_PERP_ASSET_MAP) - .unwrap() - .unwrap(); - let price = PerpAssetMap::try_from_account_bytes(&account.data) - .unwrap() - .find_by_symbol("SOL") - .unwrap() - .unwrap() - .metadata - .oracle_price() - .mark_price - .price; - assert_eq!( - (price.ticks.as_inner(), price.slot), - (1, 1_000), - "the mark must be fresh at the slot the program reads it, not the scheduled one" - ); - } + use phoenix_rise_accounts::{ + PhoenixAccount, + perp_asset_map::{PerpAssetMap, PerpAssetMetadata}, + }; + use solana_system_interface::instruction as system_instruction; + use surfpool_types::AccountAddress; + + use super::*; + use crate::scenarios::{ + protocols::phoenix_eternal::v1::market::tests::perp_asset_map_account, + registry::template_registry, + }; #[test] - fn maintenance_factor_patches_only_the_selected_market_factor() { - let account = perp_asset_map_account(); - let factors = |data: &[u8]| { - PerpAssetMap::try_from_account_bytes(data) - .unwrap() - .find_by_symbol("SOL") - .unwrap() - .unwrap() - .metadata - .risk_params() - .risk_factors + fn a_failed_write_puts_back_the_accounts_written_before_it() { + let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let (first, unloadable) = (Pubkey::new_unique(), Pubkey::new_unique()); + let before = Account { + lamports: 1_000_000, + data: vec![1; 8], + owner: PHOENIX_PROGRAM_ID, + ..Account::default() }; - let before = factors(&account.data); - let values = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), + svm.set_account(&first, before.clone()).unwrap(); + // A program account whose bytes are no program is refused. + let broken_program = Account { + lamports: 1_000_000, + data: vec![0xff; 64], + owner: Pubkey::from_str_const("BPFLoader2111111111111111111111111111111111"), + executable: true, + rent_epoch: 0, + }; + let writes = vec![ ( - MAINTENANCE_FACTOR_FIELD.to_string(), - serde_json::json!("10000"), + first, + Account { + data: vec![2; 8], + ..before.clone() + }, ), - ]); + (unloadable, broken_program), + ]; - let patched = - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123).unwrap(); - assert_eq!(factors(&patched), [10_000, before[1], before[2]]); - let changed = patched - .iter() - .zip(&account.data) - .filter(|(after, before)| after != before) - .count(); - assert!(changed <= 2, "only the factor's two bytes may change"); - - let maintenance = |factor: &str| { - let values = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - ( - MAINTENANCE_FACTOR_FIELD.to_string(), - serde_json::json!(factor), - ), - ]); - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123) - }; - // Just above the backstop factor still leaves a Liquidatable tier. - let lowest = before[1] + 1; - assert_eq!( - factors(&maintenance(&lowest.to_string()).unwrap()), - [lowest, before[1], before[2]] - ); - // At or below the backstop factor, above 100%, or not basis points. - for rejected in [&before[1].to_string(), "0", "10001", "65536", "1.5"] { - assert!(maintenance(rejected).is_err(), "{rejected}"); - } + assert!(write_all_or_none(&mut svm, writes, 1).is_err()); + assert_eq!(svm.inner.get_account(&first).unwrap(), Some(before)); } - // These templates' fields are codec inputs, not IDL paths, so the registry's IDL check - // cannot cover them; this ties the YAML field names to what the codec accepts. #[test] - fn market_list_reports_what_the_market_writers_wrote() { - let mut account = perp_asset_map_account(); - for (field, value) in [ - (DIRECT_MARK_TICKS_FIELD, "777"), - (MAINTENANCE_FACTOR_FIELD, "9000"), - ] { - let values = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - (field.to_string(), serde_json::json!(value)), - ]); - account.data = - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123).unwrap(); - } + fn a_failed_write_deletes_the_accounts_written_before_it_created() { + let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let (created, unloadable) = (Pubkey::new_unique(), Pubkey::new_unique()); + let broken_program = Account { + lamports: 1_000_000, + data: vec![0xff; 64], + owner: Pubkey::from_str_const("BPFLoader2111111111111111111111111111111111"), + executable: true, + rent_epoch: 0, + }; + let writes = vec![ + ( + created, + Account { + lamports: 1_000_000, + data: vec![2; 8], + owner: PHOENIX_PROGRAM_ID, + ..Account::default() + }, + ), + (unloadable, broken_program), + ]; - assert_eq!( - phoenix_markets(PHOENIX_PERP_ASSET_MAP, &account).unwrap(), - vec![PhoenixMarket { - symbol: "SOL".to_string(), - orderbook: Pubkey::from_str_const("71Si24E4uc3oCaPbPZTozC1ptSNNqygjjebxSmErSsC2"), - mark_ticks: 777, - tick_size: 100, - base_lot_decimals: 2, - maintenance_risk_factor_bps: 9_000, - backstop_risk_factor_bps: 2_000, - }] + assert!(write_all_or_none(&mut svm, writes, 1).is_err()); + assert!(svm.get_account(&created).unwrap().is_none()); + assert!( + !svm.offline_accounts + .contains_key(&created.to_string()) + .unwrap() ); - - let foreign = Account { - owner: Pubkey::new_unique(), - ..perp_asset_map_account() - }; - assert!(phoenix_markets(PHOENIX_PERP_ASSET_MAP, &foreign).is_err()); } - #[test] - fn the_codec_accepts_every_market_template_field_set() { - let registry = TemplateRegistry::new(); - let market_templates: Vec<_> = registry + /// Every Phoenix template that carries a fixed address: its id, account type and address. + pub(crate) fn template_addresses() -> Vec<(&'static str, &'static str, Pubkey)> { + template_registry() .by_protocol("Phoenix Eternal") .into_iter() - .filter(|template| template.account_type == "PerpAssetMap") - .collect(); - assert_eq!( - market_templates.len(), - 2, - "direct mark and maintenance margin" - ); - - for template in market_templates { - assert_eq!( - template.address, - AccountAddress::Pubkey(PHOENIX_PERP_ASSET_MAP.to_string()), - "{}", - template.id - ); - let values = template - .properties - .iter() - .map(|property| { - let value = match property.path.as_str() { - MARKET_SYMBOL_FIELD => "SOL", - MAINTENANCE_FACTOR_FIELD => "10000", - _ => "1", - }; - (property.path.clone(), serde_json::json!(value)) - }) - .collect(); - forge_phoenix_override(&Pubkey::new_unique(), &perp_asset_map_account(), &values, 1) - .unwrap_or_else(|error| panic!("{}: {error}", template.id)); - } - } - - #[test] - fn market_overrides_ignore_keys_outside_the_codec_inputs() { - // An editor that starts from the decoded map sends its top-level scalars and arrays too. - let account = perp_asset_map_account(); - for (field, value) in [ - (DIRECT_MARK_TICKS_FIELD, "1"), - (MAINTENANCE_FACTOR_FIELD, "10000"), - ] { - let clean = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - (field.to_string(), serde_json::json!(value)), - ]); - let mut editor = clean.clone(); - editor.insert("numAssets".to_string(), serde_json::json!(1)); - editor.insert( - "padding0".to_string(), - serde_json::json!([0, 0, 0, 0, 0, 0]), - ); - assert_eq!( - forge_phoenix_override(&Pubkey::new_unique(), &account, &editor, 123).unwrap(), - forge_phoenix_override(&Pubkey::new_unique(), &account, &clean, 123).unwrap(), - ); - } - } - - #[test] - fn market_overrides_need_a_symbol_and_exactly_one_codec_input() { - let account = perp_asset_map_account(); - let symbol = (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")); - let ticks = (DIRECT_MARK_TICKS_FIELD.to_string(), serde_json::json!("1")); - let factor = ( - MAINTENANCE_FACTOR_FIELD.to_string(), - serde_json::json!("10000"), - ); - for values in [ - vec![symbol.clone()], - vec![symbol.clone(), ticks.clone(), factor.clone()], - vec![ticks.clone()], - vec![factor.clone()], - vec![ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!(1)), - ticks.clone(), - ], - ] { - let values = HashMap::from_iter(values); - assert!( - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123).is_err(), - "{values:?}" - ); - } + .filter_map(|template| match &template.address { + AccountAddress::Pubkey(address) if !address.is_empty() => Some(( + template.id.as_str(), + template.account_type.as_str(), + Pubkey::from_str(address).unwrap_or_else(|e| panic!("{}: {e}", template.id)), + )), + _ => None, + }) + .collect() } #[test] - fn market_inputs_take_whole_json_numbers_as_well_as_strings() { - let account = perp_asset_map_account(); - let forge = |field: &str, value: serde_json::Value| { - let values = HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - (field.to_string(), value), - ]); - forge_phoenix_override(&Pubkey::new_unique(), &account, &values, 123) - }; - for (field, text, number) in [ - (DIRECT_MARK_TICKS_FIELD, "1", 1), - (MAINTENANCE_FACTOR_FIELD, "10000", 10_000), - ] { - assert_eq!( - forge(field, serde_json::json!(number)).unwrap(), - forge(field, serde_json::json!(text)).unwrap(), - "{field}" - ); - for rejected in [serde_json::json!(-1), serde_json::json!(1.5)] { - assert!( - forge(field, rejected.clone()).is_err(), - "{field}: {rejected}" - ); + fn templates_carry_the_addresses_the_writers_use() { + let addresses = template_addresses(); + assert!(!addresses.is_empty()); + let mut withdraw_queues = HashSet::new(); + for (id, account_type, address) in addresses { + match account_type { + "PerpAssetMap" => assert_eq!(address, PHOENIX_PERP_ASSET_MAP, "{id}"), + "GlobalConfiguration" => assert_eq!(address, PHOENIX_GLOBAL_CONFIG, "{id}"), + // The writers read this one from GlobalConfig; the mainnet tests check it there. + "WithdrawQueueHeader" => { + withdraw_queues.insert(address); + } + other => panic!("{id} carries an address for {other}, which nothing checks"), } } + assert_eq!(withdraw_queues.len(), 1, "{withdraw_queues:?}"); } /// Where the fixture markets' PriceComponents start: after the 48-byte map header, each @@ -1166,178 +1312,412 @@ mod tests { ); } - #[tokio::test] - async fn a_map_override_leaves_every_market_usable() { - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.inner - .set_account(PHOENIX_PERP_ASSET_MAP, two_market_map_account()) - .unwrap(); - let template = TemplateRegistry::new() - .get("phoenix-maintenance-margin-stress") - .expect("template") - .clone(); - let mut scenario = Scenario::new("maintenance".to_string(), "maintenance".to_string()); - scenario.add_override( - OverrideInstance::new(template.id, 0, template.address).with_values(HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - ( - MAINTENANCE_FACTOR_FIELD.to_string(), - serde_json::json!("9000"), - ), - ])), + #[test] + fn rewinding_funding_timestamps_moves_only_those_ahead_of_the_clock() { + let account = two_market_map_account(); + let funding = |data: &[u8], symbol: &str| *market(data, symbol).funding_accumulator(); + let seconds = |timestamp| bytemuck::cast::<_, u64>(timestamp); + let now = seconds(funding(&account.data, "SOL").last_funding_update_timestamp) - 60; + + let rewound = rewind_funding_timestamps(&PHOENIX_PERP_ASSET_MAP, &account.data, now) + .unwrap() + .expect("the fixture's last funding update is ahead of the clock"); + + for symbol in ["SOL", "BTC"] { + let before = market(&account.data, symbol); + let mut expected = *before.funding_accumulator(); + expected.last_funding_update_timestamp = bytemuck::cast(now); + if seconds(expected.start_interval_timestamp) > now { + expected.start_interval_timestamp = bytemuck::cast(now); + } + let after = market(&rewound, symbol); + assert_eq!(*after.funding_accumulator(), expected, "{symbol}"); + assert_eq!(after.oracle_price(), before.oracle_price(), "{symbol}"); + assert_eq!(after.risk_params(), before.risk_params(), "{symbol}"); + } + assert_eq!( + rewind_funding_timestamps(&PHOENIX_PERP_ASSET_MAP, &rewound, now).unwrap(), + None, + "a rewound map is written once" ); - svm.register_scenario(scenario, Some(100)).unwrap(); + assert_eq!( + rewind_funding_timestamps(&PHOENIX_PERP_ASSET_MAP, &account.data, u64::MAX).unwrap(), + None, + "timestamps before the clock stay" + ); + } - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); + #[test] + fn restamping_moves_old_reading_slots_to_the_clock_and_keeps_prices() { + let account = two_market_map_account(); + let readings = |data: &[u8], symbol: &str| { + let mark = market(data, symbol).oracle_price().mark_price; + let samples: Vec<_> = mark + .spot_price_component + .last_exchange_spot_price + .into_iter() + .chain(mark.perp_price_component.last_exchange_perp_price) + .collect(); + (mark, samples) + }; + let (sol, _) = readings(&account.data, "SOL"); + let slot = sol.price.slot + 300; - let map = svm.get_account(&PHOENIX_PERP_ASSET_MAP).unwrap().unwrap(); - let metadata = PerpAssetMap::try_from_account_bytes(&map.data) - .unwrap() - .find_by_symbol("SOL") + let restamped = restamp_readings(&PHOENIX_PERP_ASSET_MAP, &account.data, slot) .unwrap() - .unwrap() - .metadata; - assert_eq!(metadata.risk_params().risk_factors[0], 9_000); + .expect("the fixture's readings are before the slot"); + for symbol in ["SOL", "BTC"] { + let (before, before_samples) = readings(&account.data, symbol); + let (after, after_samples) = readings(&restamped, symbol); + assert_eq!(after.price.ticks, before.price.ticks, "{symbol}"); + assert_eq!(after.price.slot, slot, "{symbol}"); + assert_eq!(after.spot_price_component.slot, slot, "{symbol}"); + for (after, before) in after_samples.iter().zip(&before_samples) { + assert_eq!(after.ticks, before.ticks, "{symbol}"); + assert_eq!( + after.slot, + if before.slot == 0 { 0 } else { slot }, + "{symbol}" + ); + } assert_eq!( - oracle_thresholds(&map.data, symbol), - RAISED_THRESHOLDS, + after.book_price_component, before.book_price_component, "{symbol}" ); + assert_eq!( + market(&restamped, symbol).risk_params(), + market(&account.data, symbol).risk_params(), + "{symbol}" + ); + } + assert_eq!( + restamp_readings(&PHOENIX_PERP_ASSET_MAP, &restamped, slot).unwrap(), + None, + "a restamped map is written once" + ); + assert_eq!( + restamp_readings(&PHOENIX_PERP_ASSET_MAP, &account.data, 1).unwrap(), + None, + "readings ahead of the clock stay" + ); + } + + #[test] + fn keeps_every_spline_region_quoting_and_writes_nothing_else() { + // A header naming two splines, mainnet's short lifespans, and a spare slot after them. + let spare = 112 + 2 * size_of::(); + let mut data = vec![6_u8; spare + size_of::()]; + data[..112].fill(0); + data[..8].copy_from_slice(&PhoenixAccount::SplineCollection.discriminant()); + data[40..43].copy_from_slice(b"SOL"); + data[72..76].copy_from_slice(&2_u32.to_le_bytes()); + let address = Pubkey::new_unique(); + + let patched = keep_quoting(&address, &data).unwrap().unwrap(); + + for spline in SplineCollection::try_from_account_bytes(&patched) + .unwrap() + .splines() + { + let spline = spline.unwrap(); + for region in spline.bid_regions().iter().chain(spline.ask_regions()) { + assert_eq!(region.lifespan(), GTC_LIFESPAN); + } } + let changed = (0..data.len()).filter(|i| patched[*i] != data[*i]).count(); + assert_eq!( + changed, + 2 * 2 * SPLINE_REGION_CAPACITY * 8, + "only the lifespans change" + ); + assert_eq!( + keep_quoting(&address, &patched).unwrap(), + None, + "written once" + ); } #[tokio::test] - async fn a_map_override_still_applies_when_the_thresholds_cannot_be_raised() { - // A second entry with a non-ASCII symbol: finding SOL stops before it, raising every - // market's threshold does not. - let mut map = perp_asset_map_account(); - map.data[32..36].copy_from_slice(&2_u32.to_le_bytes()); - map.data[1_632] = 0xFF; + async fn keeps_every_local_market_usable_and_never_fails_an_override() { let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.inner.set_account(PHOENIX_PERP_ASSET_MAP, map).unwrap(); - let template = TemplateRegistry::new() - .get("phoenix-maintenance-margin-stress") - .expect("template") - .clone(); - let mut scenario = Scenario::new("maintenance".to_string(), "maintenance".to_string()); - scenario.add_override( - OverrideInstance::new(template.id, 0, template.address).with_values(HashMap::from([ - (MARKET_SYMBOL_FIELD.to_string(), serde_json::json!("SOL")), - ( - MAINTENANCE_FACTOR_FIELD.to_string(), - serde_json::json!("9000"), - ), - ])), + // Nothing to raise and nowhere to fetch the map from: the override goes on. + keep_markets_usable(&mut svm, &None, 0).await; + assert!( + svm.inner + .get_account(&PHOENIX_PERP_ASSET_MAP) + .unwrap() + .is_none() ); - svm.register_scenario(scenario, Some(100)).unwrap(); - svm.materialize_overrides_for_slot(&None, 100) - .await + svm.inner + .set_account(PHOENIX_PERP_ASSET_MAP, two_market_map_account()) .unwrap(); - - let map = svm.get_account(&PHOENIX_PERP_ASSET_MAP).unwrap().unwrap(); - let metadata = PerpAssetMap::try_from_account_bytes(&map.data) - .unwrap() - .find_by_symbol("SOL") - .unwrap() + keep_markets_usable(&mut svm, &None, 0).await; + let map = svm + .inner + .get_account(&PHOENIX_PERP_ASSET_MAP) .unwrap() - .metadata; - assert_eq!(metadata.risk_params().risk_factors[0], 9_000); + .unwrap(); + for symbol in ["SOL", "BTC"] { + assert_eq!( + oracle_thresholds(&map.data, symbol), + RAISED_THRESHOLDS, + "{symbol}" + ); + } } - #[tokio::test] - async fn any_phoenix_override_leaves_the_local_map_usable() { - use crate::surfnet::GeyserEvent; - - let trader = Pubkey::new_unique(); - let funded = trader_account_for(trader, 500); - let (mut svm, _events_rx, geyser_rx) = SurfnetSvm::default(); - svm.set_account(&PHOENIX_PERP_ASSET_MAP, perp_asset_map_account()) - .unwrap(); - svm.set_account(&trader, funded.clone()).unwrap(); - let scenario = build_phoenix_collateral_scenario(trader, &funded, "100").unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); + #[test] + fn reads_template_inputs_as_strings_or_numbers() { + let values = HashMap::from([ + ("symbol".to_string(), serde_json::json!("SOL")), + ("target_ticks".to_string(), serde_json::json!("10750")), + ("as_number".to_string(), serde_json::json!(10750)), + ]); + assert_eq!(text_input(&values, "symbol").unwrap(), "SOL"); + let ticks = |field| whole_number_input::(&values, field).unwrap(); + assert_eq!((ticks("target_ticks"), ticks("as_number")), (10750, 10750)); + + let bad = HashMap::from([ + ("symbol".to_string(), serde_json::json!("")), + ("target_ticks".to_string(), serde_json::json!("-1")), + ("as_number".to_string(), serde_json::json!(1.5)), + ]); + assert!(text_input(&bad, "symbol").is_err()); + assert!(text_input(&bad, "missing").is_err()); + assert!(whole_number_input::(&bad, "target_ticks").is_err()); + assert!(whole_number_input::(&bad, "as_number").is_err()); + } - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); + #[test] + fn takes_the_oracle_target_in_ticks_or_as_a_percentage_of_the_mark() { + let target = |pairs: &[(&str, serde_json::Value)]| { + let values = pairs + .iter() + .map(|(field, value)| (field.to_string(), value.clone())) + .collect(); + oracle_target(&values, 10_801) + }; + let ticks = serde_json::json!("10261"); + let percent = |value| (CHANGE_PERCENT_FIELD, value); + assert_eq!( + target(&[(TARGET_TICKS_FIELD, ticks.clone())]).unwrap(), + 10_261 + ); + assert_eq!(target(&[percent(serde_json::json!("-5"))]).unwrap(), 10_261); + assert_eq!( + target(&[percent(serde_json::json!("+2.5"))]).unwrap(), + 11_071 + ); + assert_eq!( + target(&[percent(serde_json::json!(-0.25))]).unwrap(), + 10_774 + ); + assert_eq!( + target(&[ + (TARGET_TICKS_FIELD, serde_json::json!("")), + percent(serde_json::json!("10")) + ]) + .unwrap(), + 11_881 + ); - let stressed = svm.get_account(&trader).unwrap().unwrap(); - assert_eq!(stressed.data[COLLATERAL_BYTE_RANGE], 100_i64.to_le_bytes()); - let map = svm.get_account(&PHOENIX_PERP_ASSET_MAP).unwrap().unwrap(); - assert_eq!(oracle_thresholds(&map.data, "SOL"), RAISED_THRESHOLDS); + for bad in ["5%", "abc", "inf", "-100", "-150"] { + assert!( + target(&[percent(serde_json::json!(bad))]).is_err(), + "{bad:?} is refused" + ); + } + assert!(target(&[]).is_err(), "one of the two is needed"); assert!( - geyser_rx.try_iter().any(|event| matches!( - event, - GeyserEvent::UpdateAccount(update) - if update.pubkey == PHOENIX_PERP_ASSET_MAP - && update.account == map - && update.slot == 100 - )), - "Geyser plugins receive the raised map at the materialized slot" + target(&[ + (TARGET_TICKS_FIELD, ticks), + percent(serde_json::json!("-5")) + ]) + .is_err(), + "not both" ); } - #[tokio::test] - async fn override_on_any_other_phoenix_account_leaves_the_local_map_usable() { - const EXPIRES_AT_BYTE_RANGE: core::ops::Range = 88..96; - let permission = Pubkey::new_unique(); - let mut data = vec![0_u8; 168]; - data[..8].copy_from_slice(&PhoenixAccount::PermissionAccount.discriminant()); - let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&PHOENIX_PERP_ASSET_MAP, perp_asset_map_account()) - .unwrap(); - svm.set_account( - &permission, - Account { - data, - ..trader_account() - }, + #[test] + fn reads_collateral_as_a_signed_string_or_number() { + let collateral = |value| { + let values = HashMap::from([(EFFECTIVE_COLLATERAL_FIELD.to_string(), value)]); + usdc_input(&values, EFFECTIVE_COLLATERAL_FIELD) + }; + assert_eq!( + collateral(serde_json::json!(" -5030.91685 ")).unwrap(), + -5_030_916_850 + ); + assert_eq!(collateral(serde_json::json!(8.191788)).unwrap(), 8_191_788); + assert_eq!( + collateral(serde_json::json!("750.60748")).unwrap(), + 750_607_480 + ); + assert_eq!(collateral(serde_json::json!(12)).unwrap(), 12_000_000); + for bad in ["", "12 USDC", "NaN", "inf", "1e13"] { + assert!(collateral(serde_json::json!(bad)).is_err(), "{bad}"); + } + assert!(collateral(serde_json::json!(true)).is_err()); + } + + const LIQUIDATOR: Pubkey = + Pubkey::from_str_const("BP7sV1VFnbPMPyJX1tZNbXHbZkyLNFEaBWJhyMvkbxKz"); + const QUOTE_MINT: &str = "PhUsd11YkbjSaWjFncfAAmatntsjx3MgDR9B6g1ks3A"; + + fn liquidation_accounts() -> Vec<(&'static str, Pubkey)> { + let trader = Pubkey::new_unique(); + vec![ + ( + "phoenixProgram", + Pubkey::from_str(&phoenix_idl().address).unwrap(), + ), + ("phoenixLogAuthority", Pubkey::new_unique()), + ("globalConfiguration", Pubkey::new_unique()), + ("liquidatorWallet", LIQUIDATOR), + ("liquidatedTrader", trader), + ("traderAccount", trader), + ("perpAssetMap", Pubkey::new_unique()), + ("globalTraderIndex", Pubkey::new_unique()), + ("activeTraderBuffer", Pubkey::new_unique()), + ("orderbook", Pubkey::new_unique()), + ("splines", Pubkey::new_unique()), + ] + } + + #[test] + fn encodes_a_mainnet_liquidation_byte_for_byte() { + // liquidate_via_market_order data of a mainnet transaction at slot 429053028. + let mainnet = "fbf1b86c46467fc605d08ca0437ee7562b6648e76d5094af350b57c3820b6716fc96e777d449a9dd\ + 951c0000000000004d4000000000000000"; + let instruction = phoenix_instruction( + "LiquidateViaMarketOrder", + &liquidation_accounts(), + &serde_json::json!({ + "params": { + "assetMint": QUOTE_MINT, + "liquidationSize": { "inner": 7317 }, + "liquidationPrice": { "inner": 16461 }, + "fillOrKill": false, + } + }), ) .unwrap(); - let mut scenario = Scenario::new("permission".to_string(), "permission".to_string()); - scenario.add_override( - OverrideInstance::new( - "phoenix-permission-limits".to_string(), - 0, - AccountAddress::Pubkey(permission.to_string()), - ) - .with_values(HashMap::from([( - "expiresAtTimestamp".to_string(), - serde_json::json!(1), - )])), + let expected: Vec = (0..mainnet.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&mainnet[i..i + 2], 16).unwrap()) + .collect(); + assert_eq!(instruction.data, expected); + assert_eq!(instruction.accounts.len(), 11); + let liquidator = &instruction.accounts[3]; + assert_eq!( + ( + liquidator.pubkey, + liquidator.is_signer, + liquidator.is_writable + ), + (LIQUIDATOR, true, false) ); - svm.register_scenario(scenario, Some(100)).unwrap(); + assert!(instruction.accounts[4].is_writable); + } - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); + #[test] + fn capability_toggles_keep_a_capability_left_empty() { + let values = HashMap::from([ + ("PlaceLimitOrder".to_string(), serde_json::json!("")), + ("WithdrawCollateral".to_string(), serde_json::json!("false")), + ("DepositCollateral".to_string(), serde_json::json!(true)), + ]); + assert_eq!( + capability_toggles(&values).unwrap(), + serde_json::json!({ "params": { "toggles": [ + { "target": { "DepositCollateral": null }, "enable": true }, + { "target": { "WithdrawCollateral": null }, "enable": false }, + ] } }) + ); + let empty = HashMap::from([("PlaceLimitOrder".to_string(), serde_json::json!(""))]); + let error = capability_toggles(&empty).unwrap_err().to_string(); + assert!(error.contains("set at least one"), "{error}"); + let bad = HashMap::from([("PlaceLimitOrder".to_string(), serde_json::json!("yes"))]); + assert!(capability_toggles(&bad).is_err()); + } + + #[test] + fn refuses_missing_and_unknown_accounts_and_arguments() { + let args = serde_json::json!({ + "params": { + "assetMint": QUOTE_MINT, + "liquidationSize": { "inner": 1 }, + "liquidationPrice": { "inner": 1 }, + "fillOrKill": false, + } + }); + let mut missing = liquidation_accounts(); + missing.retain(|(name, _)| *name != "orderbook"); + assert!(phoenix_instruction("LiquidateViaMarketOrder", &missing, &args).is_err()); - let expired = svm.get_account(&permission).unwrap().unwrap(); - assert_eq!(expired.data[EXPIRES_AT_BYTE_RANGE], 1_i64.to_le_bytes()); - let map = svm.get_account(&PHOENIX_PERP_ASSET_MAP).unwrap().unwrap(); - assert_eq!(oracle_thresholds(&map.data, "SOL"), RAISED_THRESHOLDS); + let mut unknown = liquidation_accounts(); + unknown.push(("orderBook", Pubkey::new_unique())); + assert!(phoenix_instruction("LiquidateViaMarketOrder", &unknown, &args).is_err()); + + assert!( + phoenix_instruction( + "LiquidateViaMarketOrder", + &liquidation_accounts(), + &serde_json::json!({}) + ) + .is_err() + ); + assert!(phoenix_instruction("NoSuchInstruction", &[], &args).is_err()); } - #[tokio::test] - async fn an_override_still_applies_when_the_map_cannot_be_kept() { - let trader = Pubkey::new_unique(); - let funded = trader_account_for(trader, 500); - // No local map and no upstream datasource to fetch one from. + fn funded_svm(owner: &Pubkey) -> SurfnetSvm { let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); - svm.set_account(&trader, funded.clone()).unwrap(); - let scenario = build_phoenix_collateral_scenario(trader, &funded, "100").unwrap(); - svm.register_scenario(scenario, Some(100)).unwrap(); + svm.inner.airdrop(owner, 5_000_000_000).unwrap(); + svm + } - svm.materialize_overrides_for_slot(&None, 100) - .await - .unwrap(); + #[test] + fn returns_what_the_instructions_wrote_and_leaves_the_local_vm_alone() { + // Neither key is ever available here, yet the transfer runs as its owner. + let owner = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + let svm = funded_svm(&owner); + let before = svm.inner.get_account(&owner).unwrap().unwrap(); + + let written = run_instructions( + &svm, + &[system_instruction::transfer(&owner, &recipient, 1_000_000)], + ) + .unwrap(); + + let lamports: Vec<(Pubkey, u64)> = written + .iter() + .map(|(pubkey, account)| (*pubkey, account.lamports)) + .collect(); + assert_eq!( + lamports, + vec![(owner, before.lamports - 1_000_000), (recipient, 1_000_000)] + ); + assert_eq!(svm.inner.get_account(&owner).unwrap().unwrap(), before); + assert!(svm.inner.get_account(&recipient).unwrap().is_none()); + } - let stressed = svm.get_account(&trader).unwrap().unwrap(); - assert_eq!(stressed.data[COLLATERAL_BYTE_RANGE], 100_i64.to_le_bytes()); - assert_eq!(svm.get_account(&PHOENIX_PERP_ASSET_MAP).unwrap(), None); + #[test] + fn a_failing_instruction_fails_the_run_with_its_logs() { + let owner = Pubkey::new_unique(); + let svm = funded_svm(&owner); + let error = run_instructions( + &svm, + &[ + system_instruction::transfer(&owner, &Pubkey::new_unique(), 1_000_000), + system_instruction::transfer(&owner, &Pubkey::new_unique(), u64::MAX), + ], + ) + .unwrap_err() + .to_string(); + assert!(error.contains("instruction 1 failed"), "{error}"); + assert!(error.contains("logs"), "{error}"); } } diff --git a/crates/core/src/scenarios/protocols/phoenix-eternal/v1/trader.rs b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/trader.rs new file mode 100644 index 000000000..f5752f0a5 --- /dev/null +++ b/crates/core/src/scenarios/protocols/phoenix-eternal/v1/trader.rs @@ -0,0 +1,950 @@ +use std::{collections::HashSet, ops::Range}; + +use bytemuck::{Pod, Zeroable}; +use log::warn; +use phoenix_rise_accounts::{ + PhoenixAccount, + multi_arena::MultiArenaHeader, + trader::{TRADER_CAPABILITY_HOT, TraderHeader, TraderState}, +}; +use solana_account::Account; +use solana_clock::Clock; +use solana_commitment_config::CommitmentConfig; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +use super::{ + market::{Market, MarketContext, explain_move_failure, phoenix_markets}, + state_builder::{ + Exchange, PHOENIX_GLOBAL_CONFIG, PHOENIX_PROGRAM_ID, Sandbox, hydrate, keep_markets_usable, + local_account, log_authority, phoenix_instruction, + }, +}; +use crate::{ + error::{SurfpoolError, SurfpoolResult}, + surfnet::{ + remote::SurfnetRemoteClient, + svm::{SurfnetSvm, SurfnetSvmConfig}, + }, +}; + +pub const HAWKEYE_PROGRAM_ID: Pubkey = + Pubkey::from_str_const("RiSeVw3ZjNfsaXPRb4mgaqYaEEt41pNNJoDvVh7pgQj"); +pub(crate) const VIEW_MARGIN: [u8; 8] = [0xb2, 0x0a, 0x7c, 0xad, 0xec, 0xd2, 0x75, 0x06]; +const MARGIN_VIEW_MAGIC: u64 = 0x955f5b9d3dff253f; +const VIEW_MARGIN_FOR_ASSET: [u8; 8] = [0x20, 0x12, 0xaf, 0xfe, 0xa1, 0xed, 0x4d, 0x9b]; +const ASSET_VIEW_MAGIC: u64 = 0xdb58fff8285e1db9; + +/// What Hawkeye's `view_margin` returns for one trader. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, Pod, Zeroable)] +pub struct MarginView { + magic: u64, + pub version: u16, + pub position_count: u16, + pub risk_state: u8, + pub risk_tier: u8, + pub is_liquidatable: u8, + padding: u8, + pub collateral_quote_lots: i64, + pub effective_collateral_quote_lots: i64, + pub free_collateral_quote_lots: i64, + pub withdrawable_collateral_quote_lots: u64, + pub initial_margin_quote_lots: u64, + pub maintenance_margin_quote_lots: u64, + pub cancel_margin_quote_lots: u64, + pub backstop_margin_quote_lots: u64, + pub high_risk_margin_quote_lots: u64, + pub unrealized_pnl_quote_lots: i64, + pub discounted_unrealized_pnl_quote_lots: i64, + pub unsettled_funding_quote_lots: i64, +} + +impl MarginView { + /// The view in `view_margin`'s return data. + pub(crate) fn from_return_data(data: &[u8]) -> SurfpoolResult { + read_view(data, "margin", MARGIN_VIEW_MAGIC) + } +} + +/// A Hawkeye view in its return data, which starts with the view's `magic`. Hawkeye is copied +/// from mainnet, where an upgrade may append fields, so bytes past the view are ignored. +fn read_view(data: &[u8], view: &str, magic: u64) -> SurfpoolResult { + let len = size_of::(); + let read = data + .get(..len) + .map(bytemuck::pod_read_unaligned) + .ok_or_else(|| { + SurfpoolError::internal(format!( + "Hawkeye's {view} view returned {} bytes, fewer than its {len}", + data.len() + )) + })?; + let found: u64 = bytemuck::pod_read_unaligned(&data[..8]); + if found != magic { + return Err(SurfpoolError::internal(format!( + "Hawkeye's {view} view returned magic {found:#x}" + ))); + } + Ok(read) +} + +/// What Hawkeye's `view_margin_for_asset` returns for one trader in one market. +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct AssetView { + magic: u64, + pub asset_id: u32, + pub version: u16, + pub has_position_or_orders: u8, + padding: u8, + pub risk_state: u8, + pub risk_tier: u8, + padding1: [u8; 6], + /// Positive for a long, negative for a short. + pub base_lots: i64, + pub virtual_quote_lots: i64, + pub mark_price_ticks: u64, + pub entry_price_quote_lots_per_base_lot: u64, + pub position_value_quote_lots: i64, + pub unrealized_pnl_quote_lots: i64, + pub discounted_unrealized_pnl_quote_lots: i64, + pub unsettled_funding_quote_lots: i64, + pub initial_margin_quote_lots: u64, + pub maintenance_margin_quote_lots: u64, + pub cancel_margin_quote_lots: u64, + pub backstop_margin_quote_lots: u64, + pub high_risk_margin_quote_lots: u64, +} + +impl AssetView { + /// The view in `view_margin_for_asset`'s return data. + pub fn from_return_data(data: &[u8]) -> SurfpoolResult { + read_view(data, "asset", ASSET_VIEW_MAGIC) + } +} + +pub(crate) const VIEW_BBO: [u8; 8] = [0x37, 0x5f, 0x23, 0x2d, 0x53, 0xaf, 0x12, 0x52]; +const BBO_VIEW_MAGIC: u64 = 0xefca1fa31fa74171; + +/// What Hawkeye's BBO view returns for one market, its makers' splines included. +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct BboView { + magic: u64, + pub version: u16, + pub flags: u8, + padding: [u8; 5], + pub best_bid_ticks: u64, + pub best_ask_ticks: u64, + pub mark_price_ticks: u64, + pub index_price_ticks: u64, + pub mark_price_last_updated_slot: u64, + pub index_price_last_updated_slot: u64, +} + +impl BboView { + /// The view in the BBO view's return data. + pub fn from_return_data(data: &[u8]) -> SurfpoolResult { + read_view(data, "BBO", BBO_VIEW_MAGIC) + } + + /// Whether a bid still rests above an ask. A side reported as 0 has no price to cross. + pub fn crossed(&self) -> bool { + self.best_bid_ticks != 0 + && self.best_ask_ticks != 0 + && self.best_bid_ticks > self.best_ask_ticks + } +} + +/// One market a trader has a position or resting orders in, as Hawkeye reports it. +#[derive(Clone)] +pub struct Holding { + pub market: Market, + /// Positive for a long, negative for a short, 0 for resting orders alone. + pub base_lots: i64, + pub maintenance_margin_quote_lots: u64, +} + +/// Every listed market where Hawkeye reports a position or resting orders of `trader`. Hawkeye +/// reads positions where Phoenix keeps them, and the Trader account's own copy can miss markets. +pub fn holdings( + svm: &SurfnetSvm, + exchange: &Exchange, + trader: &Pubkey, +) -> SurfpoolResult> { + let map = local_account(svm, &exchange.perp_asset_map)?; + let mut views = Sandbox::new(svm); + let mut held = Vec::new(); + for market in phoenix_markets(exchange.perp_asset_map, &map)? { + let data = views.view(&asset_view(exchange, trader, market.asset_id)?)?; + let view = AssetView::from_return_data(&data)?; + if view.has_position_or_orders != 0 { + held.push(Holding { + market, + base_lots: view.base_lots, + maintenance_margin_quote_lots: view.maintenance_margin_quote_lots, + }); + } + } + Ok(held) +} + +/// The trader canceling its resting orders on every market in `held`: Phoenix refuses to +/// liquidate a trader with risk-increasing orders on any market. +async fn cancel_all( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + exchange: &Exchange, + trader: Pubkey, + authority: Pubkey, + held: &[Holding], +) -> SurfpoolResult> { + let books: Vec = held + .iter() + .flat_map(|held| [held.market.orderbook, held.market.splines]) + .collect(); + hydrate(svm, remote_ctx, &books).await?; + held.iter() + .map(|held| { + phoenix_instruction( + "CancelAll", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("traderWallet", authority), + ("traderAccount", trader), + ("perpAssetMap", exchange.perp_asset_map), + ("globalTraderIndex", exchange.global_trader_index), + ("activeTraderBuffer", exchange.active_trader_buffer), + ("orderbook", held.market.orderbook), + ("splines", held.market.splines), + ], + &serde_json::json!({}), + ) + }) + .collect() +} + +/// Hawkeye's margin view of `trader`, read inside the sandbox. +pub fn margin( + sandbox: &mut Sandbox, + exchange: &Exchange, + trader: &Pubkey, +) -> SurfpoolResult { + let data = sandbox.view(&hawkeye_view(exchange, &[*trader], VIEW_MARGIN.to_vec()))?; + MarginView::from_return_data(&data) +} + +/// Hawkeye's BBO view of `market`, read inside the sandbox. +pub fn bbo(sandbox: &mut Sandbox, exchange: &Exchange, market: &Market) -> SurfpoolResult { + let books = [market.orderbook, market.splines]; + BboView::from_return_data(&sandbox.view(&hawkeye_view(exchange, &books, VIEW_BBO.to_vec()))?) +} + +/// Hawkeye's `view_margin_for_asset` of `trader` in the market with `asset_id`. +pub fn asset_view( + exchange: &Exchange, + trader: &Pubkey, + asset_id: u64, +) -> SurfpoolResult { + let asset_id = u32::try_from(asset_id).map_err(|_| { + SurfpoolError::internal(format!("asset id {asset_id} does not fit Hawkeye's u32")) + })?; + let mut data = VIEW_MARGIN_FOR_ASSET.to_vec(); + data.extend(asset_id.to_le_bytes()); + data.extend([0; 4]); + Ok(hawkeye_view(exchange, &[*trader], data)) +} + +/// A Hawkeye view over the exchange accounts and `extra`; `data` is the view's discriminator and +/// its parameters. +fn hawkeye_view(exchange: &Exchange, extra: &[Pubkey], data: Vec) -> Instruction { + let accounts = [ + PHOENIX_PROGRAM_ID, + PHOENIX_GLOBAL_CONFIG, + exchange.global_trader_index, + exchange.active_trader_buffer, + exchange.perp_asset_map, + ] + .iter() + .chain(extra) + .map(|address| AccountMeta::new_readonly(*address, false)) + .collect(); + Instruction { + program_id: HAWKEYE_PROGRAM_ID, + accounts, + data, + } +} + +/// One position of a trader, as a surfnet reports it. +pub struct TraderPosition { + pub symbol: String, + pub orderbook: Pubkey, + /// Positive for a long, negative for a short. + pub base_lots: i64, + pub maintenance_margin_quote_lots: u64, +} + +/// One trader as a surfnet reports it. +pub struct TraderPositions { + pub positions: Vec, + /// Whether the local GlobalTraderIndex lists the trader. + pub hot: bool, + /// Hawkeye's view as `phoenix-trader-cancel-orders` leaves the trader, a hot trader's orders + /// canceled. + pub margin: MarginView, + /// The same view once the given markets move and uncross the way `phoenix-market-move` and + /// `phoenix-market-uncross` play them; `None` without moves. + pub margin_after_moves: Option, + /// Why `phoenix-trader-cancel-orders` or `phoenix-trader-collateral` would refuse the trader. + pub refusal: Option, +} + +/// Every listed market `trader` holds a position in, as Hawkeye reports it on a copy of the +/// surfnet `client` points at, which loads any account it is missing; nothing is written back. +/// `moves` are markets and the ticks they move to, for the margin after them. +pub async fn trader_positions( + client: SurfnetRemoteClient, + trader: Pubkey, + moves: &[(String, u64)], +) -> SurfpoolResult { + let (mut svm, _events, _geyser) = SurfnetSvm::new(SurfnetSvmConfig::default())?; + let commitment = CommitmentConfig::confirmed(); + let clock = client + .get_account(&solana_sysvar::clock::ID, commitment) + .await? + .map_account()?; + let clock: Clock = bincode::deserialize(&clock.data) + .map_err(|e| SurfpoolError::internal(format!("the surfnet's Clock is invalid: {e}")))?; + svm.inner.set_sysvar(&clock); + svm.inner.set_sysvar(&client.get_last_restart_slot().await?); + trader_positions_in(&mut svm, &Some((client, commitment)), trader, moves).await +} + +/// [`trader_positions`] computed in `svm`, which also takes the markets' upkeep and every account +/// it loads. +pub(crate) async fn trader_positions_in( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + trader: Pubkey, + moves: &[(String, u64)], +) -> SurfpoolResult { + hydrate(svm, remote_ctx, &[trader]).await?; + let account = svm.inner.get_account(&trader)?.ok_or_else(|| { + SurfpoolError::internal(format!("there is no Phoenix Trader at {trader}")) + })?; + if account.owner != PHOENIX_PROGRAM_ID { + return Err(SurfpoolError::invalid_account_owner( + trader, + Some("Expected a Phoenix Trader"), + )); + } + let slot = svm.inner.get_sysvar::().slot; + keep_markets_usable(svm, remote_ctx, slot).await; + let symbols: Vec<&str> = moves.iter().map(|(symbol, _)| symbol.as_str()).collect(); + let mut moved = MarketContext::load_all(svm, remote_ctx, &symbols).await?; + let (mut lever, still_listed) = LeverState::load(svm, remote_ctx, trader, &[]).await?; + let (margin_now, quote_lot_collateral) = lever.reading(trader)?; + let refusal = still_listed + .or_else(|| { + let left = lever.state.account(&trader).ok()?; + with_collateral(trader, &left, quote_lot_collateral).err() + }) + .map(|refusal| refusal.to_string()); + let LeverState { + exchange, + hot, + held, + mut state, + } = lever; + let margin_after_moves = if moves.is_empty() { + None + } else { + // A cancel freezes the mark for risk in its slot, so the moves play a slot later, each + // followed by its uncross, the way the scenarios play them after the cancel. + state.next_slot(); + for (context, (_, ticks)) in moved.iter_mut().zip(moves) { + context.move_in(&mut state, *ticks)?; + context + .uncross_in(&mut state) + .map_err(explain_move_failure)?; + } + Some(margin(&mut state, &exchange, &trader)?) + }; + Ok(TraderPositions { + positions: held + .into_iter() + .filter(|held| held.base_lots != 0) + .map(|held| TraderPosition { + symbol: held.market.symbol, + orderbook: held.market.orderbook, + base_lots: held.base_lots, + maintenance_margin_quote_lots: held.maintenance_margin_quote_lots, + }) + .collect(), + hot, + margin: margin_now, + margin_after_moves, + refusal, + }) +} + +/// The state `phoenix-trader-cancel-orders` leaves, which the collateral is worked out from: a hot +/// trader's orders canceled, a cold trader left as it is, since a cancel changes its margin too. +pub(crate) struct LeverState<'a> { + pub(crate) exchange: Exchange, + pub(crate) hot: bool, + held: Vec, + pub(crate) state: Sandbox<'a>, +} + +impl<'a> LeverState<'a> { + /// The state, with the refusal when Phoenix still lists the trader after the cancel. With + /// `markets`, only their orders are canceled and a trader still listed is expected; without, + /// all markets' are. + pub(crate) async fn load( + svm: &'a mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + trader: Pubkey, + markets: &[&str], + ) -> SurfpoolResult<(LeverState<'a>, Option)> { + let account = local_account(svm, &trader)?; + let header = TraderHeader::try_read_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data(trader, "Expected a Phoenix Trader", Some(e)) + })?; + let exchange = Exchange::load(svm, remote_ctx).await?; + hydrate( + svm, + remote_ctx, + &[ + HAWKEYE_PROGRAM_ID, + exchange.perp_asset_map, + exchange.active_trader_buffer, + ], + ) + .await?; + let hot = indexed(&local_account(svm, &exchange.global_trader_index)?, &trader)?; + refuse_unindexed_hot_trader(trader, &header, hot)?; + let held = holdings(svm, &exchange, &trader)?; + let canceled = if markets.is_empty() { + held.clone() + } else { + let map = local_account(svm, &exchange.perp_asset_map)?; + let symbols = markets + .iter() + .map(|symbol| Ok(Market::find(&exchange.perp_asset_map, &map.data, symbol)?.symbol)) + .collect::>>()?; + held.iter() + .filter(|held| symbols.contains(&held.market.symbol)) + .cloned() + .collect() + }; + let cancels = if hot { + let authority = Pubkey::new_from_array(header.authority); + cancel_all(svm, remote_ctx, &exchange, trader, authority, &canceled).await? + } else { + Vec::new() + }; + let mut state = Sandbox::new(svm); + if !cancels.is_empty() { + state.run(&cancels)?; + } + let still_listed = markets.is_empty() + && hot + && indexed(&state.account(&exchange.global_trader_index)?, &trader)?; + let refusal = still_listed.then(|| { + let why = if cancels.is_empty() { + "has no orders whose cancel would take it out" + } else { + "stays there after its orders are canceled" + }; + SurfpoolError::internal(format!( + "{trader} is in Phoenix's active trader index and {why}" + )) + }); + Ok(( + LeverState { + exchange, + hot, + held, + state, + }, + refusal, + )) + } + + /// Hawkeye's margin of `trader` in this state, and its collateral field. + pub(crate) fn reading(&mut self, trader: Pubkey) -> SurfpoolResult<(MarginView, i64)> { + let view = margin(&mut self.state, &self.exchange, &trader)?; + let account = self.state.account(&trader)?; + let header = TraderHeader::try_read_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data(trader, "Expected a Phoenix Trader", Some(e)) + })?; + Ok(( + view, + bytemuck::cast(header.trader_state.quote_lot_collateral), + )) + } +} + +/// The writes of `trader` canceling all its resting orders on `markets`, or on every market when +/// there are none, which takes a hot trader out of Phoenix's index; a cold trader has none and +/// nothing is written. A trader Phoenix still lists after a cancel on every market is refused. +pub async fn cancel_order_writes( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + trader: Pubkey, + markets: &[&str], +) -> SurfpoolResult> { + // The template's own account is the PerpAssetMap, so the trader may not be local yet. + hydrate(svm, remote_ctx, &[trader]).await?; + let (lever, refusal) = LeverState::load(svm, remote_ctx, trader, markets).await?; + if let Some(refusal) = refusal { + return Err(refusal); + } + lever.state.writes() +} + +/// The writes that bring `trader`'s effective collateral to `wanted` quote lots from Hawkeye's view +/// when it plays. Effective collateral moves one for one with the collateral field, which is +/// written where Phoenix reads it: a hot trader's record in the GlobalTraderIndex, which Phoenix +/// copies back to the Trader account when the trader leaves the index, or a cold trader's account. +/// A spline market maker is refused. +pub async fn collateral_writes( + svm: &mut SurfnetSvm, + remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + trader: Pubkey, + account: &Account, + wanted: i64, +) -> SurfpoolResult> { + let header = TraderHeader::try_read_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data(trader, "Expected a Phoenix Trader", Some(e)) + })?; + let exchange = Exchange::load(svm, remote_ctx).await?; + hydrate( + svm, + remote_ctx, + &[ + HAWKEYE_PROGRAM_ID, + exchange.perp_asset_map, + exchange.active_trader_buffer, + ], + ) + .await?; + let index = local_account(svm, &exchange.global_trader_index)?; + let record = index_record(&index, &trader)?; + refuse_unindexed_hot_trader(trader, &header, record.is_some())?; + let view = margin(&mut Sandbox::new(svm), &exchange, &trader)?; + let field: i64 = bytemuck::cast(match &record { + Some(range) => { + bytemuck::pod_read_unaligned::(&index.data[range.clone()]) + .quote_lot_collateral + } + None => header.trader_state.quote_lot_collateral, + }); + let quote_lots = wanted + .checked_sub(view.effective_collateral_quote_lots) + .and_then(|gap| field.checked_add(gap)) + .ok_or_else(|| { + SurfpoolError::internal(format!( + "an effective collateral of {wanted} quote lots does not fit {trader}'s collateral" + )) + })?; + if wanted < view.high_risk_margin_quote_lots as i64 { + warn!( + "{trader}: an effective collateral of {wanted} quote lots is below its high-risk margin \ + of {}; a keeper's liquidation there can leave it underwater and fail, and below 0 \ + Phoenix refuses it", + view.high_risk_margin_quote_lots + ); + } + let left = with_collateral(trader, account, quote_lots)?; + let Some(range) = record else { + return Ok(vec![(trader, left)]); + }; + let mut data = index.data.clone(); + let mut state: TraderState = bytemuck::pod_read_unaligned(&data[range.clone()]); + state.quote_lot_collateral = bytemuck::cast(quote_lots); + data[range].copy_from_slice(bytemuck::bytes_of(&state)); + let index_write = (exchange.global_trader_index, Account { data, ..index }); + if header.trader_state.is_hot() { + return Ok(vec![index_write]); + } + // The local index was copied before the trader left it upstream, its account after. Phoenix + // reads that account while its HOT flag is clear and copies the stale record over it on a + // cancel, so the account goes back to hot, matching the index. + let mut hot = header; + hot.trader_state.quote_lot_collateral = bytemuck::cast(quote_lots); + hot.trader_state.flags |= TRADER_CAPABILITY_HOT; + let mut repaired = left; + repaired.data[..size_of::()].copy_from_slice(bytemuck::bytes_of(&hot)); + Ok(vec![index_write, (trader, repaired)]) +} + +/// The Trader `account` with its collateral field set to `quote_lots`, or the refusal of a spline +/// market maker. +pub(crate) fn with_collateral( + trader: Pubkey, + account: &Account, + quote_lots: i64, +) -> SurfpoolResult { + let mut header = TraderHeader::try_read_from_account_bytes(&account.data).map_err(|e| { + SurfpoolError::invalid_account_data(trader, "Expected a Phoenix Trader", Some(e)) + })?; + if header.num_markets_with_splines > 0 { + return Err(SurfpoolError::internal(format!( + "{trader} quotes splines on {} markets; liquidating a spline market maker is not \ + supported", + header.num_markets_with_splines + ))); + } + header.trader_state.quote_lot_collateral = bytemuck::cast(quote_lots); + let mut data = account.data.clone(); + data[..size_of::()].copy_from_slice(bytemuck::bytes_of(&header)); + Ok(Account { + data, + ..account.clone() + }) +} + +/// Phoenix looks a hot trader up in the index, and fails its cancels and liquidations with +/// TraderNotFound when it is missing there. +fn refuse_unindexed_hot_trader( + trader: Pubkey, + header: &TraderHeader, + listed: bool, +) -> SurfpoolResult<()> { + if header.trader_state.is_hot() && !listed { + return Err(SurfpoolError::internal(format!( + "{trader} is marked hot, but the local GlobalTraderIndex does not list it, so Phoenix \ + can neither cancel its orders nor liquidate it; the two accounts were fetched at \ + different times. Restart the surfnet (or reset the Phoenix accounts) and play again" + ))); + } + Ok(()) +} + +/// Whether the GlobalTraderIndex `index` lists `trader`, so Phoenix keeps its state there. +fn indexed(index: &Account, trader: &Pubkey) -> SurfpoolResult { + Ok(index_record(index, trader)?.is_some()) +} + +/// The byte range of `trader`'s TraderState in the GlobalTraderIndex `index`, while it lists the +/// trader. +fn index_record(index: &Account, trader: &Pubkey) -> SurfpoolResult>> { + Ok(index_trader_state_ranges(index)? + .into_iter() + .find_map(|(listed, range)| (listed == *trader).then_some(range))) +} + +/// Every hot Trader the GlobalTraderIndex tree reaches, with its TraderState byte range. Freed +/// nodes keep stale keys and collateral, so the walk starts at the root; a repeated key fails. +pub fn index_trader_state_ranges(index: &Account) -> SurfpoolResult)>> { + let invalid = || SurfpoolError::internal("Invalid Phoenix GlobalTraderIndex tree"); + if index.owner != PHOENIX_PROGRAM_ID { + return Err(SurfpoolError::internal( + "Expected a Phoenix-owned GlobalTraderIndex account", + )); + } + let header = MultiArenaHeader::try_from_account_bytes( + "GlobalTraderIndex", + &index.data, + PhoenixAccount::GlobalTraderIndexHeader.discriminant(), + ) + .map_err(|error| { + SurfpoolError::internal(format!("Invalid Phoenix GlobalTraderIndex: {error}")) + })?; + if header.num_arenas() != 1 || header.superblock().num_active_arenas() != 1 { + return Err(SurfpoolError::internal( + "Phoenix trader lookups require a single-arena GlobalTraderIndex", + )); + } + // MultiArenaHeader (48), superblock (32), tree root and padding (16), then + // 1-based Sokoban nodes: four u32 registers, a 32-byte key, and IDL TraderState. + const NODES_START: usize = 96; + const NODE_LEN: usize = 64; + let data = &index.data; + if data.len() < NODES_START || !(data.len() - NODES_START).is_multiple_of(NODE_LEN) { + return Err(invalid()); + } + let capacity = (data.len() - NODES_START) / NODE_LEN; + let read_u32 = |offset| u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()); + let mut pending = vec![read_u32(80)]; + let mut visited = HashSet::new(); + let mut keys = HashSet::new(); + let mut ranges = Vec::new(); + while let Some(node) = pending.pop() { + if node == 0 { + continue; + } + if node >= header.superblock().bump_index() + || node as usize > capacity + || !visited.insert(node) + { + return Err(invalid()); + } + let start = NODES_START + (node as usize - 1) * NODE_LEN; + pending.extend([read_u32(start), read_u32(start + 4)]); + let key = Pubkey::new_from_array(data[start + 16..start + 48].try_into().unwrap()); + if !keys.insert(key) { + return Err(invalid()); + } + ranges.push((key, start + 48..start + 64)); + } + if visited.len() != header.superblock().size() as usize { + return Err(invalid()); + } + Ok(ranges) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + pub(crate) fn index_trader_state_range( + index: &Account, + trader_key: &[u8; 32], + ) -> SurfpoolResult> { + index_record(index, &Pubkey::new_from_array(*trader_key))?.ok_or_else(|| { + SurfpoolError::internal("Hot Phoenix Trader has no reachable GlobalTraderIndex entry") + }) + } + + #[test] + fn a_view_with_appended_fields_decodes_and_a_short_one_is_refused() { + let view = MarginView { + magic: MARGIN_VIEW_MAGIC, + collateral_quote_lots: 42, + ..MarginView::zeroed() + }; + let mut data = bytemuck::bytes_of(&view).to_vec(); + data.extend([7; 16]); + let read = MarginView::from_return_data(&data).unwrap(); + assert_eq!(read.collateral_quote_lots, 42); + + let short = &data[..size_of::() - 1]; + assert!(MarginView::from_return_data(short).is_err()); + assert!(AssetView::from_return_data(&data).is_err(), "wrong magic"); + } + + const FIRST_KEY: [u8; 32] = [11; 32]; + const SECOND_KEY: [u8; 32] = [22; 32]; + + fn write_u32(data: &mut [u8], offset: usize, value: u32) { + data[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); + } + + fn index_account() -> Account { + let mut data = vec![0; 96 + 3 * 64]; + data[..8].copy_from_slice(&PhoenixAccount::GlobalTraderIndexHeader.discriminant()); + write_u32(&mut data, 48, 2); + data[52..54].copy_from_slice(&1_u16.to_le_bytes()); + data[54..56].copy_from_slice(&1_u16.to_le_bytes()); + write_u32(&mut data, 56, 3); + write_u32(&mut data, 60, 4); + write_u32(&mut data, 64, 3); + write_u32(&mut data, 80, 2); + write_u32(&mut data, 160, 1); + write_u32(&mut data, 104, 2); + for (slot, key, collateral) in [ + (0, FIRST_KEY, 111_i64), + (1, SECOND_KEY, 222_i64), + (2, FIRST_KEY, 999_i64), + ] { + let start = 96 + slot * 64; + data[start + 16..start + 48].copy_from_slice(&key); + data[start + 48..start + 56].copy_from_slice(&collateral.to_le_bytes()); + write_u32(&mut data, start + 56, TRADER_CAPABILITY_HOT); + } + Account { + data, + owner: PHOENIX_PROGRAM_ID, + ..Account::default() + } + } + + #[test] + fn a_hot_trader_the_index_does_not_list_is_refused() { + let index = index_account(); + let mut header = TraderHeader::zeroed(); + header.trader_state.flags = TRADER_CAPABILITY_HOT; + let listed = Pubkey::new_from_array(FIRST_KEY); + let missing = Pubkey::new_unique(); + let in_index = |trader: &Pubkey| indexed(&index, trader).unwrap(); + assert!(refuse_unindexed_hot_trader(listed, &header, in_index(&listed)).is_ok()); + let error = refuse_unindexed_hot_trader(missing, &header, in_index(&missing)) + .unwrap_err() + .to_string(); + assert!( + error.contains(&format!( + "{missing} is marked hot, but the local GlobalTraderIndex does not list it" + )), + "{error}" + ); + header.trader_state.flags = 0; + assert!( + refuse_unindexed_hot_trader(missing, &header, in_index(&missing)).is_ok(), + "a cold trader is not looked up" + ); + } + + fn trader_account(header: TraderHeader) -> Account { + let mut data = bytemuck::bytes_of(&header).to_vec(); + // The position map follows the header and must stay as it is. + data.extend([7; 48]); + Account { + lamports: 1, + data, + owner: PHOENIX_PROGRAM_ID, + ..Account::default() + } + } + + fn cold_trader_header() -> TraderHeader { + let mut header = TraderHeader::zeroed(); + header.discriminant = u64::from_le_bytes(PhoenixAccount::Trader.discriminant()); + header.trader_state.quote_lot_collateral = bytemuck::cast(123_i64); + header + } + + #[test] + fn collateral_lands_where_the_idl_puts_trader_state_quote_lot_collateral() { + let trader = Pubkey::new_unique(); + let account = trader_account(cold_trader_header()); + let quote_lots = -5_030_916_850_i64; + + let written = with_collateral(trader, &account, quote_lots).unwrap(); + + // Discriminant (8), sequence number (16), key and authority (32 each), then TraderState, + // which opens with the collateral. + assert_eq!(written.data[88..96], quote_lots.to_le_bytes()); + let changed: Vec = (0..account.data.len()) + .filter(|index| written.data[*index] != account.data[*index]) + .collect(); + assert!( + changed.iter().all(|index| (88..96).contains(index)), + "{changed:?}" + ); + assert_eq!( + (written.lamports, written.owner), + (account.lamports, account.owner) + ); + + let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let through_idl = svm + .get_forged_account_data( + &trader, + &account.data, + super::super::state_builder::phoenix_idl(), + &std::collections::HashMap::from([( + "traderState.quoteLotCollateral".to_string(), + serde_json::json!(quote_lots), + )]), + ) + .unwrap(); + assert_eq!(written.data, through_idl); + } + + #[test] + fn the_idl_writes_max_positions_and_leaves_the_trader_preference_bits() { + let mut header = cold_trader_header(); + header.max_positions = 128; + header.trader_preference_bits = 5; + let account = trader_account(header); + + let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let written = svm + .get_forged_account_data( + &Pubkey::new_unique(), + &account.data, + super::super::state_builder::phoenix_idl(), + &std::collections::HashMap::from([( + "maxPositions".to_string(), + serde_json::json!(10), + )]), + ) + .unwrap(); + let header: TraderHeader = + bytemuck::pod_read_unaligned(&written[..size_of::()]); + assert_eq!( + (header.max_positions, header.trader_preference_bits), + (10, 5) + ); + } + + #[test] + fn collateral_refuses_a_spline_market_maker() { + let mut maker = cold_trader_header(); + maker.num_markets_with_splines = 3; + let trader = Pubkey::new_unique(); + let error = with_collateral(trader, &trader_account(maker), 1) + .unwrap_err() + .to_string(); + assert!( + error.contains(&format!( + "{trader} quotes splines on 3 markets; liquidating a spline market maker is not \ + supported" + )), + "{error}" + ); + } + + #[test] + fn index_lookup_selects_reachable_keys_and_rejects_malformed_trees() { + let index = index_account(); + assert_eq!( + index_trader_state_range(&index, &FIRST_KEY).unwrap(), + 144..160 + ); + assert_eq!( + index_trader_state_range(&index, &SECOND_KEY).unwrap(), + 208..224 + ); + assert!(index_trader_state_range(&index, &[44; 32]).is_err()); + + type Corrupt = fn(&mut Account); + let corruptions: [(&str, Corrupt); 8] = [ + ("only a freed duplicate matches", |index| { + index.data[112..144].copy_from_slice(&[33; 32]) + }), + ("cycle", |index| write_u32(&mut index.data, 96, 2)), + ("size disagrees with the reachable nodes", |index| { + write_u32(&mut index.data, 48, 3) + }), + ("child beyond capacity", |index| { + write_u32(&mut index.data, 60, 100); + write_u32(&mut index.data, 164, 4); + }), + ("child above the bump index", |index| { + write_u32(&mut index.data, 60, 2) + }), + ("duplicate reachable key", |index| { + index.data[176..208].copy_from_slice(&FIRST_KEY) + }), + ("wrong owner", |index| index.owner = Pubkey::new_unique()), + ("wrong discriminator", |index| index.data[..8].fill(0)), + ]; + for (case, corrupt) in corruptions { + let mut index = index_account(); + corrupt(&mut index); + assert!( + index_trader_state_range(&index, &FIRST_KEY).is_err(), + "{case}" + ); + } + for (arenas, active) in [(0_u16, 1_u16), (2, 1), (1, 0), (1, 2)] { + let mut index = index_account(); + index.data[52..54].copy_from_slice(&arenas.to_le_bytes()); + index.data[54..56].copy_from_slice(&active.to_le_bytes()); + assert!(index_trader_state_range(&index, &FIRST_KEY).is_err()); + } + for len in [0, 79, 95, 96 + 3 * 64 - 1] { + let mut index = index_account(); + index.data.truncate(len); + assert!(index_trader_state_range(&index, &FIRST_KEY).is_err()); + } + } +} diff --git a/crates/core/src/scenarios/registry.rs b/crates/core/src/scenarios/registry.rs index edef806d0..4e2088488 100644 --- a/crates/core/src/scenarios/registry.rs +++ b/crates/core/src/scenarios/registry.rs @@ -606,11 +606,11 @@ mod tests { // Pyth (1) + Jupiter (1) + Raydium CLMM (1) + Raydium AMM v4 (4) + Drift (4) + Meteora (2) // + Kamino (Lend 17, Scope 3, Farms 5, Swap 2, Vault 5, Liquidity 4 = 36) - // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + Phoenix Eternal (8) = 70 + // + Whirlpool (6) + SPL Token (2) + Pump (2) + PumpSwap (3) + Phoenix Eternal (19) = 81 assert_eq!( registry.count(), - 70, - "Registry should load 70 templates total" + 81, + "Registry should load 81 templates total" ); assert!(registry.contains("pyth-price-feed-v2")); @@ -682,8 +682,9 @@ mod tests { assert!(registry.contains("pump-amm-canonical-pool")); assert!(registry.contains("pump-amm-global-config")); - assert!(registry.contains("phoenix-trader-collateral-stress")); - assert!(registry.contains("phoenix-direct-mark-risk-shock")); + assert!(registry.contains("phoenix-market-move")); + assert!(registry.contains("phoenix-oracle-price")); + assert!(registry.contains("phoenix-trader-collateral")); } #[test] @@ -1013,8 +1014,8 @@ templates: [] let phoenix_templates = registry.by_protocol("Phoenix Eternal"); assert_eq!( phoenix_templates.len(), - 8, - "Should have 8 Phoenix Eternal templates" + 19, + "Should have 19 Phoenix Eternal templates" ); } diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 5b9f718f6..9ead7dc02 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -104,7 +104,10 @@ use crate::{ rpc::utils::convert_transaction_metadata_from_canonical, scenarios::{ TemplateRegistry, account_data_values, - protocols::phoenix_eternal::v1::state_builder::prepare_phoenix_override, template_registry, + protocols::phoenix_eternal::v1::state_builder::{ + prepare_phoenix_override, write_all_or_none, + }, + template_registry, }, storage::{OverlayStorage, Storage, StorageBackend}, surfnet::{ @@ -310,7 +313,7 @@ fn json_integer_digits(json: &serde_json::Value, target: &str) -> SurfpoolResult } /// Converts JSON into a txtx [`Value`] using the expected IDL type -fn json_to_txtx_value_for_idl_type( +pub(crate) fn json_to_txtx_value_for_idl_type( json: &serde_json::Value, idl_type: &IdlType, idl_types: &[IdlTypeDef], @@ -3564,28 +3567,22 @@ impl SurfnetSvm { match prepare_phoenix_override( self, + &override_instance.template_id, &account_pubkey, &account, &account_values, remote_ctx, - // Only an account core has just refetched counts as fresh. - fetch_from_upstream && settled_this_slot.contains(&account_pubkey), target_slot, ) .await { Ok(Some(writes)) => { - for (pubkey, written) in writes { - if let Err(e) = - self.set_scenario_override_account(&pubkey, written, target_slot) - { - warn!( - "Failed to set {} for override {}: {}", - pubkey, override_instance.id, e - ); - break; - } - settled_this_slot.insert(pubkey); + match write_all_or_none(self, writes, target_slot) { + Ok(written) => settled_this_slot.extend(written), + Err(e) => warn!( + "Skipping override {} for {}: {}", + override_instance.id, account_pubkey, e + ), } continue; } diff --git a/crates/core/src/tests/phoenix/mod.rs b/crates/core/src/tests/phoenix/mod.rs index cda211b64..08cf16b25 100644 --- a/crates/core/src/tests/phoenix/mod.rs +++ b/crates/core/src/tests/phoenix/mod.rs @@ -1,11 +1,8 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; -use bytemuck::{Pod, Zeroable}; use phoenix_rise_accounts::{ - global_config::GlobalConfig, - pda::derive_spline_collection_address, - perp_asset_map::PerpAssetMap, - trader::{TRADER_CAPABILITY_HOT, TraderHeader}, + global_config::GlobalConfig, orderbook::Orderbook, pda::derive_spline_collection_address, + perp_asset_map::PerpAssetMap, trader::TraderHeader, }; use solana_account::Account; use solana_clock::Clock; @@ -13,26 +10,35 @@ use solana_commitment_config::CommitmentConfig; use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_instruction::{AccountMeta, Instruction}; use solana_keypair::Keypair; +use solana_program_pack::Pack; use solana_pubkey::Pubkey; use solana_signer::Signer; +use solana_sysvar::last_restart_slot::LastRestartSlot; use solana_transaction::Transaction; +use spl_associated_token_account_interface::address::get_associated_token_address; use surfpool_types::DEFAULT_MAINNET_RPC_URL; use crate::{ + error::SurfpoolResult, scenarios::protocols::phoenix_eternal::v1::{ - collateral::{index_trader_state_range, index_trader_state_ranges, trader_header}, + market::{Market, MarketContext, move_market, phoenix_markets, uncross_market}, state_builder::{ - PHOENIX_ETERNAL_PROGRAM_ID, PHOENIX_GLOBAL_TRADER_INDEX, PHOENIX_PERP_ASSET_MAP, - build_phoenix_collateral_scenario, phoenix_markets, + Exchange, PHOENIX_GLOBAL_CONFIG, PHOENIX_PERP_ASSET_MAP, PHOENIX_PROGRAM_ID, + keep_markets_usable, local_account, log_authority, phoenix_instruction, + run_instructions, tests::template_addresses, + }, + trader::{ + BboView, HAWKEYE_PROGRAM_ID, MarginView, VIEW_BBO, VIEW_MARGIN, holdings, + index_trader_state_ranges, tests::index_trader_state_range, trader_positions, + trader_positions_in, }, }, surfnet::{locker::SurfnetSvmLocker, remote::SurfnetRemoteClient, svm::SurfnetSvm}, }; const RPC_URL_ENV: &str = "SURFPOOL_TEST_RPC_URL"; -const PHOENIX_GLOBAL_CONFIG: Pubkey = - Pubkey::from_str_const("2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ"); - +const LAST_RESTART_SLOT: Pubkey = + Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"); fn client() -> SurfnetRemoteClient { SurfnetRemoteClient::new( std::env::var(RPC_URL_ENV).unwrap_or_else(|_| DEFAULT_MAINNET_RPC_URL.to_string()), @@ -55,30 +61,6 @@ async fn fetch(addresses: &[Pubkey]) -> Vec { .collect() } -/// Hawkeye checks it, because it reads a hot Trader's collateral from the GlobalTraderIndex and -/// its positions from the ActiveTraderBuffer, and the Trader account's copies of both can lag. -/// A margin view that fails rejects the trader, and its error is returned. -fn trader_is_eligible( - locker: &SurfnetSvmLocker, - graph: &PhoenixMainnetGraph, -) -> Result { - let header = trader_header(&graph.trader, graph.account(&graph.trader)).unwrap_or_else(|e| { - panic!( - "{} is in the GlobalTraderIndex but is not a valid Trader: {e}", - graph.trader - ) - }); - // The collateral stress keys off the Trader account's hot flag. - if !header.trader_state.is_hot() { - return Ok(false); - } - let margin = try_hawkeye_margin(locker, graph)?; - Ok(margin.collateral_quote_lots > 0 - && margin.position_count > 0 - && margin.maintenance_margin_quote_lots > 0 - && margin.is_liquidatable == 0) -} - /// A zero-copy layout cannot be round-tripped against itself, so drift shows up as an /// invariant that stops holding on mainnet bytes. #[tokio::test(flavor = "multi_thread")] @@ -93,16 +75,26 @@ async fn mainnet_accounts_satisfy_the_typed_layout_invariants() { "GlobalConfig stores its own address, so a moved field shows up here first" ); assert_eq!( - ( - Pubkey::new_from_array(global.perp_asset_map_key()), - Pubkey::new_from_array(global.global_trader_index_header_key()), - ), - (PHOENIX_PERP_ASSET_MAP, PHOENIX_GLOBAL_TRADER_INDEX), - "the hardcoded Phoenix singletons moved; update them to what GlobalConfig points at" + Pubkey::new_from_array(global.perp_asset_map_key()), + PHOENIX_PERP_ASSET_MAP, + "the PerpAssetMap the templates carry moved; update it to what GlobalConfig points at" ); + for (id, account_type, address) in template_addresses() { + let named = match account_type { + "PerpAssetMap" => global.perp_asset_map_key(), + "GlobalConfiguration" => global.account_key(), + "WithdrawQueueHeader" => global.withdraw_queue_key(), + other => panic!("{id} carries an address for {other}, which nothing checks"), + }; + assert_eq!( + address, + Pubkey::new_from_array(named), + "{id} carries the address GlobalConfig names" + ); + } let map_account = graph.account(&graph.perp_asset_map); - assert_eq!(map_account.owner, PHOENIX_ETERNAL_PROGRAM_ID); + assert_eq!(map_account.owner, PHOENIX_PROGRAM_ID); let markets = phoenix_markets(graph.perp_asset_map, map_account) .expect("mainnet PerpAssetMap should decode"); let map = PerpAssetMap::try_from_account_bytes(&map_account.data) @@ -133,14 +125,12 @@ async fn mainnet_accounts_satisfy_the_typed_layout_invariants() { // program would no longer find. assert_eq!( graph.account(spline).owner, - PHOENIX_ETERNAL_PROGRAM_ID, + PHOENIX_PROGRAM_ID, "the derived spline collection must belong to the Eternal program" ); } } -const HAWKEYE_VIEW_MARGIN_DISCRIMINANT: [u8; 8] = [0xb2, 0x0a, 0x7c, 0xad, 0xec, 0xd2, 0x75, 0x06]; -const HAWKEYE_VIEW_BBO_DISCRIMINANT: [u8; 8] = [0x37, 0x5f, 0x23, 0x2d, 0x53, 0xaf, 0x12, 0x52]; const ETERNAL_PROGRAMDATA: Pubkey = Pubkey::from_str_const("B5ayDaz9HegiNZqYeBtcFqfZBVSGwjB2CJgHshoSfMQg"); const HAWKEYE_PROGRAMDATA: Pubkey = @@ -160,276 +150,12 @@ async fn deployed_program(programdata: Pubkey) -> Vec { bytes } -fn mainnet_graph_cache() -> &'static tokio::sync::Mutex>> -{ - static CACHE: std::sync::OnceLock< - tokio::sync::Mutex>>, - > = std::sync::OnceLock::new(); +fn mainnet_graph_cache() -> &'static tokio::sync::Mutex> { + static CACHE: std::sync::OnceLock>> = + std::sync::OnceLock::new(); CACHE.get_or_init(|| tokio::sync::Mutex::new(None)) } -#[tokio::test(flavor = "multi_thread")] -async fn phoenix_state_preparation_changes_hawkeye_risk_outcomes() { - let (collateral_locker, graph) = phoenix_behavior_locker().await; - - let account = |key: &Pubkey| { - collateral_locker - .with_svm_reader(|svm| svm.get_account(key)) - .unwrap() - .unwrap() - }; - let before_trader = account(&graph.trader); - let before_index = account(&graph.global_trader_index); - let header = TraderHeader::try_read_from_account_bytes(&before_trader.data).unwrap(); - assert!( - header.trader_state.is_hot(), - "the regression requires a hot trader" - ); - let range = index_trader_state_range(&before_index, &header.key).unwrap(); - let before = hawkeye_margin(&collateral_locker, &graph); - assert!( - before.collateral_quote_lots > 0, - "no eligible mainnet candidate: the discovered trader has no collateral to stress" - ); - assert!( - before.position_count > 0, - "no eligible mainnet candidate: the program sees no position for the discovered trader" - ); - assert!( - before.maintenance_margin_quote_lots > 0, - "no eligible mainnet candidate: the discovered trader's positions require no margin" - ); - assert_eq!( - before.is_liquidatable, 0, - "no eligible mainnet candidate: the discovered trader is already liquidatable" - ); - - // Effective collateral moves one-for-one with collateral, whatever else it counts (uPnL, - // funding, ...), so this target puts it at half the maintenance margin. A fixed target of 1 - // left about one mainnet trader in four healthy. - let maintenance = - i64::try_from(before.maintenance_margin_quote_lots).expect("maintenance margin fits i64"); - let target = - before.collateral_quote_lots - before.effective_collateral_quote_lots + maintenance / 2; - - let scenario = - build_phoenix_collateral_scenario(graph.trader, &before_trader, &target.to_string()) - .unwrap(); - collateral_locker - .register_scenario(scenario, Some(graph.clock.slot)) - .unwrap(); - assert_eq!(account(&graph.trader), before_trader); - assert_eq!(account(&graph.global_trader_index), before_index); - collateral_locker - .materialize_overrides_for_slot(&None, graph.clock.slot) - .await - .unwrap(); - let after_collateral = hawkeye_margin(&collateral_locker, &graph); - assert_eq!( - after_collateral.collateral_quote_lots, target, - "the program reads the collateral the preparation wrote" - ); - assert!( - after_collateral.effective_collateral_quote_lots < before.effective_collateral_quote_lots, - "stressing collateral must lower what the risk engine can count on" - ); - - assert_eq!( - after_collateral.is_liquidatable, - 1, - "the stress must leave the trader liquidatable: effective collateral {} against \ - maintenance margin {}", - after_collateral.effective_collateral_quote_lots, - after_collateral.maintenance_margin_quote_lots - ); - let mut expected_trader = before_trader; - expected_trader.data[88..96].copy_from_slice(&target.to_le_bytes()); - let mut expected_index = before_index; - expected_index.data[range.start..range.start + 8].copy_from_slice(&target.to_le_bytes()); - assert_eq!(account(&graph.trader), expected_trader); - assert_eq!(account(&graph.global_trader_index), expected_index); - - // Whether the position liquidates depends on its side, so the cascade asserts what the - // program reads, not the outcome. - let (mark_locker, graph) = phoenix_behavior_locker().await; - let (symbol, orderbook, spline) = graph.markets[0].clone(); - let trader_account = mark_locker - .with_svm_reader(|svm| svm.get_account(&graph.trader)) - .unwrap() - .unwrap(); - let prepared_collateral = hawkeye_margin(&mark_locker, &graph).collateral_quote_lots / 2; - let mut cascade = build_phoenix_collateral_scenario( - graph.trader, - &trader_account, - &prepared_collateral.to_string(), - ) - .unwrap(); - let mut shock = phoenix_market_override( - "phoenix-direct-mark-risk-shock", - graph.perp_asset_map, - &[("symbol", symbol.as_str()), ("target_ticks", "1")], - ); - shock.scenario_relative_slot = 1; - cascade.add_override(shock); - mark_locker - .register_scenario(cascade, Some(graph.clock.slot)) - .unwrap(); - mark_locker - .materialize_overrides_for_slot(&None, graph.clock.slot) - .await - .unwrap(); - let before_mark = hawkeye_bbo_for_market(&graph, &mark_locker, orderbook, spline); - assert_eq!( - hawkeye_margin(&mark_locker, &graph).collateral_quote_lots, - prepared_collateral, - "stage 0 prepares the collateral the cascade was built with" - ); - assert_ne!(before_mark.mark_price_ticks, 1); - mark_locker.with_svm_writer(|svm| { - let mut clock = graph.clock.clone(); - clock.slot += 1; - svm.inner.set_sysvar(&clock); - }); - mark_locker - .materialize_overrides_for_slot(&None, graph.clock.slot + 1) - .await - .unwrap(); - let after_mark = hawkeye_bbo_for_market(&graph, &mark_locker, orderbook, spline); - assert_eq!( - after_mark.mark_price_last_updated_slot, - graph.clock.slot + 1 - ); - assert_eq!( - after_mark.mark_price_ticks, 1, - "stage 1 shocks the mark the program itself reads" - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn collateral_stress_follows_the_index_when_the_hot_flag_lags() { - let (locker, graph) = phoenix_behavior_locker().await; - let set_trader = |trader: &Account| { - locker.with_svm_writer(|svm| svm.set_account(&graph.trader, trader.clone()).unwrap()) - }; - // A Trader refetched after it left the hot set has its HOT flag cleared while the local - // index still holds its record. - let mut lagging = locker - .with_svm_reader(|svm| svm.get_account(&graph.trader)) - .unwrap() - .unwrap(); - let flags = u32::from_le_bytes(lagging.data[96..100].try_into().unwrap()); - lagging.data[96..100].copy_from_slice(&(flags & !TRADER_CAPABILITY_HOT).to_le_bytes()); - set_trader(&lagging); - let before = hawkeye_margin(&locker, &graph); - let maintenance = - i64::try_from(before.maintenance_margin_quote_lots).expect("maintenance margin fits i64"); - let target = - before.collateral_quote_lots - before.effective_collateral_quote_lots + maintenance / 2; - - // The program keeps reading the index record, so the Trader account alone changes nothing. - let mut account_only = lagging.clone(); - account_only.data[88..96].copy_from_slice(&target.to_le_bytes()); - set_trader(&account_only); - assert_eq!( - hawkeye_margin(&locker, &graph).collateral_quote_lots, - before.collateral_quote_lots - ); - set_trader(&lagging); - - let scenario = - build_phoenix_collateral_scenario(graph.trader, &lagging, &target.to_string()).unwrap(); - locker - .register_scenario(scenario, Some(graph.clock.slot)) - .unwrap(); - locker - .materialize_overrides_for_slot(&None, graph.clock.slot) - .await - .unwrap(); - let after = hawkeye_margin(&locker, &graph); - assert_eq!( - after.collateral_quote_lots, target, - "the stress lands in the index record the program reads" - ); - assert_eq!( - after.is_liquidatable, 1, - "the stress must leave the trader liquidatable: effective collateral {} against \ - maintenance margin {}", - after.effective_collateral_quote_lots, after.maintenance_margin_quote_lots - ); -} - -#[tokio::test(flavor = "multi_thread")] -async fn maintenance_margin_stress_raises_the_mainnet_requirement() { - let (locker, graph) = phoenix_behavior_locker().await; - let map = PerpAssetMap::try_from_account_bytes(&graph.account(&graph.perp_asset_map).data) - .expect("mainnet PerpAssetMap decodes"); - - // For a hot Trader the program reads positions from the ActiveTraderBuffer, which the Trader - // account's copy can lag behind, so every market's factor is doubled rather than the one - // the copy names. The overrides share a slot, and each reads the map the previous one wrote. - let mut scenario = surfpool_types::Scenario::new( - "phoenix-maintenance-margin-stress".to_string(), - "Double every Phoenix market's maintenance factor".to_string(), - ); - let mut doubled = Vec::new(); - for entry in map.iter() { - let entry = entry.expect("mainnet map entry decodes"); - // Doubled up to 100%, the most a factor can be. - let factor = entry.metadata.risk_params().risk_factors[0] - .saturating_mul(2) - .min(10_000); - scenario.add_override(phoenix_market_override( - "phoenix-maintenance-margin-stress", - graph.perp_asset_map, - &[ - ("symbol", entry.symbol.as_str()), - ("maintenance_risk_factor_bps", &factor.to_string()), - ], - )); - doubled.push((entry.symbol.as_str().to_string(), factor)); - } - - let before = hawkeye_margin(&locker, &graph); - assert!( - before.maintenance_margin_quote_lots > 0, - "no eligible mainnet candidate: the discovered trader's positions require no margin" - ); - locker - .register_scenario(scenario, Some(graph.clock.slot)) - .unwrap(); - locker - .materialize_overrides_for_slot(&None, graph.clock.slot) - .await - .unwrap(); - // A rejected override is only logged, so each market is checked for the factor it was given. - let stressed = locker - .with_svm_reader(|svm| svm.get_account(&graph.perp_asset_map)) - .unwrap() - .unwrap(); - let stressed = PerpAssetMap::try_from_account_bytes(&stressed.data) - .expect("stressed PerpAssetMap decodes"); - for (symbol, factor) in &doubled { - let entry = stressed - .find_by_symbol(symbol) - .expect("symbol lookup should decode") - .expect("the stress keeps every market listed"); - assert_eq!( - entry.metadata.risk_params().risk_factors[0], - *factor, - "{symbol} takes its own doubled factor" - ); - } - let after = hawkeye_margin(&locker, &graph); - - assert_eq!(after.collateral_quote_lots, before.collateral_quote_lots); - assert!( - after.maintenance_margin_quote_lots > before.maintenance_margin_quote_lots, - "a stricter factor must raise the maintenance margin the program computes: {} -> {}", - before.maintenance_margin_quote_lots, - after.maintenance_margin_quote_lots - ); -} - #[tokio::test(flavor = "multi_thread")] async fn scenario_keeps_the_trader_usable_as_the_clock_moves_on() { let later = |graph: &PhoenixMainnetGraph| { @@ -448,21 +174,33 @@ async fn scenario_keeps_the_trader_usable_as_the_clock_moves_on() { ); let (locker, graph) = phoenix_behavior_locker().await; - let trader_account = locker - .with_svm_reader(|svm| svm.get_account(&graph.trader)) - .unwrap() - .unwrap(); - let collateral = hawkeye_margin(&locker, &graph).collateral_quote_lots; - let scenario = - build_phoenix_collateral_scenario(graph.trader, &trader_account, &collateral.to_string()) - .unwrap(); - locker - .register_scenario(scenario, Some(graph.clock.slot)) - .unwrap(); - locker - .materialize_overrides_for_slot(&None, graph.clock.slot) - .await - .unwrap(); + // Any Phoenix override keeps the markets usable; this one leaves SOL's fees as they are. + let (_, orderbook, _) = graph.markets[0].clone(); + let book = phoenix_rise_accounts::orderbook::Orderbook::try_from_account_bytes( + &graph.account(&orderbook).data, + ) + .unwrap() + .header; + play( + &locker, + graph.clock.slot, + vec![phoenix_override( + "phoenix-market-fees", + graph.perp_asset_map, + &[ + ("symbol", "SOL"), + ( + "defaultTakerFeeMicro", + &book.default_taker_fee_micro().to_string(), + ), + ( + "defaultMakerFeeMicro", + &book.default_maker_fee_micro().to_string(), + ), + ], + )], + ) + .await; let prepared = hawkeye_margin(&locker, &graph); locker.with_svm_writer(|svm| svm.inner.set_sysvar(&later(&graph))); @@ -486,33 +224,89 @@ async fn scenario_keeps_the_trader_usable_as_the_clock_moves_on() { ); } +/// A fresh surfnet whose trader a new wallet opened locally: long SOL and short BTC, with bids +/// resting on SOL, BTC and ETH that make it hot. async fn phoenix_behavior_locker() -> (SurfnetSvmLocker, PhoenixMainnetGraph) { - let eternal_program = deployed_program(ETERNAL_PROGRAMDATA).await; - let hawkeye_program = deployed_program(HAWKEYE_PROGRAMDATA).await; - let graph = phoenix_mainnet_graph().await; - let locker = phoenix_surfnet(&graph, &eternal_program, &hawkeye_program); + let mut graph = phoenix_mainnet_graph().await; + let locker = phoenix_surfnet(&graph).await; + let trader = LocalTrader::create(&locker, 10_000).await.unwrap(); + trader + .place_market_order(&locker, "SOL", 1_000) + .await + .unwrap(); + trader + .place_market_order(&locker, "BTC", -100) + .await + .unwrap(); + for symbol in ["SOL", "BTC", "ETH"] { + trader + .place_limit_order(&locker, symbol, 100, 5) + .await + .unwrap(); + } + graph.trader = trader.account; (locker, graph) } +/// The behaviour tests start from a trader a new wallet opens locally, as a user does on mainnet: +/// hot, as its resting orders make it, healthy, with collateral, positions on SOL and BTC and orders +/// alone on ETH. +#[tokio::test(flavor = "multi_thread")] +async fn the_local_trader_is_hot_and_healthy_with_positions_on_sol_and_btc() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let (locker, graph) = phoenix_behavior_locker().await; + let trader = graph.trader; + let index = locker.with_svm_reader(|svm| local_account(svm, &graph.global_trader_index)); + assert!( + index_trader_state_range(&index.unwrap(), &trader.to_bytes()).is_ok(), + "{trader}: hot" + ); + let margin = hawkeye_margin(&locker, &graph); + assert!( + margin.collateral_quote_lots > 0 + && margin.maintenance_margin_quote_lots > 0 + && margin.is_liquidatable == 0, + "{trader}: healthy, with collateral and margin: {margin:?}" + ); + let mut held: Vec<(String, i64)> = { + let mut svm = locker.0.write().await; + let exchange = Exchange::load(&mut svm, &remote).await.unwrap(); + holdings(&svm, &exchange, &trader) + .unwrap() + .into_iter() + .map(|held| (held.market.symbol, held.base_lots)) + .collect() + }; + held.sort(); + assert_eq!( + held, + [ + ("BTC".to_string(), -100), + ("ETH".to_string(), 0), + ("SOL".to_string(), 1_000) + ], + "{trader}: its positions, and orders alone on ETH" + ); +} + /// A local VM holding the deployed programs and every graph account, at the graph's clock. -fn phoenix_surfnet( - graph: &PhoenixMainnetGraph, - eternal_program: &[u8], - hawkeye_program: &[u8], -) -> SurfnetSvmLocker { +async fn phoenix_surfnet(graph: &PhoenixMainnetGraph) -> SurfnetSvmLocker { + let eternal_program = deployed_program(ETERNAL_PROGRAMDATA).await; + let hawkeye_program = deployed_program(HAWKEYE_PROGRAMDATA).await; let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); let locker = SurfnetSvmLocker::new(svm); locker.with_svm_writer(|svm_writer| { svm_writer.inner.set_sysvar(&graph.clock); + svm_writer.inner.set_sysvar(&graph.last_restart_slot); svm_writer .inner .svm - .add_program(PHOENIX_ETERNAL_PROGRAM_ID, eternal_program) + .add_program(PHOENIX_PROGRAM_ID, &eternal_program) .unwrap(); svm_writer .inner .svm - .add_program(HAWKEYE_PROGRAM_ID, hawkeye_program) + .add_program(HAWKEYE_PROGRAM_ID, &hawkeye_program) .unwrap(); for (address, account) in &graph.accounts { svm_writer.set_account(address, account.clone()).unwrap(); @@ -523,10 +317,8 @@ fn phoenix_surfnet( async fn phoenix_mainnet_graph() -> PhoenixMainnetGraph { let mut cache = mainnet_graph_cache().lock().await; - match cache.as_ref() { - Some(Ok(graph)) => return graph.clone(), - Some(Err(reason)) => panic!("{reason}"), - None => {} + if let Some(graph) = cache.as_ref() { + return graph.clone(); } let global_account = fetch(&[PHOENIX_GLOBAL_CONFIG]).await.remove(0); @@ -536,18 +328,18 @@ async fn phoenix_mainnet_graph() -> PhoenixMainnetGraph { let global_trader_index = Pubkey::new_from_array(global.global_trader_index_header_key()); let active_trader_buffer = Pubkey::new_from_array(global.active_trader_buffer_header_key()); - // One request carries both the market list and the trader index discovery walks. - let mut discovery = fetch(&[perp_asset_map, global_trader_index]).await; - let map_account = discovery.remove(0); - let index_account = discovery.remove(0); + let map_account = fetch(&[perp_asset_map]).await.remove(0); let map = PerpAssetMap::try_from_account_bytes(&map_account.data) .expect("mainnet PerpAssetMap decodes"); let mut markets = Vec::new(); + // A local trader deposits the collateral mint into the global vault. let mut addresses = vec![ PHOENIX_GLOBAL_CONFIG, perp_asset_map, global_trader_index, active_trader_buffer, + Pubkey::new_from_array(global.canonical_token_mint_key()), + Pubkey::new_from_array(global.global_vault_key()), ]; for symbol in ["SOL", "BTC"] { let entry = map @@ -556,119 +348,80 @@ async fn phoenix_mainnet_graph() -> PhoenixMainnetGraph { .expect("mainnet SOL/BTC market"); let orderbook = Pubkey::new_from_array(entry.metadata.static_market_params().market_account); - let spline = derive_spline_collection_address(&PHOENIX_ETERNAL_PROGRAM_ID, &orderbook); + let spline = derive_spline_collection_address(&PHOENIX_PROGRAM_ID, &orderbook); addresses.extend([orderbook, spline]); markets.push((symbol.to_string(), orderbook, spline)); } - let eternal_program = deployed_program(ETERNAL_PROGRAMDATA).await; - let hawkeye_program = deployed_program(HAWKEYE_PROGRAMDATA).await; + // The clock and every account from one bank, so the programs accept them together. let clock_address = Pubkey::from_str_const("SysvarC1ock11111111111111111111111111111111"); - // Every hot Trader is a candidate, in address order, so the pick moves only when it or a - // trader ahead of it changes. - let mut candidates: Vec = index_trader_state_ranges(&index_account) - .expect("mainnet GlobalTraderIndex should walk") + let keys: Vec = addresses + .iter() + .copied() + .chain([LAST_RESTART_SLOT, clock_address]) + .collect(); + let mut fetched = fetch(&keys).await; + let clock: Clock = bincode::deserialize(&fetched.pop().unwrap().data).unwrap(); + assert!(clock.slot > 0); + // A surfnet loads the restart sysvar from its datasource; the bare test VM has none. + let last_restart_slot: LastRestartSlot = + bincode::deserialize(&fetched.pop().unwrap().data).unwrap(); + + let mut graph = PhoenixMainnetGraph { + clock, + last_restart_slot, + accounts: addresses.iter().copied().zip(fetched).collect(), + global_trader_index, + active_trader_buffer, + perp_asset_map, + trader: Pubkey::default(), + markets, + }; + let books = fetch_books(&map_account, perp_asset_map, &addresses).await; + graph.accounts.extend(books); + *cache = Some(graph.clone()); + graph +} + +/// Every market's book and splines not in `held`, read seconds after the trader index, as a +/// surfnet's first load reads them. A book read minutes later names traders by index nodes that +/// moved, and Phoenix then panics or fails with InvalidAccountData. +async fn fetch_books( + map: &Account, + perp_asset_map: Pubkey, + held: &[Pubkey], +) -> Vec<(Pubkey, Account)> { + let missing: Vec = phoenix_markets(perp_asset_map, map) + .unwrap() .into_iter() - .map(|(trader, _)| trader) + .flat_map(|market| [market.orderbook, market.splines]) + .filter(|address| !held.contains(address)) .collect(); - candidates.sort_unstable(); - // Every batch re-reads the dependencies and the clock, so each fills a request to the - // 100-account cap. - let batch_len = 100 - addresses.len() - 1; - let mut last_error = None; - for batch in candidates.chunks(batch_len) { - // Read the clock, every dependency and this batch of traders from one bank, so a trader - // is checked, and then tested, on state the programs accept together. - let keys: Vec = addresses - .iter() - .chain(batch) - .chain([&clock_address]) - .copied() - .collect(); - let mut fetched = client() - .get_multiple_accounts(&keys, CommitmentConfig::confirmed()) + let mut books = Vec::new(); + for chunk in missing.chunks(100) { + let fetched = client() + .get_multiple_accounts(chunk, CommitmentConfig::confirmed()) .await - .unwrap_or_else(|e| panic!("failed to fetch {keys:?} from mainnet: {e}")) - .into_iter(); - let dependencies: Vec<(Pubkey, Account)> = addresses - .iter() - .zip(fetched.by_ref()) - .map(|(address, result)| { - let account = result.map_account().unwrap_or_else(|_| { - panic!("{address} no longer exists on mainnet; the test needs a new address") - }); - (*address, account) - }) - .collect(); - // A trader closed since the index was read is skipped, not an error. - let traders: Vec<(Pubkey, Account)> = batch - .iter() - .zip(fetched.by_ref()) - .filter_map(|(trader, result)| Some((*trader, result.map_account().ok()?))) - .collect(); - let clock_account = fetched - .next() - .and_then(|result| result.map_account().ok()) - .expect("the clock is read with the graph"); - let clock: Clock = bincode::deserialize(&clock_account.data).unwrap(); - assert!(clock.slot > 0); - - let mut graph = PhoenixMainnetGraph { - clock, - accounts: dependencies.iter().chain(&traders).cloned().collect(), - global_trader_index, - active_trader_buffer, - perp_asset_map, - trader: Pubkey::default(), - markets: markets.clone(), - }; - // A trader that left the index since it was listed is no longer hot. - let indexed: HashSet = - index_trader_state_ranges(graph.account(&global_trader_index)) - .expect("mainnet GlobalTraderIndex should walk") - .into_iter() - .map(|(trader, _)| trader) - .collect(); - let locker = phoenix_surfnet(&graph, &eternal_program, &hawkeye_program); - for (trader, account) in traders - .iter() - .filter(|(trader, _)| indexed.contains(trader)) - { - graph.trader = *trader; - match trader_is_eligible(&locker, &graph) { - Ok(false) => {} - Err(error) => last_error = Some(format!("{trader}: {error}")), - Ok(true) => { - // The rest of the batch was read only to be checked. - graph.accounts = dependencies - .into_iter() - .chain([(*trader, account.clone())]) - .collect(); - *cache = Some(Ok(graph.clone())); - return graph; - } - } - } + .unwrap_or_else(|e| panic!("failed to fetch the Phoenix books from mainnet: {e}")); + books.extend( + chunk + .iter() + .zip(fetched) + .filter_map(|(address, result)| Some((*address, result.map_account().ok()?))), + ); } - - let reason = format!( - "no eligible mainnet candidate: no hot Phoenix Trader in the GlobalTraderIndex has \ - collateral, a position Hawkeye margins and a healthy account{}", - last_error - .map(|error| format!("; the last margin view that failed was {error}")) - .unwrap_or_default() - ); - *cache = Some(Err(reason.clone())); - panic!("{reason}") + books } #[derive(Clone)] struct PhoenixMainnetGraph { clock: Clock, + last_restart_slot: LastRestartSlot, accounts: Vec<(Pubkey, Account)>, global_trader_index: Pubkey, active_trader_buffer: Pubkey, perp_asset_map: Pubkey, + /// The local trader [`phoenix_behavior_locker`] opens; the mainnet graph alone has none. trader: Pubkey, /// Symbol, orderbook and spline for each market the Hawkeye BBO view reads. markets: Vec<(String, Pubkey, Pubkey)>, @@ -702,7 +455,7 @@ fn try_hawkeye_view( ) -> Result, String> { let payer = Keypair::new(); let accounts = [ - PHOENIX_ETERNAL_PROGRAM_ID, + PHOENIX_PROGRAM_ID, PHOENIX_GLOBAL_CONFIG, graph.global_trader_index, graph.active_trader_buffer, @@ -734,7 +487,7 @@ fn try_hawkeye_view( }) } -fn hawkeye_margin(locker: &SurfnetSvmLocker, graph: &PhoenixMainnetGraph) -> HawkeyeMarginView { +fn hawkeye_margin(locker: &SurfnetSvmLocker, graph: &PhoenixMainnetGraph) -> MarginView { try_hawkeye_margin(locker, graph) .unwrap_or_else(|e| panic!("Hawkeye margin view failed for {}: {e}", graph.trader)) } @@ -742,22 +495,9 @@ fn hawkeye_margin(locker: &SurfnetSvmLocker, graph: &PhoenixMainnetGraph) -> Haw fn try_hawkeye_margin( locker: &SurfnetSvmLocker, graph: &PhoenixMainnetGraph, -) -> Result { - let data = try_hawkeye_view( - locker, - graph, - HAWKEYE_VIEW_MARGIN_DISCRIMINANT, - &[graph.trader], - )?; - let margin = bytemuck::try_pod_read_unaligned::(&data) - .map_err(|e| format!("the margin view returned {} bytes: {e:?}", data.len()))?; - if margin.magic != HAWKEYE_MARGIN_RETURN_MAGIC { - return Err(format!( - "the margin view returned magic {:#x}", - margin.magic - )); - } - Ok(margin) +) -> Result { + let data = try_hawkeye_view(locker, graph, VIEW_MARGIN, &[graph.trader])?; + MarginView::from_return_data(&data).map_err(|e| e.to_string()) } fn hawkeye_bbo_for_market( @@ -765,73 +505,20 @@ fn hawkeye_bbo_for_market( locker: &SurfnetSvmLocker, orderbook: Pubkey, spline: Pubkey, -) -> HawkeyeBboView { - let data = hawkeye_view( - locker, - graph, - HAWKEYE_VIEW_BBO_DISCRIMINANT, - &[orderbook, spline], - ); - let bbo = bytemuck::pod_read_unaligned::(&data); - assert_eq!(bbo.magic, HAWKEYE_BBO_RETURN_MAGIC); - bbo -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -struct HawkeyeMarginView { - magic: u64, - version: u16, - position_count: u16, - risk_state: u8, - risk_tier: u8, - is_liquidatable: u8, - padding: u8, - collateral_quote_lots: i64, - effective_collateral_quote_lots: i64, - free_collateral_quote_lots: i64, - withdrawable_collateral_quote_lots: u64, - initial_margin_quote_lots: u64, - maintenance_margin_quote_lots: u64, - cancel_margin_quote_lots: u64, - backstop_margin_quote_lots: u64, - high_risk_margin_quote_lots: u64, - unrealized_pnl_quote_lots: i64, - discounted_unrealized_pnl_quote_lots: i64, - unsettled_funding_quote_lots: i64, -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Pod, Zeroable)] -struct HawkeyeBboView { - magic: u64, - version: u16, - flags: u8, - padding: [u8; 5], - best_bid_ticks: u64, - best_ask_ticks: u64, - mark_price_ticks: u64, - index_price_ticks: u64, - mark_price_last_updated_slot: u64, - index_price_last_updated_slot: u64, -} - -const HAWKEYE_PROGRAM_ID: Pubkey = - Pubkey::from_str_const("RiSeVw3ZjNfsaXPRb4mgaqYaEEt41pNNJoDvVh7pgQj"); - -const HAWKEYE_MARGIN_RETURN_MAGIC: u64 = 0x955f5b9d3dff253f; - -const HAWKEYE_BBO_RETURN_MAGIC: u64 = 0xefca1fa31fa74171; - -fn phoenix_market_override( +) -> BboView { + let data = hawkeye_view(locker, graph, VIEW_BBO, &[orderbook, spline]); + BboView::from_return_data(&data).unwrap() +} + +fn phoenix_override( template_id: &str, - perp_asset_map: Pubkey, + account: Pubkey, values: &[(&str, &str)], ) -> surfpool_types::OverrideInstance { surfpool_types::OverrideInstance::new( template_id.to_string(), 0, - surfpool_types::AccountAddress::Pubkey(perp_asset_map.to_string()), + surfpool_types::AccountAddress::Pubkey(account.to_string()), ) .with_values( values @@ -840,3 +527,1935 @@ fn phoenix_market_override( .collect(), ) } + +/// `phoenix-trader-cancel-orders` for `trader` on `symbol`, or on every market, which the template +/// takes as values; its own account is the PerpAssetMap. +fn cancel_orders(trader: Pubkey, symbol: Option<&str>) -> surfpool_types::OverrideInstance { + let trader = trader.to_string(); + phoenix_override( + "phoenix-trader-cancel-orders", + PHOENIX_PERP_ASSET_MAP, + &[ + ("trader", trader.as_str()), + ("symbol", symbol.unwrap_or("")), + ], + ) +} + +/// `phoenix-trader-collateral` bringing `trader`'s effective collateral to `quote_lots`, which the +/// template takes in USDC. +fn set_collateral(trader: Pubkey, quote_lots: i64) -> surfpool_types::OverrideInstance { + let sign = if quote_lots < 0 { "-" } else { "" }; + let lots = quote_lots.unsigned_abs(); + let usdc = format!("{sign}{}.{:06}", lots / 1_000_000, lots % 1_000_000); + phoenix_override( + "phoenix-trader-collateral", + trader, + &[("effective_collateral_usdc", usdc.as_str())], + ) +} + +/// `phoenix-market-move` of `symbol` to `ticks`, then the `phoenix-market-uncross` that matches the +/// resting orders the move crossed. +fn move_and_uncross(symbol: &str, ticks: u64) -> [surfpool_types::OverrideInstance; 2] { + let ticks = ticks.to_string(); + [ + phoenix_override( + "phoenix-market-move", + PHOENIX_PERP_ASSET_MAP, + &[("symbol", symbol), ("target_ticks", ticks.as_str())], + ), + phoenix_override( + "phoenix-market-uncross", + PHOENIX_PERP_ASSET_MAP, + &[("symbol", symbol)], + ), + ] +} + +/// A market move reprices the oracle readings and the makers' quotes and leaves the resting orders +/// where they are; the uncross after it matches the ones the move crossed, so the book quotes +/// around the target. +#[tokio::test(flavor = "multi_thread")] +async fn a_market_move_reprices_the_oracles_and_the_book_together() { + let (locker, graph) = phoenix_behavior_locker().await; + let remote = Some((client(), CommitmentConfig::confirmed())); + let resting = |orderbook: &Pubkey| { + let book = locker + .with_svm_reader(|svm| svm.get_account(orderbook)) + .unwrap() + .unwrap(); + let book = Orderbook::try_from_account_bytes(&book.data).unwrap(); + let prices = |orders: phoenix_rise_accounts::orderbook::OrderbookSideIter<'_>| { + orders + .map(|order| order.price_in_ticks().as_inner()) + .collect::>() + }; + (prices(book.bid_orders()), prices(book.ask_orders())) + }; + + for (symbol, orderbook, spline) in graph.markets.clone() { + let before = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline); + let target = before.mark_price_ticks * 93 / 100; + let map_before = locker + .with_svm_reader(|svm| svm.get_account(&graph.perp_asset_map)) + .unwrap(); + let orders_before = resting(&orderbook); + + let writes = { + let mut svm = locker.0.write().await; + move_market(&mut svm, &remote, &symbol, target) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")) + }; + assert_eq!( + locker + .with_svm_reader(|svm| svm.get_account(&graph.perp_asset_map)) + .unwrap(), + map_before, + "{symbol}: the move runs on a copy of the local VM" + ); + let written: Vec = writes.iter().map(|(pubkey, _)| *pubkey).collect(); + assert!( + written.contains(&graph.perp_asset_map) && written.contains(&spline), + "{symbol}: the oracles and the splines move together, wrote {written:?}" + ); + apply(&locker, writes).await; + assert_eq!( + resting(&orderbook), + orders_before, + "{symbol}: the move alone leaves the resting orders where they are" + ); + + let writes = { + let mut svm = locker.0.write().await; + uncross_market(&mut svm, &remote, &symbol) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")) + }; + apply(&locker, writes).await; + let after = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline); + assert_eq!( + after.index_price_ticks, target, + "{symbol}: the index is the oracle reports" + ); + // The mark also weighs in the book, which now quotes around the target. + assert!( + after.mark_price_ticks.abs_diff(target) <= (target / 1_000).max(2), + "{symbol}: the mark follows the oracles and the book to the target" + ); + assert!( + after.best_bid_ticks < after.best_ask_ticks, + "{symbol}: resting orders the move crossed are matched" + ); + let band = target / 100; + assert!( + after.best_bid_ticks + band >= target && after.best_ask_ticks <= target + band, + "{symbol}: the makers quote around the new price" + ); + + let uncrossed = resting(&orderbook); + let writes = { + let mut svm = locker.0.write().await; + uncross_market(&mut svm, &remote, &symbol) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")) + }; + apply(&locker, writes).await; + assert_eq!( + resting(&orderbook), + uncrossed, + "{symbol}: an uncross on a book that is not crossed matches nothing" + ); + } +} + +/// The oracle price template moves only the oracle readings, to the ticks given or by a percentage +/// of the mark; the book and the makers' quotes stay as they were. +#[tokio::test(flavor = "multi_thread")] +async fn an_oracle_price_override_moves_the_oracles_and_leaves_the_book() { + let (locker, graph) = phoenix_behavior_locker().await; + let remote = Some((client(), CommitmentConfig::confirmed())); + // The maintenance every Phoenix override runs first, so the book and splines compared below + // change only through the template. + { + let mut svm = locker.0.write().await; + keep_markets_usable(&mut svm, &remote, graph.clock.slot).await; + } + let account = |address| locker.with_svm_reader(|svm| local_account(svm, &address).unwrap()); + + for (symbol, orderbook, spline) in graph.markets.clone() { + let before = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline); + let (book, splines) = (account(orderbook), account(spline)); + let target = before.mark_price_ticks * 93 / 100; + let ticks = [ + ("symbol", symbol.clone()), + ("target_ticks", target.to_string()), + ]; + configure( + &locker, + "phoenix-oracle-price", + graph.perp_asset_map, + &ticks, + ) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")); + + let after = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline); + assert_eq!( + after.index_price_ticks, target, + "{symbol}: the oracles report the target" + ); + assert_eq!( + (after.best_bid_ticks, after.best_ask_ticks), + (before.best_bid_ticks, before.best_ask_ticks), + "{symbol}: the book keeps its prices" + ); + assert!( + account(orderbook) == book && account(spline) == splines, + "{symbol}: the order book and the makers' splines are not written" + ); + + let map = account(graph.perp_asset_map); + let mark = Market::find(&graph.perp_asset_map, &map.data, &symbol) + .unwrap() + .mark_ticks; + let percent = [ + ("symbol", symbol.clone()), + ("change_percent", "-2.5".to_string()), + ]; + configure( + &locker, + "phoenix-oracle-price", + graph.perp_asset_map, + &percent, + ) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")); + // Within a tick: the template rounds a float. + let index = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline).index_price_ticks; + assert!( + index.abs_diff(mark * 975 / 1_000) <= 1, + "{symbol}: a percentage is taken from the mark, {index} from {mark}" + ); + } +} + +/// A move leaves the resting bids above the new price crossed, and one uncross crank matches at most +/// 64 of them, so `phoenix-market-uncross` cranks until the book is uncrossed. +#[tokio::test(flavor = "multi_thread")] +async fn a_move_past_more_resting_bids_than_one_crank_matches_uncrosses_the_book() { + let (locker, graph) = phoenix_behavior_locker().await; + let remote = Some((client(), CommitmentConfig::confirmed())); + for (symbol, orderbook, spline) in graph.markets.clone() { + let bids: Vec = Orderbook::try_from_account_bytes(&graph.account(&orderbook).data) + .unwrap() + .bid_orders() + .map(|bid| bid.price_in_ticks().as_inner()) + .collect(); + // Just below the 81st best bid, at least 81 resting bids cross the re-centred splines. + let Some(target) = bids.get(80).map(|price| price - 1) else { + continue; + }; + let writes = { + let mut svm = locker.0.write().await; + move_market(&mut svm, &remote, &symbol, target) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")) + }; + apply(&locker, writes).await; + let moved = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline); + assert!( + moved.crossed(), + "{symbol}: the move alone leaves the bids above {target} crossed: {moved:?}" + ); + let bids_from = |price: u64| { + let book = locker + .with_svm_reader(|svm| svm.get_account(&orderbook)) + .unwrap() + .unwrap(); + Orderbook::try_from_account_bytes(&book.data) + .unwrap() + .bid_orders() + .filter(|bid| bid.price_in_ticks().as_inner() >= price) + .count() + }; + // Bids between the target and the makers' new ask are not crossed and stay. + let crossed = bids_from(moved.best_ask_ticks); + if crossed <= 64 { + continue; + } + + let writes = { + let mut svm = locker.0.write().await; + uncross_market(&mut svm, &remote, &symbol) + .await + .unwrap_or_else(|e| panic!("{symbol}: {e}")) + }; + apply(&locker, writes).await; + let after = hawkeye_bbo_for_market(&graph, &locker, orderbook, spline); + assert!( + after.best_bid_ticks < after.best_ask_ticks, + "{symbol}: the book is uncrossed at {target} ticks: {after:?}" + ); + let matched = crossed - bids_from(moved.best_ask_ticks); + assert!( + matched > 64, + "{symbol}: the cranks matched {matched} of the {crossed} crossed bids, more than one crank does" + ); + return; + } + panic!( + "no eligible mainnet candidate: no book of {:?} leaves more than 64 bids crossed", + graph.markets + ); +} + +/// A one-lot market buy by `trader` in `symbol`, run on a copy of the test VM; it shows whether +/// Phoenix lets the trader place market orders there. +/// The market `symbol` in the surfnet of `locker`, loaded from mainnet when it is missing. +async fn market_context(locker: &SurfnetSvmLocker, symbol: &str) -> SurfpoolResult { + let remote = Some((client(), CommitmentConfig::confirmed())); + let mut svm = locker.0.write().await; + MarketContext::load(&mut svm, &remote, symbol).await +} + +async fn buy_one_lot( + locker: &SurfnetSvmLocker, + trader: Pubkey, + symbol: &str, +) -> SurfpoolResult> { + let remote = Some((client(), CommitmentConfig::confirmed())); + let mut svm = locker.0.write().await; + let market = MarketContext::load(&mut svm, &remote, symbol).await?; + let header = TraderHeader::try_read_from_account_bytes(&local_account(&svm, &trader)?.data) + .expect("a Phoenix Trader"); + let wallet = Pubkey::new_from_array(header.authority); + run_instructions(&svm, &[market_order(&market, wallet, trader, 1)?]) +} + +/// The accounts of an order `trader` places in the market of `context`, signed by its `wallet`. +fn order_accounts( + context: &MarketContext, + wallet: Pubkey, + trader: Pubkey, +) -> [(&'static str, Pubkey); 10] { + [ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("traderWallet", wallet), + ("traderAccount", trader), + ("perpAssetMap", context.exchange.perp_asset_map), + ("globalTraderIndex", context.exchange.global_trader_index), + ("activeTraderBuffer", context.exchange.active_trader_buffer), + ("orderbook", context.market.orderbook), + ("splines", context.market.splines), + ] +} + +/// A market order of `lots`, a buy when positive and a sell when negative, which fills at once. +fn market_order( + context: &MarketContext, + wallet: Pubkey, + trader: Pubkey, + lots: i64, +) -> SurfpoolResult { + let side = if lots > 0 { "Bid" } else { "Ask" }; + let client_order_id = [0_u8; 16]; + phoenix_instruction( + "PlaceMarketOrder", + &order_accounts(context, wallet, trader), + &serde_json::json!({ + "orderPacket": { + "kind": { + "ImmediateOrCancel": { + "side": { side: null }, + "priceInTicks": null, + "numBaseLots": { "inner": lots.unsigned_abs() }, + "numQuoteLots": null, + "minBaseLotsToFill": { "inner": lots.unsigned_abs() }, + "minQuoteLotsToFill": { "inner": 0 }, + "selfTradeBehavior": { "DecrementTake": null }, + "matchLimit": null, + "clientOrderId": client_order_id, + "lastValidSlot": null, + "orderFlags": { "flags": 0 }, + "cancelExisting": false, + } + } + } + }), + ) +} + +/// A Trader a new wallet opens in the local VM, as a user opens one on mainnet: registered, then +/// granted every capability by the risk authority, which unfreezes it, and funded with a deposit. +#[derive(Clone, Copy)] +struct LocalTrader { + wallet: Pubkey, + account: Pubkey, +} + +impl LocalTrader { + /// A trader with `usdc` of collateral, no positions and no orders. + async fn create(locker: &SurfnetSvmLocker, usdc: u64) -> SurfpoolResult { + let wallet = Keypair::new().pubkey(); + let account = Pubkey::find_program_address( + &[b"trader", wallet.as_ref(), &[0, 0]], + &PHOENIX_PROGRAM_ID, + ) + .0; + let remote = Some((client(), CommitmentConfig::confirmed())); + let exchange = { + let mut svm = locker.0.write().await; + Exchange::load(&mut svm, &remote).await? + }; + let mint = Pubkey::new_from_array(exchange.config.canonical_token_mint_key()); + let token_account = get_associated_token_address(&wallet, &mint); + // A USDC token unit is a quote lot. + let amount = usdc * 1_000_000; + let mut data = vec![0; spl_token_interface::state::Account::LEN]; + spl_token_interface::state::Account { + mint, + owner: wallet, + amount, + state: spl_token_interface::state::AccountState::Initialized, + ..Default::default() + } + .pack_into_slice(&mut data); + locker.with_svm_writer(|svm| { + svm.inner.airdrop(&wallet, 10_000_000_000).unwrap(); + let lamports = svm.inner.minimum_balance_for_rent_exemption(data.len()); + svm.set_account( + &token_account, + Account { + lamports, + data, + owner: spl_token_interface::ID, + executable: false, + rent_epoch: 0, + }, + ) + })?; + + let risk = Pubkey::new_from_array(exchange.config.risk_authority()); + let toggles = [ + "PlaceLimitOrder", + "PlaceMarketOrder", + "RiskIncreasingTrade", + "RiskReducingTrade", + "DepositCollateral", + "WithdrawCollateral", + ] + .map(|target| serde_json::json!({ "target": { target: null }, "enable": true })); + let instructions = [ + phoenix_instruction( + "RegisterTrader", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("payer", wallet), + ("traderWallet", wallet), + ("traderAccount", account), + ("systemProgram", solana_sdk_ids::system_program::id()), + ], + &serde_json::json!({ + "params": { + "maxPositions": 32, + "traderPreferenceBits": 0, + "traderPdaIndex": 0, + "traderSubaccountIndex": 0, + } + }), + )?, + phoenix_instruction( + "SetTraderCapability", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("authority", risk), + ("maybePermissionAccount", risk), + ("traderAccount", account), + ("globalTraderIndex", exchange.global_trader_index), + ("activeTraderBuffer", exchange.active_trader_buffer), + ], + &serde_json::json!({ "params": { "toggles": toggles } }), + )?, + phoenix_instruction( + "DepositFunds", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("traderWallet", wallet), + ("traderTokenAccount", token_account), + ("traderAccount", account), + ( + "globalVault", + Pubkey::new_from_array(exchange.config.global_vault_key()), + ), + ("tokenProgram", spl_token_interface::ID), + ("globalTraderIndex", exchange.global_trader_index), + ("activeTraderBuffer", exchange.active_trader_buffer), + ], + &serde_json::json!({ "params": { "amount": amount } }), + )?, + ]; + let writes = locker.with_svm_reader(|svm| run_instructions(svm, &instructions))?; + apply(locker, writes).await; + Ok(Self { wallet, account }) + } + + /// A market order of `lots` in `symbol`, a buy when positive and a sell when negative, which + /// opens or changes the trader's position there. + async fn place_market_order( + &self, + locker: &SurfnetSvmLocker, + symbol: &str, + lots: i64, + ) -> SurfpoolResult<()> { + let context = market_context(locker, symbol).await?; + let order = market_order(&context, self.wallet, self.account, lots)?; + let writes = locker.with_svm_reader(|svm| run_instructions(svm, &[order]))?; + apply(locker, writes).await; + Ok(()) + } + + /// A PostOnly order of `lots` in `symbol`, which rests `away_percent` from the mark: a bid below + /// it when positive, an ask above it when negative. The first resting order makes the trader + /// hot, in Phoenix's index. + async fn place_limit_order( + &self, + locker: &SurfnetSvmLocker, + symbol: &str, + lots: i64, + away_percent: u64, + ) -> SurfpoolResult<()> { + let context = market_context(locker, symbol).await?; + let mark = context.market.mark_ticks; + let (side, price) = if lots > 0 { + ("Bid", mark - mark * away_percent / 100) + } else { + ("Ask", mark + mark * away_percent / 100) + }; + let client_order_id = [0_u8; 16]; + let order = phoenix_instruction( + "PlaceLimitOrder", + &order_accounts(&context, self.wallet, self.account), + &serde_json::json!({ + "orderPacket": { + "kind": { + "PostOnly": { + "side": { side: null }, + "priceInTicks": { "inner": price }, + "numBaseLots": { "inner": lots.unsigned_abs() }, + "clientOrderId": client_order_id, + "slide": false, + "lastValidSlot": null, + "orderFlags": { "flags": 0 }, + "cancelExisting": false, + } + } + } + }), + )?; + let writes = locker.with_svm_reader(|svm| run_instructions(svm, &[order]))?; + apply(locker, writes).await; + Ok(()) + } +} + +/// Plays a scenario of `overrides`, in this order, at `slot` through the materializer. +async fn play( + locker: &SurfnetSvmLocker, + slot: u64, + overrides: Vec, +) { + let mut scenario = surfpool_types::Scenario::new("play".to_string(), "play".to_string()); + for instance in overrides { + scenario.add_override(instance); + } + locker.register_scenario(scenario, Some(slot)).unwrap(); + locker + .materialize_overrides_for_slot(&Some((client(), CommitmentConfig::confirmed())), slot) + .await + .unwrap(); +} + +/// Runs a template through the Phoenix dispatch the materializer calls and applies its writes, +/// returning the program's refusal when there is one. +async fn configure( + locker: &SurfnetSvmLocker, + template_id: &str, + account: Pubkey, + values: &[(&str, String)], +) -> Result<(), String> { + let values: std::collections::HashMap = values + .iter() + .map(|(field, value)| (field.to_string(), serde_json::json!(value))) + .collect(); + let mut svm = locker.0.write().await; + let target = svm.inner.get_account(&account).unwrap().unwrap_or_default(); + let writes = + crate::scenarios::protocols::phoenix_eternal::v1::state_builder::prepare_phoenix_override( + &mut svm, + template_id, + &account, + &target, + &values, + &Some((client(), CommitmentConfig::confirmed())), + 0, + ) + .await + .map_err(|e| format!("{template_id}: {e}"))? + .unwrap_or_default(); + for (pubkey, written) in writes { + svm.set_account(&pubkey, written).unwrap(); + } + Ok(()) +} + +fn sol(field: &'static str, value: String) -> Vec<(&'static str, String)> { + vec![("symbol", "SOL".to_string()), (field, value)] +} + +fn sol_metadata( + locker: &SurfnetSvmLocker, + graph: &PhoenixMainnetGraph, +) -> phoenix_rise_accounts::perp_asset_map::PerpAssetMetadata { + let map = locker + .with_svm_reader(|svm| svm.get_account(&graph.perp_asset_map)) + .unwrap() + .unwrap(); + PerpAssetMap::try_from_account_bytes(&map.data) + .unwrap() + .find_by_symbol("SOL") + .unwrap() + .unwrap() + .metadata +} + +#[tokio::test(flavor = "multi_thread")] +async fn market_and_exchange_configuration_reaches_the_accounts_phoenix_reads() { + let (locker, graph) = phoenix_behavior_locker().await; + let (_, orderbook, _) = graph.markets[0].clone(); + let before = sol_metadata(&locker, &graph); + let slot = graph.clock.slot; + + // Studio's risk factor preset sends only the maintenance factor; the others, left out or + // empty, keep their values. + play( + &locker, + slot, + vec![phoenix_override( + "phoenix-market-risk-factors", + graph.perp_asset_map, + &[ + ("symbol", "SOL"), + ("maintenanceRiskFactor", "7000"), + ("backstopRiskFactor", ""), + ], + )], + ) + .await; + let [_, backstop, high_risk] = before.risk_params().risk_factors; + assert_eq!( + sol_metadata(&locker, &graph).risk_params().risk_factors, + [7000, backstop, high_risk], + "the risk authority set SOL's factors" + ); + + configure( + &locker, + "phoenix-market-cancel-risk-factor", + graph.perp_asset_map, + &sol("cancelOrderRiskFactor", "8000".into()), + ) + .await + .unwrap(); + assert_eq!( + sol_metadata(&locker, &graph) + .risk_params() + .cancel_order_risk_factor, + 8000 + ); + + configure( + &locker, + "phoenix-market-max-liquidation-size", + graph.perp_asset_map, + &sol("maxLiquidationSize", "123456".into()), + ) + .await + .unwrap(); + assert_eq!( + sol_metadata(&locker, &graph) + .risk_params() + .max_liquidation_size + .as_inner(), + 123456 + ); + + let cap = before.open_interest_params().open_interest_cap.as_inner() * 2; + configure( + &locker, + "phoenix-market-open-interest-cap", + graph.perp_asset_map, + &sol("openInterestCap", cap.to_string()), + ) + .await + .unwrap(); + assert_eq!( + sol_metadata(&locker, &graph) + .open_interest_params() + .open_interest_cap + .as_inner(), + cap + ); + + let max_rate = before.funding_accumulator().max_funding_rate.as_inner() / 2; + configure( + &locker, + "phoenix-market-funding", + graph.perp_asset_map, + &sol("maxFundingRate", max_rate.to_string()), + ) + .await + .unwrap(); + let funding = *sol_metadata(&locker, &graph).funding_accumulator(); + assert_eq!(funding.max_funding_rate.as_inner(), max_rate); + assert_eq!( + funding.funding_period_seconds, + before.funding_accumulator().funding_period_seconds, + "a funding field left out keeps its value" + ); + + configure( + &locker, + "phoenix-market-fees", + graph.perp_asset_map, + &[ + ("symbol", "SOL".to_string()), + ("defaultTakerFeeMicro", "1000".to_string()), + ("defaultMakerFeeMicro", "-50".to_string()), + ], + ) + .await + .unwrap(); + let book = locker + .with_svm_reader(|svm| svm.get_account(&orderbook)) + .unwrap() + .unwrap(); + let header = phoenix_rise_accounts::orderbook::Orderbook::try_from_account_bytes(&book.data) + .unwrap() + .header; + assert_eq!( + ( + header.default_taker_fee_micro(), + header.default_maker_fee_micro() + ), + (1000, -50) + ); + + let global = + GlobalConfig::try_from_account_bytes(&graph.account(&PHOENIX_GLOBAL_CONFIG).data).unwrap(); + let queue = Pubkey::new_from_array(global.withdraw_queue_key()); + configure( + &locker, + "phoenix-withdraw-limits", + queue, + &[ + ("maxBudget", "1000000000".to_string()), + ("replenishAmountPerSlot", "0".to_string()), + ], + ) + .await + .unwrap(); + configure( + &locker, + "phoenix-withdraw-parameters", + queue, + &[ + ("depositCooldownPeriodInSlots", "300".to_string()), + ("withdrawalFee", "1000000".to_string()), + ("enqueueingFee", "2000000".to_string()), + ], + ) + .await + .unwrap(); + let queue_account = locker + .with_svm_reader(|svm| svm.get_account(&queue)) + .unwrap() + .unwrap(); + let queue_header = + phoenix_rise_accounts::withdraw_queue::WithdrawQueueHeader::try_from_account_bytes( + &queue_account.data, + ) + .unwrap(); + assert_eq!( + ( + queue_header.withdraw_throttle().max_budget().as_inner(), + queue_header + .withdraw_throttle() + .replenish_amount_per_slot() + .as_inner(), + queue_header.withdrawal_fee().as_inner(), + queue_header.enqueueing_fee().as_inner(), + ), + (1_000_000_000, 0, 1_000_000, 2_000_000) + ); + let global_data = || { + locker + .with_svm_reader(|svm| svm.get_account(&PHOENIX_GLOBAL_CONFIG)) + .unwrap() + .unwrap() + .data + }; + assert_eq!( + GlobalConfig::try_from_account_bytes(&global_data()) + .unwrap() + .deposit_cooldown_period_in_slots(), + 300 + ); + + play( + &locker, + slot + 1, + vec![phoenix_override( + "phoenix-market-status", + graph.perp_asset_map, + &[("symbol", "SOL"), ("nextMarketStatus", "Paused")], + )], + ) + .await; + let refused = buy_one_lot(&locker, graph.trader, "SOL") + .await + .expect_err("a paused SOL market refuses a market order") + .to_string(); + assert!( + refused.contains("Market status Paused does not satisfy"), + "{refused}" + ); + + let global_before = global_data(); + play( + &locker, + slot + 2, + vec![phoenix_override( + "phoenix-exchange-status", + PHOENIX_GLOBAL_CONFIG, + &[("maintenance", "true")], + )], + ) + .await; + assert_ne!( + global_data(), + global_before, + "the root authority switched a status flag" + ); +} + +/// Sends a keeper's liquidation of `trader`'s position in `symbol`, as much as the market allows in +/// one go and at most 10% through the mark, like the keepers on mainnet. It is a real transaction, +/// signed by a funded keeper, with signatures checked as on any surfnet. +async fn send_liquidation( + locker: &SurfnetSvmLocker, + trader: Pubkey, + symbol: &str, + long: bool, +) -> Result<(), String> { + let context = market_context(locker, symbol) + .await + .map_err(|e| format!("{symbol}: {e}"))?; + let keeper = Keypair::new(); + let mark = context.market.mark_ticks; + let limit = if long { + mark - mark / 10 + } else { + mark + mark / 10 + }; + let mint = Pubkey::new_from_array(context.exchange.config.canonical_token_mint_key()); + let liquidation = phoenix_instruction( + "LiquidateViaMarketOrder", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("liquidatorWallet", keeper.pubkey()), + ("liquidatedTrader", trader), + ("traderAccount", trader), + ("perpAssetMap", context.exchange.perp_asset_map), + ("globalTraderIndex", context.exchange.global_trader_index), + ("activeTraderBuffer", context.exchange.active_trader_buffer), + ("orderbook", context.market.orderbook), + ("splines", context.market.splines), + ], + &serde_json::json!({ + "params": { + "assetMint": mint.to_string(), + "liquidationSize": { "inner": context.market.max_liquidation_size }, + "liquidationPrice": { "inner": limit }, + "fillOrKill": false, + } + }), + ) + .unwrap(); + send_signed(locker, &keeper, liquidation) +} + +/// `instruction` in a transaction `signer` signs and pays for, with signature checks on. +fn send_signed( + locker: &SurfnetSvmLocker, + signer: &Keypair, + instruction: Instruction, +) -> Result<(), String> { + locker.with_svm_writer(|svm| { + svm.inner.airdrop(&signer.pubkey(), 1_000_000_000).unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(1_400_000), + instruction, + ], + Some(&signer.pubkey()), + &[signer], + svm.latest_blockhash(), + ); + svm.send_transaction(transaction.into(), false, true) + .map(|_| ()) + .map_err(|failed| format!("{:?}, logs: {:?}", failed.err, failed.meta.logs)) + }) +} + +/// A keeper's ForceCancelRiskIncreasing of `trader`'s orders in `market`, which Phoenix allows any +/// signer while the trader is liquidatable. +fn force_cancel( + exchange: &Exchange, + keeper: Pubkey, + trader: Pubkey, + market: &Market, +) -> Instruction { + phoenix_instruction( + "ForceCancelRiskIncreasing", + &[ + ("phoenixProgram", PHOENIX_PROGRAM_ID), + ("phoenixLogAuthority", log_authority()), + ("globalConfiguration", PHOENIX_GLOBAL_CONFIG), + ("authority", keeper), + ("traderAccount", trader), + ("perpAssetMap", exchange.perp_asset_map), + ("globalTraderIndex", exchange.global_trader_index), + ("activeTraderBuffer", exchange.active_trader_buffer), + ("orderbook", market.orderbook), + ("splines", market.splines), + ], + &serde_json::json!({}), + ) + .unwrap() +} + +async fn apply(locker: &SurfnetSvmLocker, writes: Vec<(Pubkey, Account)>) { + locker.with_svm_writer(|svm| { + for (pubkey, account) in writes { + svm.set_account(&pubkey, account).unwrap(); + } + }); +} + +/// The symbols of the markets where Hawkeye reports a position of `trader`, with its base lots. +async fn held_positions( + locker: &SurfnetSvmLocker, + remote: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + trader: &Pubkey, +) -> Vec<(String, i64)> { + let mut svm = locker.0.write().await; + let exchange = Exchange::load(&mut svm, remote).await.unwrap(); + holdings(&svm, &exchange, trader) + .unwrap() + .into_iter() + .filter(|held| held.base_lots != 0) + .map(|held| (held.market.symbol, held.base_lots)) + .collect() +} + +/// The base lots of `trader`'s position in `symbol`, as Hawkeye reports it; 0 when it has none. +async fn position_lots( + locker: &SurfnetSvmLocker, + remote: &Option<(SurfnetRemoteClient, CommitmentConfig)>, + trader: &Pubkey, + symbol: &str, +) -> i64 { + held_positions(locker, remote, trader) + .await + .into_iter() + .find_map(|(held, lots)| (held == symbol).then_some(lots)) + .unwrap_or(0) +} + +/// A keeper's liquidation of `trader`'s position of `lots` in `symbol` (see [`send_liquidation`]), +/// which must land and leave a smaller position; `case` names the case in the failures. +async fn liquidate_position(locker: &SurfnetSvmLocker, trader: Pubkey, symbol: &str, case: &str) { + let remote = Some((client(), CommitmentConfig::confirmed())); + let held = position_lots(locker, &remote, &trader, symbol).await; + assert_ne!(held, 0, "{case}: no {symbol} position to liquidate"); + send_liquidation(locker, trader, symbol, held > 0) + .await + .unwrap_or_else(|e| panic!("{case}: the keeper's liquidation failed: {e}")); + let left = position_lots(locker, &remote, &trader, symbol).await; + assert!(left.abs() < held.abs(), "{case}: {symbol} {held} -> {left}"); +} + +/// Sets the locker's Clock `slots` past the graph's, its time with it, and returns the new slot. +fn advance_slots(locker: &SurfnetSvmLocker, graph: &PhoenixMainnetGraph, slots: u64) -> u64 { + let mut clock = graph.clock.clone(); + clock.slot += slots; + clock.unix_timestamp += (slots * 2 / 5) as i64; + locker.with_svm_writer(|svm| svm.inner.set_sysvar(&clock)); + clock.slot +} + +/// `phoenix-trader-collateral` lands the effective collateral exactly on its target: for a hot +/// trader in its index record, with its orders resting, and kept when a cancel later takes it out of +/// the index; and again once it is cold. The target comes from the tool's margin, the trader with its +/// orders canceled. +#[tokio::test(flavor = "multi_thread")] +async fn the_collateral_lands_on_its_target_hot_and_cold() { + let remote = Some((client(), CommitmentConfig::confirmed())); + // A keeper liquidates right after Play and once longer than any maker's quotes last on their own. + for delay in [0, 500] { + let (locker, graph) = phoenix_behavior_locker().await; + let trader = graph.trader; + let listed = || { + let index = + locker.with_svm_reader(|svm| local_account(svm, &graph.global_trader_index)); + index_trader_state_range(&index.unwrap(), &trader.to_bytes()).is_ok() + }; + // One liquidation's worth while hot, then every position's worth once cold. + for (hot, every_position) in [(true, false), (false, true)] { + let held = { + let mut svm = locker.0.write().await; + trader_positions_in(&mut svm, &remote, trader, &[]) + .await + .unwrap() + }; + assert!(held.refusal.is_none(), "{trader}: {:?}", held.refusal); + assert_eq!( + held.hot, hot, + "{trader}: hot at first, cold after the cancel" + ); + let view = held.margin; + let (lower, upper) = if every_position { + ( + view.high_risk_margin_quote_lots, + view.backstop_margin_quote_lots, + ) + } else { + ( + view.backstop_margin_quote_lots, + view.maintenance_margin_quote_lots, + ) + }; + let target = ((lower + upper) / 2) as i64; + play( + &locker, + graph.clock.slot, + vec![set_collateral(trader, target)], + ) + .await; + let prepared = hawkeye_margin(&locker, &graph); + assert_eq!( + prepared.effective_collateral_quote_lots, target, + "{trader} (hot: {hot}): Play lands on the target" + ); + assert_eq!( + prepared.is_liquidatable, 1, + "{trader}: liquidatable after Play" + ); + if hot { + assert!( + listed(), + "{trader}: the collateral alone leaves it in the index" + ); + // Phoenix carries the record to the Trader account. The cancel touches the markets, + // which moves the effective collateral a little, as a keeper's instructions do. + play(&locker, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + assert!(!listed(), "{trader}: the cancel takes it out of the index"); + let kept = hawkeye_margin(&locker, &graph).effective_collateral_quote_lots; + let band = view.maintenance_margin_quote_lots - view.backstop_margin_quote_lots; + assert!( + kept.abs_diff(target) <= band / 20, + "{trader}: the cancel keeps the collateral written while hot: {kept} from {target}" + ); + } + } + + let index = locker.with_svm_reader(|svm| local_account(svm, &graph.global_trader_index)); + assert!(index_trader_state_range(&index.unwrap(), &trader.to_bytes()).is_err()); + + advance_slots(&locker, &graph, delay); + let (symbol, _) = held_positions(&locker, &remote, &trader).await.remove(0); + liquidate_position(&locker, trader, &symbol, &format!("{trader} +{delay}")).await; + } +} + +/// A trader that left Phoenix's index upstream after the local VM copied the index, its account +/// copied only then, is cold in its account while the local index still lists it. +/// `phoenix-trader-collateral` lands on its target there, a cancel keeps it, and a keeper +/// liquidates the trader. +#[tokio::test(flavor = "multi_thread")] +async fn the_collateral_lands_on_a_trader_whose_account_went_cold_after_the_index_was_copied() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let (locker, graph) = phoenix_behavior_locker().await; + let trader = graph.trader; + let listed = || { + let index = locker.with_svm_reader(|svm| local_account(svm, &graph.global_trader_index)); + index_trader_state_range(&index.unwrap(), &trader.to_bytes()).is_ok() + }; + let hot = || { + let account = locker.with_svm_reader(|svm| svm.get_account(&trader)); + TraderHeader::try_read_from_account_bytes(&account.unwrap().unwrap().data) + .unwrap() + .trader_state + .is_hot() + }; + // Phoenix's own cancel takes the trader out of its index, then every other account goes back + // to before it: the account copied after the trader left, the index before. + let mut others: Vec = graph + .accounts + .iter() + .map(|(key, _)| *key) + .chain( + graph + .markets + .iter() + .flat_map(|(_, book, spline)| [*book, *spline]), + ) + .filter(|key| *key != trader) + .collect(); + others.sort(); + others.dedup(); + let before: Vec<(Pubkey, Account)> = locker.with_svm_reader(|svm| { + others + .iter() + .filter_map(|key| svm.get_account(key).unwrap().map(|account| (*key, account))) + .collect() + }); + play(&locker, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + apply(&locker, before).await; + assert!( + listed() && !hot(), + "{trader}: listed in the index, cold in its account" + ); + + let held = { + let mut svm = locker.0.write().await; + trader_positions_in(&mut svm, &remote, trader, &[]) + .await + .unwrap() + }; + assert!(held.refusal.is_none(), "{trader}: {:?}", held.refusal); + let view = held.margin; + let target = + ((view.backstop_margin_quote_lots + view.maintenance_margin_quote_lots) / 2) as i64; + play( + &locker, + graph.clock.slot, + vec![set_collateral(trader, target)], + ) + .await; + let prepared = hawkeye_margin(&locker, &graph); + assert_eq!( + prepared.effective_collateral_quote_lots, target, + "{trader}: Play lands on the target" + ); + assert_eq!( + prepared.is_liquidatable, 1, + "{trader}: liquidatable after Play" + ); + assert!( + listed() && hot(), + "{trader}: hot again, as the index lists it" + ); + + // The cancel copies the index record, which holds the collateral, to the account. + play(&locker, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + assert!(!listed(), "{trader}: the cancel takes it out of the index"); + let kept = hawkeye_margin(&locker, &graph).effective_collateral_quote_lots; + let band = view.maintenance_margin_quote_lots - view.backstop_margin_quote_lots; + assert!( + kept.abs_diff(target) <= band / 20, + "{trader}: the cancel keeps the collateral: {kept} from {target}" + ); + let (symbol, _) = held_positions(&locker, &remote, &trader).await.remove(0); + liquidate_position(&locker, trader, &symbol, &trader.to_string()).await; +} + +/// `phoenix-trader-cancel-orders` cancels a hot trader's resting orders on every market, which takes +/// it out of Phoenix's index; its positions stay open. On a cold trader it changes nothing, and a +/// trader the surfnet has not loaded yet is fetched first. +#[tokio::test(flavor = "multi_thread")] +async fn canceling_orders_takes_a_hot_trader_out_of_the_index_and_keeps_its_positions() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let (locker, graph) = phoenix_behavior_locker().await; + let trader = graph.trader; + let listed_trader = |trader: Pubkey| { + let index = locker.with_svm_reader(|svm| local_account(svm, &graph.global_trader_index)); + index_trader_state_range(&index.unwrap(), &trader.to_bytes()).is_ok() + }; + let listed = || listed_trader(trader); + let cancel = || vec![cancel_orders(trader, None)]; + assert!(listed(), "{trader}: hot at first"); + let positions = held_positions(&locker, &remote, &trader).await; + + play(&locker, graph.clock.slot, cancel()).await; + assert!(!listed(), "{trader}: the cancel takes it out of the index"); + assert_eq!( + held_positions(&locker, &remote, &trader).await, + positions, + "{trader}: positions stay open" + ); + let order_only = { + let mut svm = locker.0.write().await; + let exchange = Exchange::load(&mut svm, &remote).await.unwrap(); + holdings(&svm, &exchange, &trader) + .unwrap() + .into_iter() + .filter(|held| held.base_lots == 0) + .count() + }; + assert_eq!( + order_only, 0, + "{trader}: no market is left with resting orders alone" + ); + let canceled = hawkeye_margin(&locker, &graph); + + play(&locker, graph.clock.slot, cancel()).await; + let again = hawkeye_margin(&locker, &graph); + assert_eq!( + ( + again.effective_collateral_quote_lots, + again.maintenance_margin_quote_lots, + ), + ( + canceled.effective_collateral_quote_lots, + canceled.maintenance_margin_quote_lots, + ), + "{trader}: cold, a second cancel changes nothing" + ); + + // Any hot trader the graph did not copy, other than a spline market maker the cancel refuses. + let unloaded: Vec = + index_trader_state_ranges(graph.account(&graph.global_trader_index)) + .unwrap() + .into_iter() + .map(|(other, _)| other) + .filter(|other| { + locker.with_svm_reader(|svm| svm.inner.get_account(other).unwrap().is_none()) + }) + .take(5) + .collect(); + let mut taken_out = None; + for other in unloaded { + play(&locker, graph.clock.slot, vec![cancel_orders(other, None)]).await; + if !listed_trader(other) { + taken_out = Some(other); + break; + } + } + assert!( + taken_out.is_some(), + "a hot trader the surfnet had not loaded is fetched and taken out of the index" + ); +} + +/// `phoenix-trader-cancel-orders` with one market cancels the trader's orders there and leaves its +/// orders on the other markets, and its place in the index, as they were; with several markets +/// separated by commas, it cancels on each, and a repeated or empty entry changes nothing. An +/// unknown market, or a symbol that is not text, refuses the whole override, so nothing is +/// canceled. An empty symbol cancels the rest and takes the trader out of the index. +#[tokio::test(flavor = "multi_thread")] +async fn canceling_orders_on_named_markets_leaves_the_others() { + let graph = phoenix_mainnet_graph().await; + let locker = phoenix_surfnet(&graph).await; + let local = LocalTrader::create(&locker, 10_000).await.unwrap(); + let symbols = ["SOL", "BTC", "ETH", "HYPE"]; + for symbol in symbols { + local + .place_limit_order(&locker, symbol, 100, 5) + .await + .unwrap(); + local + .place_limit_order(&locker, symbol, -100, 5) + .await + .unwrap(); + } + let trader = local.account; + let index = || { + locker + .with_svm_reader(|svm| local_account(svm, &graph.global_trader_index)) + .unwrap() + }; + let listed = || index_trader_state_range(&index(), &trader.to_bytes()).is_ok(); + + // Resting orders name their trader by its node in the index, numbered from 1 after the + // index's 96-byte header in 64-byte nodes, the TraderState 48 bytes into its node. + let range = index_trader_state_range(&index(), &trader.to_bytes()).unwrap(); + let node = ((range.start - 96 - 48) / 64 + 1) as u32; + let map = graph.account(&graph.perp_asset_map); + let orders_of = || { + symbols.map(|symbol| { + let orderbook = Market::find(&graph.perp_asset_map, &map.data, symbol) + .unwrap() + .orderbook; + let book = locker + .with_svm_reader(|svm| local_account(svm, &orderbook)) + .unwrap(); + let book = Orderbook::try_from_account_bytes(&book.data).unwrap(); + let count = book + .bid_orders() + .chain(book.ask_orders()) + .filter(|order| { + order + .order() + .trader_position_id() + .trader_id() + .as_u32_checked() + == Some(node) + }) + .count(); + (symbol, count) + }) + }; + let placed = [("SOL", 2), ("BTC", 2), ("ETH", 2), ("HYPE", 2)]; + assert_eq!(orders_of(), placed, "{trader}: a bid and an ask on each"); + + let unknown = configure( + &locker, + "phoenix-trader-cancel-orders", + graph.perp_asset_map, + &[ + ("trader", trader.to_string()), + ("symbol", "SOL, NOT-A-MARKET".to_string()), + ], + ) + .await; + assert!( + unknown + .as_ref() + .is_err_and(|e| e.contains("Phoenix lists no market NOT-A-MARKET")), + "{unknown:?}" + ); + play( + &locker, + graph.clock.slot, + vec![cancel_orders(trader, Some("SOL, NOT-A-MARKET"))], + ) + .await; + assert_eq!( + orders_of(), + placed, + "{trader}: Play cancels nothing, SOL included" + ); + + let mut as_list = cancel_orders(trader, None); + as_list + .values + .insert("symbol".to_string(), serde_json::json!(["SOL"])); + play(&locker, graph.clock.slot, vec![as_list]).await; + assert_eq!( + orders_of(), + placed, + "{trader}: a symbol that is not text cancels nothing" + ); + + play( + &locker, + graph.clock.slot, + vec![cancel_orders(trader, Some("BTC"))], + ) + .await; + assert_eq!( + orders_of(), + [("SOL", 2), ("BTC", 0), ("ETH", 2), ("HYPE", 2)], + "{trader}: BTC's orders go, the others stay" + ); + assert!( + listed(), + "{trader}: still in the index with orders on other markets" + ); + + let list = "SOL, , ETH, SOL"; + play( + &locker, + graph.clock.slot, + vec![cancel_orders(trader, Some(list))], + ) + .await; + assert_eq!( + orders_of(), + [("SOL", 0), ("BTC", 0), ("ETH", 0), ("HYPE", 2)], + "{trader}: '{list}' cancels on SOL and ETH only" + ); + assert!(listed(), "{trader}: still in the index with orders on HYPE"); + + play(&locker, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + assert_eq!( + orders_of(), + [("SOL", 0), ("BTC", 0), ("ETH", 0), ("HYPE", 0)], + "{trader}: an empty symbol cancels the rest" + ); + assert!( + !listed(), + "{trader}: with no orders left it leaves the index" + ); +} + +/// The margin the tool predicts after a move is the margin Phoenix computes once the move plays, +/// and the collateral set from it leaves the trader for a keeper after moves up to 35%. Without that +/// collateral a big move takes a trader straight past the band below zero, where Phoenix refuses to +/// liquidate it. +#[tokio::test(flavor = "multi_thread")] +async fn the_margin_after_moves_is_the_margin_once_the_moves_play() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let graph = phoenix_mainnet_graph().await; + let margin_of = |locker: &SurfnetSvmLocker, trader: Pubkey| { + MarginView::from_return_data(&hawkeye_view(locker, &graph, VIEW_MARGIN, &[trader])).unwrap() + }; + let mark = |locker: &SurfnetSvmLocker, symbol: &str| { + let map = locker.with_svm_reader(|svm| local_account(svm, &graph.perp_asset_map)); + Market::find(&graph.perp_asset_map, &map.unwrap().data, symbol) + .unwrap() + .mark_ticks + }; + // About 10x: 1,000 USDC against some 10,000 USDC of SOL long and BTC short, within the 25x and + // 40x Phoenix opens, so a 20% move against both takes more than the deposit. The bid makes the + // trader hot. + let positions = [("SOL", 8_000_i64), ("BTC", -200)]; + let leveraged = { + let graph = &graph; + move || async move { + let locker = phoenix_surfnet(graph).await; + let trader = LocalTrader::create(&locker, 1_000).await.unwrap(); + for (symbol, lots) in positions { + trader + .place_market_order(&locker, symbol, lots) + .await + .unwrap(); + } + trader + .place_limit_order(&locker, "SOL", 1_000, 5) + .await + .unwrap(); + (locker, trader.account) + } + }; + let (biggest, biggest_lots) = positions[0]; + + for percent in [2_u64, 10, 20, 35] { + let case = format!("{percent}% against"); + let (locker, trader) = leveraged().await; + // Every market moves against the trader's position there. + let moves: Vec<(String, u64)> = positions + .iter() + .map(|(symbol, lots)| { + let factor = if *lots > 0 { + 100 - percent + } else { + 100 + percent + }; + (symbol.to_string(), mark(&locker, symbol) * factor / 100) + }) + .collect(); + let predicted = { + let mut svm = locker.0.write().await; + trader_positions_in(&mut svm, &remote, trader, &moves) + .await + .unwrap_or_else(|e| panic!("{case}: {e}")) + }; + assert!( + predicted.hot && predicted.refusal.is_none(), + "{case}: hot, and the templates take it: {:?}", + predicted.refusal + ); + let now = predicted.margin; + let after = predicted.margin_after_moves.unwrap(); + let band = after.maintenance_margin_quote_lots - after.backstop_margin_quote_lots; + let target = + ((after.backstop_margin_quote_lots + after.maintenance_margin_quote_lots) / 2) as i64; + let cushion = |view: &MarginView| { + view.effective_collateral_quote_lots - view.maintenance_margin_quote_lots as i64 + }; + assert!( + cushion(&after) < cushion(&now), + "{case}: the move pushes the trader toward liquidation" + ); + let market_moves: Vec<_> = moves + .iter() + .flat_map(|(symbol, ticks)| move_and_uncross(symbol, *ticks)) + .collect(); + + // The collateral lands a slot before the move, which changes it by what the tool predicts. + let wanted = + now.effective_collateral_quote_lots - after.effective_collateral_quote_lots + target; + play( + &locker, + graph.clock.slot, + vec![cancel_orders(trader, None), set_collateral(trader, wanted)], + ) + .await; + assert_eq!( + margin_of(&locker, trader).is_liquidatable, + 0, + "{case}: healthy until the move" + ); + let next = advance_slots(&locker, &graph, 1); + play(&locker, next, market_moves.clone()).await; + for (symbol, ticks) in &moves { + let reached = mark(&locker, symbol); + assert!( + reached.abs_diff(*ticks) <= ticks / 1_000, + "{case}: {symbol}'s mark reached {reached}, not {ticks} ticks" + ); + } + + let played = margin_of(&locker, trader); + let effective = played.effective_collateral_quote_lots; + assert_eq!( + played.is_liquidatable, 1, + "{case}: liquidatable after the move" + ); + assert!( + effective > played.backstop_margin_quote_lots as i64 + && effective < played.maintenance_margin_quote_lots as i64, + "{case}: {effective} is between backstop and maintenance" + ); + assert!( + effective.abs_diff(target) <= band / 20, + "{case}: {effective} is within 5% of the band from {target}" + ); + liquidate_position(&locker, trader, biggest, &case).await; + + if percent >= 20 { + let (bare, trader) = leveraged().await; + play(&bare, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + let next = advance_slots(&bare, &graph, 1); + play(&bare, next, market_moves).await; + let left = margin_of(&bare, trader).effective_collateral_quote_lots; + assert!( + left < 0, + "{case}, without the collateral: {left} is below zero" + ); + let refused = send_liquidation(&bare, trader, biggest, biggest_lots > 0).await; + assert!( + refused + .as_ref() + .is_err_and(|e| e.to_lowercase().contains("underwater")), + "{case}, without the collateral: refused as underwater: {refused:?}" + ); + } + } +} + +/// An oracle-only move against a trader's position moves its margin while the order book stays where +/// it was; with its collateral then set in the band the move leaves, a keeper's liquidation fills +/// against that book. +#[tokio::test(flavor = "multi_thread")] +async fn an_oracle_only_move_leaves_a_trader_liquidatable_for_a_keeper() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let (locker, graph) = phoenix_behavior_locker().await; + let trader = graph.trader; + play(&locker, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + // A cancel freezes the mark for risk in its slot, so the move plays a slot later. + let slot = advance_slots(&locker, &graph, 1); + let (symbol, lots) = held_positions(&locker, &remote, &trader) + .await + .into_iter() + .max_by_key(|(_, lots)| lots.unsigned_abs()) + .unwrap(); + let map = locker.with_svm_reader(|svm| local_account(svm, &graph.perp_asset_map)); + let market = phoenix_markets(graph.perp_asset_map, &map.unwrap()) + .unwrap() + .into_iter() + .find(|market| market.symbol == symbol) + .unwrap(); + let quotes = || { + let view = hawkeye_bbo_for_market(&graph, &locker, market.orderbook, market.splines); + (view.best_bid_ticks, view.best_ask_ticks) + }; + let book = quotes(); + let before = hawkeye_margin(&locker, &graph); + let change = if lots > 0 { "-5" } else { "5" }; + let oracle_move = phoenix_override( + "phoenix-oracle-price", + graph.perp_asset_map, + &[("symbol", symbol.as_str()), ("change_percent", change)], + ); + play(&locker, slot, vec![oracle_move]).await; + assert_eq!( + quotes(), + book, + "{trader}: the oracle move leaves the {symbol} book's best bid and ask" + ); + let moved = hawkeye_margin(&locker, &graph); + assert!( + moved.effective_collateral_quote_lots < before.effective_collateral_quote_lots, + "{trader}: the oracle move against its {symbol} position lowers its effective collateral: \ + {} -> {}", + before.effective_collateral_quote_lots, + moved.effective_collateral_quote_lots + ); + + let target = + ((moved.backstop_margin_quote_lots + moved.maintenance_margin_quote_lots) / 2) as i64; + play(&locker, slot, vec![set_collateral(trader, target)]).await; + let prepared = hawkeye_margin(&locker, &graph); + assert_eq!( + ( + prepared.effective_collateral_quote_lots, + prepared.is_liquidatable + ), + (target, 1), + "{trader}: liquidatable in the band the oracle move leaves" + ); + liquidate_position(&locker, trader, &symbol, &trader.to_string()).await; +} + +/// A higher maintenance factor on a trader's biggest market makes a trader just above its old +/// maintenance margin liquidatable without any price change, and a keeper liquidates it. +#[tokio::test(flavor = "multi_thread")] +async fn a_higher_maintenance_factor_leaves_a_trader_liquidatable_for_a_keeper() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let (locker, graph) = phoenix_behavior_locker().await; + let trader = graph.trader; + play(&locker, graph.clock.slot, vec![cancel_orders(trader, None)]).await; + let biggest = { + let mut svm = locker.0.write().await; + let exchange = Exchange::load(&mut svm, &remote).await.unwrap(); + holdings(&svm, &exchange, &trader) + .unwrap() + .into_iter() + .filter(|held| held.base_lots != 0) + .max_by_key(|held| held.maintenance_margin_quote_lots) + .unwrap() + }; + let symbol = biggest.market.symbol.as_str(); + let factor = u64::from(biggest.market.risk_factors[0]); + let raised = (factor * 3 / 2).min(10_000); + assert!( + raised > factor, + "{trader}: {symbol}'s maintenance factor {factor} is already the highest" + ); + // Half the margin the raise adds to that market above the total, so the raise outgrows it. + let added = biggest.maintenance_margin_quote_lots * (raised - factor) / factor; + let view = hawkeye_margin(&locker, &graph); + let target = (view.maintenance_margin_quote_lots + added / 2) as i64; + play( + &locker, + graph.clock.slot, + vec![set_collateral(trader, target)], + ) + .await; + assert_eq!( + hawkeye_margin(&locker, &graph).is_liquidatable, + 0, + "{trader}: healthy under the old factor" + ); + + // Phoenix keeps the maintenance factor at or below the cancel order factor, which is raised + // only when it is lower. + let map = locker.with_svm_reader(|svm| local_account(svm, &graph.perp_asset_map)); + let cancel_factor = PerpAssetMap::try_from_account_bytes(&map.unwrap().data) + .unwrap() + .find_by_symbol(symbol) + .unwrap() + .unwrap() + .metadata + .risk_params() + .cancel_order_risk_factor; + let cancel_is_lower = u64::from(cancel_factor) < raised; + let raised = raised.to_string(); + let mut changes = Vec::new(); + if cancel_is_lower { + changes.push(phoenix_override( + "phoenix-market-cancel-risk-factor", + graph.perp_asset_map, + &[ + ("symbol", symbol), + ("cancelOrderRiskFactor", raised.as_str()), + ], + )); + } + changes.push(phoenix_override( + "phoenix-market-risk-factors", + graph.perp_asset_map, + &[ + ("symbol", symbol), + ("maintenanceRiskFactor", raised.as_str()), + ], + )); + play(&locker, graph.clock.slot, changes).await; + let raised_margin = hawkeye_margin(&locker, &graph); + assert!( + raised_margin.is_liquidatable == 1 + && raised_margin.effective_collateral_quote_lots + > raised_margin.backstop_margin_quote_lots as i64, + "{trader}: liquidatable for one keeper liquidation under the raised factor: {raised_margin:?}" + ); + liquidate_position(&locker, trader, symbol, &trader.to_string()).await; +} + +/// Phoenix refuses to liquidate a hot trader with risk-increasing orders, but once the trader is +/// liquidatable any keeper can force-cancel those orders and then liquidate it. The collateral +/// lands in the trader's index record, without a cancel, on a target from the tool's margin. +#[tokio::test(flavor = "multi_thread")] +async fn a_keeper_force_cancels_a_liquidatable_hot_traders_orders_and_liquidates_it() { + let remote = Some((client(), CommitmentConfig::confirmed())); + let mut graph = phoenix_mainnet_graph().await; + let locker = phoenix_surfnet(&graph).await; + // Long SOL, with a bid on SOL that would add to the long. + let opened = LocalTrader::create(&locker, 10_000).await.unwrap(); + opened + .place_market_order(&locker, "SOL", 1_000) + .await + .unwrap(); + opened + .place_limit_order(&locker, "SOL", 1_000, 5) + .await + .unwrap(); + let trader = opened.account; + graph.trader = trader; + let held = { + let mut svm = locker.0.write().await; + trader_positions_in(&mut svm, &remote, trader, &[]) + .await + .unwrap() + }; + assert!(held.hot, "{trader}: hot, as its bid makes it"); + + // The middle of the band the orders' cancel leaves, so the trader stays liquidatable for one + // keeper liquidation once its orders are gone. + let target = ((held.margin.backstop_margin_quote_lots + + held.margin.maintenance_margin_quote_lots) + / 2) as i64; + play( + &locker, + graph.clock.slot, + vec![set_collateral(trader, target)], + ) + .await; + assert_eq!( + hawkeye_margin(&locker, &graph).is_liquidatable, + 1, + "{trader}: liquidatable without a cancel" + ); + let refused = send_liquidation(&locker, trader, "SOL", true).await; + assert!( + refused + .as_ref() + .is_err_and(|e| e.contains("risk-increasing")), + "{trader}: the keeper is refused while the bid rests: {refused:?}" + ); + + let keeper = Keypair::new(); + let sol = market_context(&locker, "SOL").await.unwrap(); + send_signed( + &locker, + &keeper, + force_cancel(&sol.exchange, keeper.pubkey(), trader, &sol.market), + ) + .unwrap_or_else(|e| panic!("{trader}: the keeper's force cancel failed: {e}")); + liquidate_position( + &locker, + trader, + "SOL", + &format!("{trader} after the force cancel"), + ) + .await; +} + +/// Keeping the markets usable, which every Phoenix override does first, keeps every maker's spline +/// regions quoting while the clock runs, a moved book's too. +#[tokio::test(flavor = "multi_thread")] +async fn a_thin_book_still_quotes_500_slots_later() { + // A stock market, which only makers' splines keep quoting. + let usable = |upstream_splines: bool| async move { + let (locker, graph) = phoenix_behavior_locker().await; + let markets = phoenix_markets(graph.perp_asset_map, graph.account(&graph.perp_asset_map)); + let market = markets + .unwrap() + .into_iter() + .find(|market| ["AAPL", "NVDA", "TSLA", "GOOGL"].contains(&market.symbol.as_str())) + .expect("Phoenix lists a stock market"); + let remote = Some((client(), CommitmentConfig::confirmed())); + let mut svm = locker.0.write().await; + keep_markets_usable(&mut svm, &remote, graph.clock.slot).await; + MarketContext::load(&mut svm, &remote, &market.symbol) + .await + .unwrap(); + if upstream_splines { + let upstream = fetch(&[market.splines]).await.remove(0); + svm.set_account(&market.splines, upstream).unwrap(); + } + drop(svm); + (locker, graph, market) + }; + let top = |locker: &SurfnetSvmLocker, graph: &PhoenixMainnetGraph, market: &Market| { + let view = hawkeye_bbo_for_market(graph, locker, market.orderbook, market.splines); + (view.best_bid_ticks, view.best_ask_ticks) + }; + + let (kept, graph, market) = usable(false).await; + let (lapsing, _, _) = usable(true).await; + let now = top(&kept, &graph, &market); + let slot = advance_slots(&kept, &graph, 500); + advance_slots(&lapsing, &graph, 500); + let lapsed = top(&lapsing, &graph, &market); + assert_eq!( + top(&kept, &graph, &market), + now, + "{}: the makers still quote", + market.symbol + ); + assert_ne!( + lapsed, now, + "{}: mainnet's own lifespans lapse", + market.symbol + ); + + let target = (now.0 + now.1) / 2 * 98 / 100; + play( + &kept, + slot, + move_and_uncross(&market.symbol, target).to_vec(), + ) + .await; + let moved = top(&kept, &graph, &market); + advance_slots(&kept, &graph, 1_000); + println!( + "{}: {now:?}, mainnet's lifespans +500 {lapsed:?}, moved to {target} {moved:?}", + market.symbol + ); + assert!( + moved.0 < target && target < moved.1, + "{}: quotes around {target}", + market.symbol + ); + assert_eq!( + top(&kept, &graph, &market), + moved, + "{}: the moved makers still quote", + market.symbol + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn the_first_load_brings_the_index_with_the_buffer_and_every_book() { + let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default(); + let remote = Some((client(), CommitmentConfig::confirmed())); + let exchange = Exchange::load(&mut svm, &remote).await.unwrap(); + + let map = svm + .inner + .get_account(&exchange.perp_asset_map) + .unwrap() + .unwrap(); + let books: Vec = phoenix_markets(exchange.perp_asset_map, &map) + .unwrap() + .into_iter() + .map(|market| market.orderbook) + .collect(); + assert!(!books.is_empty()); + for address in [exchange.global_trader_index, exchange.active_trader_buffer] + .into_iter() + .chain(books) + { + assert!( + svm.inner.get_account(&address).unwrap().is_some(), + "{address} did not come with the index" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn positions_of_an_address_without_a_trader_say_so() { + // `Pubkey::new_unique` counts up from low addresses, and some of those exist on mainnet. + let address = Keypair::new().pubkey(); + let error = trader_positions(client(), address, &[]) + .await + .err() + .expect("an address with no account has no positions") + .to_string(); + assert!( + error.contains(&format!("there is no Phoenix Trader at {address}")), + "{error}" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn trader_configuration_reaches_what_phoenix_reads() { + let (locker, graph) = phoenix_behavior_locker().await; + let symbol = "SOL"; + buy_one_lot(&locker, graph.trader, symbol) + .await + .unwrap_or_else(|e| panic!("the trader may place market orders: {e}")); + + // A capability, signed by the risk authority: a blocked market order is refused. + configure( + &locker, + "phoenix-trader-capabilities", + graph.trader, + &[("PlaceMarketOrder", "false".to_string())], + ) + .await + .unwrap(); + let refused = buy_one_lot(&locker, graph.trader, symbol) + .await + .expect_err("PlaceMarketOrder false blocks market orders") + .to_string(); + assert!( + refused.contains("disallows place market orders"), + "{refused}" + ); + configure( + &locker, + "phoenix-trader-capabilities", + graph.trader, + &[("PlaceMarketOrder", "true".to_string())], + ) + .await + .unwrap(); + buy_one_lot(&locker, graph.trader, symbol) + .await + .unwrap_or_else(|e| panic!("true allows them again: {e}")); + + // Fee multipliers, signed by the market authority, land in the record Phoenix reads. + configure( + &locker, + "phoenix-trader-fees", + graph.trader, + &[ + ("makerFeeOverrideMultiplier", "2".to_string()), + ("takerFeeOverrideMultiplier", "3".to_string()), + ], + ) + .await + .unwrap(); + let index = locker + .with_svm_reader(|svm| svm.get_account(&graph.global_trader_index)) + .unwrap() + .unwrap(); + let range = index_trader_state_range(&index, &graph.trader.to_bytes()).unwrap(); + let state = bytemuck::pod_read_unaligned::( + &index.data + [range.start..range.start + size_of::()], + ); + assert_eq!( + ( + state.maker_fee_override_multiplier, + state.taker_fee_override_multiplier + ), + (2, 3) + ); +} diff --git a/crates/mcp/src/surfpool/mod.rs b/crates/mcp/src/surfpool/mod.rs index c45e5d932..3ca1257e8 100644 --- a/crates/mcp/src/surfpool/mod.rs +++ b/crates/mcp/src/surfpool/mod.rs @@ -20,8 +20,10 @@ use surfpool_core::{ scenarios::{ TemplateRegistry, protocols::{ - phoenix_eternal::v1::state_builder::{ - PHOENIX_PERP_ASSET_MAP, build_phoenix_collateral_scenario, phoenix_markets, + phoenix_eternal::v1::{ + market::phoenix_markets, + state_builder::PHOENIX_PERP_ASSET_MAP, + trader::{MarginView, TraderPositions, trader_positions}, }, pump::v1::graduation_builder::{ build_pump_graduation_scenario, pump_graduation_addresses, @@ -159,22 +161,35 @@ pub struct GetTemplateParams { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] -pub struct CreatePhoenixCollateralScenarioParams { - #[schemars(description = "Phoenix Eternal Trader account pubkey.")] - pub trader: String, +pub struct ListPhoenixMarketsParams { #[schemars( - description = "Exact signed collateral target in quote lots, encoded as a decimal string." + description = "The port of the target running local surfnet instance (e.g., 8899, 18899, 28899, etc.). Omit to use the default port, 8899." )] - pub target_quote_lots: String, + pub surfnet_port: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] -pub struct ListPhoenixMarketsParams { +pub struct ListPhoenixTraderPositionsParams { + #[schemars(description = "The address of the trader's Phoenix Trader account.")] + pub trader: String, #[schemars( description = "The port of the target running local surfnet instance (e.g., 8899, 18899, 28899, etc.). Omit to use the default port, 8899." )] pub surfnet_port: Option, + #[schemars( + description = "Markets to move for marginAfterMoves, each { symbol, targetTicks } with targetTicks a decimal string or a number, the ticks phoenix-market-move takes. Omit for none." + )] + #[serde(default)] + pub moves: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct PhoenixMove { + pub symbol: String, + /// A decimal string or a JSON number. + pub target_ticks: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -490,23 +505,6 @@ impl Surfpool { .ok_or_else(|| format!("Studio's config at {endpoint} has no rpc_url")) } - async fn build_phoenix_collateral_scenario_from_surfnet( - &self, - studio_url: &str, - params: &CreatePhoenixCollateralScenarioParams, - ) -> Result { - let trader = Pubkey::from_str(params.trader.trim()) - .map_err(|error| format!("Invalid Trader pubkey: {error}"))?; - // Studio plays the scenario on its own surfnet, so the Trader is checked there. - let rpc_url = self.studio_rpc_url(studio_url).await?; - let accounts = self.fetch_accounts_at(&rpc_url, &[trader]).await?; - let trader_account = accounts[0] - .as_ref() - .ok_or_else(|| format!("Phoenix Trader account {trader} was not found"))?; - build_phoenix_collateral_scenario(trader, trader_account, ¶ms.target_quote_lots) - .map_err(|error| error.to_string()) - } - async fn stage_scenario(&self, scenario: Scenario) -> Result { let endpoint = format!( "http://127.0.0.1:{}/v1/scenarios", @@ -1163,28 +1161,6 @@ impl Surfpool { self.stage_scenario(preparation.scenario).await } - #[tool( - description = "Creates an editable Phoenix Eternal Trader collateral-stress scenario. Requires a Trader pubkey and exact signed quote lots as a decimal string. The Trader is read from the surfnet Studio plays scenarios on. Only lowers collateral: Play skips a target above the trader's current quoteLotCollateral with a warning, since raising it needs a real deposit. quoteLotCollateral excludes the unrealized PnL and funding Phoenix adds for effective collateral, so lowering it by N quote lots lowers effective collateral by N. Makes a single-override scenario; build a multi-slot cascade with create_scenario instead. This prepares risk state; it does not execute liquidation." - )] - async fn create_phoenix_collateral_scenario( - &self, - Parameters(params): Parameters, - ) -> Result { - let studio_url = format!( - "http://127.0.0.1:{}", - CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED - ); - let scenario = match self - .build_phoenix_collateral_scenario_from_surfnet(&studio_url, ¶ms) - .await - { - Ok(scenario) => scenario, - Err(error) => return Ok(scenario_tool_error(error)), - }; - - self.stage_scenario(scenario).await - } - async fn list_phoenix_markets_at(&self, rpc_url: &str) -> Result { let perp_asset_map = PHOENIX_PERP_ASSET_MAP; let maps = self.fetch_accounts_at(rpc_url, &[perp_asset_map]).await?; @@ -1206,8 +1182,8 @@ impl Surfpool { "markTicks": market.mark_ticks, "tickSize": market.tick_size, "baseLotDecimals": market.base_lot_decimals, - "maintenanceRiskFactorBps": market.maintenance_risk_factor_bps, - "backstopRiskFactorBps": market.backstop_risk_factor_bps, + "maintenanceRiskFactorBps": market.risk_factors[0], + "backstopRiskFactorBps": market.risk_factors[1], }) }) .collect::>(), @@ -1235,6 +1211,60 @@ impl Surfpool { } } + async fn list_phoenix_trader_positions_at( + &self, + rpc_url: &str, + trader: &str, + moves: &[PhoenixMove], + ) -> Result { + let trader = Pubkey::from_str(trader.trim()) + .map_err(|_| format!("'{trader}' is not a valid Phoenix Trader account address"))?; + let moves = moves + .iter() + .map(|moved| { + let ticks = match &moved.target_ticks { + serde_json::Value::String(text) => text.trim().parse().ok(), + serde_json::Value::Number(number) => number.as_u64(), + _ => None, + }; + ticks + .map(|ticks| (moved.symbol.clone(), ticks)) + .ok_or_else(|| { + format!("targetTicks of {} must be a whole number", moved.symbol) + }) + }) + .collect::, String>>()?; + let positions = trader_positions(SurfnetRemoteClient::new(rpc_url), trader, &moves) + .await + .map_err(|error| error.to_string())?; + Ok(trader_positions_json(&trader, &positions)) + } + + #[tool( + description = "Lists the markets where one Phoenix Eternal trader holds a position, as Phoenix's Hawkeye program reports them on a copy of the surfnet's state, sorted by symbol. Each position comes with its market symbol, its orderbook address, its side (long or short), its size in base lots (baseLots) and its maintenance margin in quote lots (maintenanceMarginQuoteLots), both decimal strings. Hawkeye reads positions where Phoenix keeps them, so this is right even when the Trader account's own copy is not; never decode the Trader account yourself. hot is true when Phoenix's active trader index lists the trader, such as one with resting orders. margin is Hawkeye's view as phoenix-trader-cancel-orders leaves the trader, a hot trader's orders canceled: collateral, effectiveCollateral, maintenanceMargin, backstopMargin and highRiskMargin in quote lots as decimal strings, and liquidatable. refusal is why phoenix-trader-cancel-orders or phoenix-trader-collateral would refuse the trader, or null. phoenix-trader-collateral takes the effective collateral wanted in USDC, a quote lot being a millionth of a USDC. Given moves, marginAfterMoves is the same view once those markets move to targetTicks and uncross the way phoenix-market-move and phoenix-market-uncross play them, for a trader that should cross into liquidation at a later slot." + )] + async fn list_phoenix_trader_positions( + &self, + Parameters(params): Parameters, + ) -> Result { + match self + .list_phoenix_trader_positions_at( + &format!( + "http://127.0.0.1:{}", + params.surfnet_port.unwrap_or(DEFAULT_RPC_PORT) + ), + ¶ms.trader, + ¶ms.moves, + ) + .await + { + Ok(payload) => Ok(CallToolResult::success(vec![Content::text( + payload.to_string(), + )])), + Err(error) => Ok(scenario_tool_error(error)), + } + } + #[tool( description = "Lists all override templates as a light index: {id, name, description, protocol, accountType, tags, hasLlmContext}. Call this first to pick a templateId, then get_override_template for that one template's full detail (properties, address, llmContext). Constants are resolved with search_constant_options; dynamic_ref properties with the tool named in their source." )] @@ -1513,6 +1543,39 @@ impl ServerHandler for Surfpool { } } +fn trader_positions_json(trader: &Pubkey, held: &TraderPositions) -> serde_json::Value { + let margin = |view: &MarginView| { + serde_json::json!({ + "collateral": view.collateral_quote_lots.to_string(), + "effectiveCollateral": view.effective_collateral_quote_lots.to_string(), + "maintenanceMargin": view.maintenance_margin_quote_lots.to_string(), + "backstopMargin": view.backstop_margin_quote_lots.to_string(), + "highRiskMargin": view.high_risk_margin_quote_lots.to_string(), + "liquidatable": view.is_liquidatable != 0, + }) + }; + serde_json::json!({ + "trader": trader.to_string(), + "positions": held + .positions + .iter() + .map(|position| { + serde_json::json!({ + "symbol": position.symbol, + "orderbook": position.orderbook.to_string(), + "side": if position.base_lots > 0 { "long" } else { "short" }, + "baseLots": position.base_lots.unsigned_abs().to_string(), + "maintenanceMarginQuoteLots": position.maintenance_margin_quote_lots.to_string(), + }) + }) + .collect::>(), + "hot": held.hot, + "margin": margin(&held.margin), + "marginAfterMoves": held.margin_after_moves.as_ref().map(margin), + "refusal": held.refusal, + }) +} + fn unlisted_phoenix_symbols( listing: &serde_json::Value, symbols: &[(String, String)], @@ -1970,11 +2033,122 @@ mod tests { assert!(errors[2].contains("'BONK'") && errors[2].contains("list_phoenix_markets")); } + #[test] + fn trader_positions_keep_the_contract_studio_reads() { + use surfpool_core::scenarios::protocols::phoenix_eternal::v1::trader::TraderPosition; + + let trader = Pubkey::new_unique(); + let orderbook = Pubkey::new_unique(); + let position = |symbol: &str, base_lots| TraderPosition { + symbol: symbol.to_string(), + orderbook, + base_lots, + maintenance_margin_quote_lots: 12_345_678_901, + }; + let mut view = MarginView::default(); + view.collateral_quote_lots = -5_030_916_850; + view.effective_collateral_quote_lots = 41_872_811; + view.maintenance_margin_quote_lots = 11_473_811; + view.backstop_margin_quote_lots = 4_589_524; + view.high_risk_margin_quote_lots = 2_294_761; + view.is_liquidatable = 1; + let mut held = TraderPositions { + positions: vec![position("ETH", 250), position("SOL", -1_000)], + hot: true, + margin: view, + margin_after_moves: None, + refusal: None, + }; + let payload = trader_positions_json(&trader, &held); + assert_eq!( + payload, + serde_json::json!({ + "trader": trader.to_string(), + "positions": [ + { + "symbol": "ETH", + "orderbook": orderbook.to_string(), + "side": "long", + "baseLots": "250", + "maintenanceMarginQuoteLots": "12345678901", + }, + { + "symbol": "SOL", + "orderbook": orderbook.to_string(), + "side": "short", + "baseLots": "1000", + "maintenanceMarginQuoteLots": "12345678901", + }, + ], + "hot": true, + "margin": { + "collateral": "-5030916850", + "effectiveCollateral": "41872811", + "maintenanceMargin": "11473811", + "backstopMargin": "4589524", + "highRiskMargin": "2294761", + "liquidatable": true, + }, + "marginAfterMoves": null, + "refusal": null, + }) + ); + held.margin_after_moves = Some(view); + let payload = trader_positions_json(&trader, &held); + assert_eq!(payload["marginAfterMoves"], payload["margin"]); + let params: ListPhoenixTraderPositionsParams = serde_json::from_value(serde_json::json!({ + "trader": trader.to_string(), + "moves": [{ "symbol": "SOL", "targetTicks": "10446" }, { "symbol": "BTC", "targetTicks": 78545 }], + })) + .unwrap(); + assert_eq!(params.moves[0].target_ticks, "10446"); + assert_eq!(params.moves[1].target_ticks, 78545); + } + + #[tokio::test] + async fn trader_positions_accept_surrounding_address_whitespace() { + let surfpool = Surfpool::new(); + let trader = Pubkey::new_unique().to_string(); + let rpc_url = "not-a-url"; + let expected = surfpool + .list_phoenix_trader_positions_at(rpc_url, &trader, &[]) + .await + .unwrap_err(); + assert!(!expected.contains("is not a valid Phoenix Trader account address")); + + for input in [format!(" {trader} "), format!("\t{trader}\r\n")] { + let error = surfpool + .list_phoenix_trader_positions_at(rpc_url, &input, &[]) + .await + .unwrap_err(); + assert_eq!(error, expected); + } + } + + #[tokio::test] + async fn trader_positions_refuse_an_invalid_address_before_any_call() { + let result = Surfpool::new() + .list_phoenix_trader_positions(Parameters(ListPhoenixTraderPositionsParams { + trader: "not-an-address".to_string(), + surfnet_port: None, + moves: Vec::new(), + })) + .await + .unwrap(); + let payload = json_of(&result); + assert!( + payload["error"] + .as_str() + .is_some_and(|error| error.contains("'not-an-address' is not a valid")), + "{payload}" + ); + } + #[tokio::test] async fn create_scenario_rejects_a_missing_dynamic_ref_value() { let surfpool = Surfpool::new(); let template = TemplateRegistry::new() - .get("phoenix-direct-mark-risk-shock") + .get("phoenix-market-move") .expect("template") .clone(); let mut scenario = surfpool_types::Scenario::new(